Merge tag '2.9.4' into develop

This commit is contained in:
Geno 2021-01-17 23:28:55 +01:00
commit d2cd482a07
38 changed files with 587 additions and 428 deletions

View File

@ -11,7 +11,7 @@ android:
- '.+' - '.+'
before_script: before_script:
- mkdir libs - mkdir libs
- wget -O libs/libwebrtc-m85.aar https://gultsch.de/files/libwebrtc-m85.aar - wget -O libs/libwebrtc-m87.aar https://gultsch.de/files/libwebrtc-m87.aar
script: script:
- ./gradlew assembleConversationsFreeSystemRelease - ./gradlew assembleConversationsFreeSystemRelease
- ./gradlew assembleQuicksyFreeCompatRelease - ./gradlew assembleQuicksyFreeCompatRelease

View File

@ -1,5 +1,14 @@
# Changelog # Changelog
### Version 2.9.4
* minor stability improvements for A/V calls
### Version 2.9.3
* Fixed connectivity issues when different accounts used different SCRAM mechanisms
* Add support for SCRAM-SHA-512
* Allow P2P (Jingle) file transfer with self contact
### Version 2.9.2 ### Version 2.9.2
* Offer Easy Invite generation on supporting servers * Offer Easy Invite generation on supporting servers

View File

@ -80,7 +80,7 @@ dependencies {
implementation 'com.squareup.okhttp3:okhttp:3.12.12' implementation 'com.squareup.okhttp3:okhttp:3.12.12'
implementation 'com.google.guava:guava:27.1-android' implementation 'com.google.guava:guava:27.1-android'
quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.11.1' quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.11.1'
//implementation fileTree(include: ['libwebrtc-m85.aar'], dir: 'libs') //implementation fileTree(include: ['libwebrtc-m87.aar'], dir: 'libs')
implementation 'org.webrtc:google-webrtc:1.0.32006' implementation 'org.webrtc:google-webrtc:1.0.32006'
} }
@ -96,8 +96,8 @@ android {
defaultConfig { defaultConfig {
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 29 targetSdkVersion 29
versionCode 402 versionCode 404
versionName "2.9.2" versionName "2.9.4"
archivesBaseName += "-$versionName" archivesBaseName += "-$versionName"
applicationId "eu.sum7.conversations" applicationId "eu.sum7.conversations"
resValue "string", "applicationId", applicationId resValue "string", "applicationId", applicationId

View File

@ -1,7 +1,10 @@
<?xml version="1.0"?> <?xml version="1.0"?>
<?xml-stylesheet href="../style.xsl" type="text/xsl"?> <?xml-stylesheet href="../style.xsl" type="text/xsl"?>
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<Project xmlns="http://usefulinc.com/ns/doap#" xmlns:foaf="http://xmlns.com/foaf/0.1/" xmlns:xmpp="https://linkmauve.fr/ns/xmpp-doap#"> <Project xmlns="http://usefulinc.com/ns/doap#"
xmlns:foaf="http://xmlns.com/foaf/0.1/"
xmlns:xmpp="https://linkmauve.fr/ns/xmpp-doap#"
xmlns:schema="https://schema.org/">
<name>Conversations</name> <name>Conversations</name>
<created>2014-01-14</created> <created>2014-01-14</created>
@ -22,13 +25,12 @@
<!-- See https://github.com/ewilderj/doap/issues/49 --> <!-- See https://github.com/ewilderj/doap/issues/49 -->
<language>en</language> <language>en</language>
<logo rdf:resource="https://raw.githubusercontent.com/iNPUTmice/Conversations/master/doap.rdf"/> <schema:logo rdf:resource="https://raw.githubusercontent.com/iNPUTmice/Conversations/master/art/ic_launcher.svg"/>
<programming-language>Java</programming-language> <programming-language>Java</programming-language>
<os>Android</os> <os>Android</os>
<!-- TODO: Categories are URIs, find a better location for them. -->
<category rdf:resource="https://linkmauve.fr/ns/xmpp-doap#category-xmpp"/> <category rdf:resource="https://linkmauve.fr/ns/xmpp-doap#category-xmpp"/>
<category rdf:resource="https://linkmauve.fr/ns/xmpp-doap#category-jabber"/> <category rdf:resource="https://linkmauve.fr/ns/xmpp-doap#category-jabber"/>
<category rdf:resource="https://linkmauve.fr/ns/xmpp-doap#category-client"/> <category rdf:resource="https://linkmauve.fr/ns/xmpp-doap#category-client"/>

View File

@ -0,0 +1,4 @@
• Fixed connectivity issues when different accounts used different SCRAM mechanisms
• Add support for SCRAM-SHA-512
• Allow P2P (Jingle) file transfer with self contact
• minor stability improvements for A/V calls

View File

@ -11,4 +11,8 @@ In ogni caso per facilitare puoi creare facilmente un account su chat.sum7.eu, u
<string name="magic_create_text_fixed">Sei stato invitato su %1$s. È già stato scelto un nome utente per te. Ti guideremo nel procedimento per creare un account.\nSarai in grado di comunicare con utenti di altri fornitori dando loro l\'indirizzo XMPP completo.</string> <string name="magic_create_text_fixed">Sei stato invitato su %1$s. È già stato scelto un nome utente per te. Ti guideremo nel procedimento per creare un account.\nSarai in grado di comunicare con utenti di altri fornitori dando loro l\'indirizzo XMPP completo.</string>
<string name="your_server_invitation">Il tuo invito al server</string> <string name="your_server_invitation">Il tuo invito al server</string>
<string name="improperly_formatted_provisioning">Codice di approvvigionamento formattato male</string> <string name="improperly_formatted_provisioning">Codice di approvvigionamento formattato male</string>
</resources> <string name="tap_share_button_send_invite">Tocca il pulsante condividi per inviare al contatto un invito per %1$s.</string>
<string name="if_contact_is_nearby_use_qr">Se il contatto è vicino, può anche scansionare il codice sottostante per accettare il tuo invito.</string>
<string name="easy_invite_share_text">Unisciti a %1$s e chatta con me: %2$s</string>
<string name="share_invite_with">Condividi invito con...</string>
</resources>

View File

@ -9,4 +9,8 @@
<string name="magic_create_text_fixed">Zostałeś zaproszony do %1$s. Nazwa użytkownika została już dla ciebie wybrana. Poprowadzimy ciebie przez proces tworzenia konta.\nBęziesz mógł komunikować się z innymi użytkownikami podając swój adres XMPP.</string> <string name="magic_create_text_fixed">Zostałeś zaproszony do %1$s. Nazwa użytkownika została już dla ciebie wybrana. Poprowadzimy ciebie przez proces tworzenia konta.\nBęziesz mógł komunikować się z innymi użytkownikami podając swój adres XMPP.</string>
<string name="your_server_invitation">Zaproszenie twojego serwera</string> <string name="your_server_invitation">Zaproszenie twojego serwera</string>
<string name="improperly_formatted_provisioning">Niepoprawnie sformatowany kod zaopatrywania</string> <string name="improperly_formatted_provisioning">Niepoprawnie sformatowany kod zaopatrywania</string>
</resources> <string name="tap_share_button_send_invite">Użyj przycisku udostępniania aby wysłać swojemu kontaktowi zaproszenie do %1$s.</string>
<string name="if_contact_is_nearby_use_qr">Jeśli twój kontakt jest blisko może przeskanować kod poniżej aby zaakceptować twoje zaproszenie.</string>
<string name="easy_invite_share_text">Dołącz do %1$s aby porozmawiać ze mną: %2$s</string>
<string name="share_invite_with">Udostępnij zaproszenie...</string>
</resources>

View File

@ -9,4 +9,8 @@
<string name="magic_create_text_fixed">Você foi convidado para %1$s. Um nome de usuário já foi escolhido para você. Nós iremos guiá-lo ao longo do processo de criação de uma conta.\nVocê conseguirá se comunicar com usuários de outros provedores dando a eles seu endereço XMPP completo.</string> <string name="magic_create_text_fixed">Você foi convidado para %1$s. Um nome de usuário já foi escolhido para você. Nós iremos guiá-lo ao longo do processo de criação de uma conta.\nVocê conseguirá se comunicar com usuários de outros provedores dando a eles seu endereço XMPP completo.</string>
<string name="your_server_invitation">Seu convite do servidor</string> <string name="your_server_invitation">Seu convite do servidor</string>
<string name="improperly_formatted_provisioning">Código de provisionamento formatado de maneira imprópria</string> <string name="improperly_formatted_provisioning">Código de provisionamento formatado de maneira imprópria</string>
</resources> <string name="tap_share_button_send_invite">Toque no botão compartilhar para enviar, para seu contato, um convite para %1$s.</string>
<string name="if_contact_is_nearby_use_qr">Se seu contato estiver por perto, ele também pode escanear o código abaixo para aceitar seu convite.</string>
<string name="easy_invite_share_text">Junte-se a %1$s e converse comigo: %2$s</string>
<string name="share_invite_with">Compartilhe o convite com...</string>
</resources>

View File

@ -7,22 +7,24 @@ import eu.siacs.conversations.xml.TagWriter;
public class Anonymous extends SaslMechanism { public class Anonymous extends SaslMechanism {
public Anonymous(TagWriter tagWriter, Account account, SecureRandom rng) { public static final String MECHANISM = "ANONYMOUS";
super(tagWriter, account, rng);
}
@Override public Anonymous(TagWriter tagWriter, Account account, SecureRandom rng) {
public int getPriority() { super(tagWriter, account, rng);
return 0; }
}
@Override @Override
public String getMechanism() { public int getPriority() {
return "ANONYMOUS"; return 0;
} }
@Override @Override
public String getClientFirstMessage() { public String getMechanism() {
return ""; return MECHANISM;
} }
@Override
public String getClientFirstMessage() {
return "";
}
} }

View File

@ -12,79 +12,82 @@ import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.xml.TagWriter; import eu.siacs.conversations.xml.TagWriter;
public class DigestMd5 extends SaslMechanism { public class DigestMd5 extends SaslMechanism {
public DigestMd5(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
super(tagWriter, account, rng);
}
@Override public static final String MECHANISM = "DIGEST-MD5";
public int getPriority() {
return 10;
}
@Override public DigestMd5(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
public String getMechanism() { super(tagWriter, account, rng);
return "DIGEST-MD5"; }
}
private State state = State.INITIAL; @Override
public int getPriority() {
return 10;
}
@Override @Override
public String getResponse(final String challenge) throws AuthenticationException { public String getMechanism() {
switch (state) { return MECHANISM;
case INITIAL: }
state = State.RESPONSE_SENT;
final String encodedResponse;
try {
final Tokenizer tokenizer = new Tokenizer(Base64.decode(challenge, Base64.DEFAULT));
String nonce = "";
for (final String token : tokenizer) {
final String[] parts = token.split("=", 2);
if (parts[0].equals("nonce")) {
nonce = parts[1].replace("\"", "");
} else if (parts[0].equals("rspauth")) {
return "";
}
}
final String digestUri = "xmpp/" + account.getServer();
final String nonceCount = "00000001";
final String x = account.getUsername() + ":" + account.getServer() + ":"
+ account.getPassword();
final MessageDigest md = MessageDigest.getInstance("MD5");
final byte[] y = md.digest(x.getBytes(Charset.defaultCharset()));
final String cNonce = CryptoHelper.random(100,rng);
final byte[] a1 = CryptoHelper.concatenateByteArrays(y,
(":" + nonce + ":" + cNonce).getBytes(Charset.defaultCharset()));
final String a2 = "AUTHENTICATE:" + digestUri;
final String ha1 = CryptoHelper.bytesToHex(md.digest(a1));
final String ha2 = CryptoHelper.bytesToHex(md.digest(a2.getBytes(Charset
.defaultCharset())));
final String kd = ha1 + ":" + nonce + ":" + nonceCount + ":" + cNonce
+ ":auth:" + ha2;
final String response = CryptoHelper.bytesToHex(md.digest(kd.getBytes(Charset
.defaultCharset())));
final String saslString = "username=\"" + account.getUsername()
+ "\",realm=\"" + account.getServer() + "\",nonce=\""
+ nonce + "\",cnonce=\"" + cNonce + "\",nc=" + nonceCount
+ ",qop=auth,digest-uri=\"" + digestUri + "\",response="
+ response + ",charset=utf-8";
encodedResponse = Base64.encodeToString(
saslString.getBytes(Charset.defaultCharset()),
Base64.NO_WRAP);
} catch (final NoSuchAlgorithmException e) {
throw new AuthenticationException(e);
}
return encodedResponse; private State state = State.INITIAL;
case RESPONSE_SENT:
state = State.VALID_SERVER_RESPONSE; @Override
break; public String getResponse(final String challenge) throws AuthenticationException {
case VALID_SERVER_RESPONSE: switch (state) {
if (challenge==null) { case INITIAL:
return null; //everything is fine state = State.RESPONSE_SENT;
} final String encodedResponse;
default: try {
throw new InvalidStateException(state); final Tokenizer tokenizer = new Tokenizer(Base64.decode(challenge, Base64.DEFAULT));
} String nonce = "";
return null; for (final String token : tokenizer) {
} final String[] parts = token.split("=", 2);
if (parts[0].equals("nonce")) {
nonce = parts[1].replace("\"", "");
} else if (parts[0].equals("rspauth")) {
return "";
}
}
final String digestUri = "xmpp/" + account.getServer();
final String nonceCount = "00000001";
final String x = account.getUsername() + ":" + account.getServer() + ":"
+ account.getPassword();
final MessageDigest md = MessageDigest.getInstance("MD5");
final byte[] y = md.digest(x.getBytes(Charset.defaultCharset()));
final String cNonce = CryptoHelper.random(100, rng);
final byte[] a1 = CryptoHelper.concatenateByteArrays(y,
(":" + nonce + ":" + cNonce).getBytes(Charset.defaultCharset()));
final String a2 = "AUTHENTICATE:" + digestUri;
final String ha1 = CryptoHelper.bytesToHex(md.digest(a1));
final String ha2 = CryptoHelper.bytesToHex(md.digest(a2.getBytes(Charset
.defaultCharset())));
final String kd = ha1 + ":" + nonce + ":" + nonceCount + ":" + cNonce
+ ":auth:" + ha2;
final String response = CryptoHelper.bytesToHex(md.digest(kd.getBytes(Charset
.defaultCharset())));
final String saslString = "username=\"" + account.getUsername()
+ "\",realm=\"" + account.getServer() + "\",nonce=\""
+ nonce + "\",cnonce=\"" + cNonce + "\",nc=" + nonceCount
+ ",qop=auth,digest-uri=\"" + digestUri + "\",response="
+ response + ",charset=utf-8";
encodedResponse = Base64.encodeToString(
saslString.getBytes(Charset.defaultCharset()),
Base64.NO_WRAP);
} catch (final NoSuchAlgorithmException e) {
throw new AuthenticationException(e);
}
return encodedResponse;
case RESPONSE_SENT:
state = State.VALID_SERVER_RESPONSE;
break;
case VALID_SERVER_RESPONSE:
if (challenge == null) {
return null; //everything is fine
}
default:
throw new InvalidStateException(state);
}
return null;
}
} }

View File

@ -1,6 +1,7 @@
package eu.siacs.conversations.crypto.sasl; package eu.siacs.conversations.crypto.sasl;
import android.util.Base64; import android.util.Base64;
import java.security.SecureRandom; import java.security.SecureRandom;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
@ -8,22 +9,24 @@ import eu.siacs.conversations.xml.TagWriter;
public class External extends SaslMechanism { public class External extends SaslMechanism {
public External(TagWriter tagWriter, Account account, SecureRandom rng) { public static final String MECHANISM = "EXTERNAL";
super(tagWriter, account, rng);
}
@Override public External(TagWriter tagWriter, Account account, SecureRandom rng) {
public int getPriority() { super(tagWriter, account, rng);
return 25; }
}
@Override @Override
public String getMechanism() { public int getPriority() {
return "EXTERNAL"; return 25;
} }
@Override @Override
public String getClientFirstMessage() { public String getMechanism() {
return Base64.encodeToString(account.getJid().asBareJid().toEscapedString().getBytes(),Base64.NO_WRAP); return MECHANISM;
} }
@Override
public String getClientFirstMessage() {
return Base64.encodeToString(account.getJid().asBareJid().toEscapedString().getBytes(), Base64.NO_WRAP);
}
} }

View File

@ -8,27 +8,30 @@ import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.xml.TagWriter; import eu.siacs.conversations.xml.TagWriter;
public class Plain extends SaslMechanism { public class Plain extends SaslMechanism {
public Plain(final TagWriter tagWriter, final Account account) {
super(tagWriter, account, null);
}
@Override public static final String MECHANISM = "PLAIN";
public int getPriority() {
return 10;
}
@Override public Plain(final TagWriter tagWriter, final Account account) {
public String getMechanism() { super(tagWriter, account, null);
return "PLAIN"; }
}
@Override @Override
public String getClientFirstMessage() { public int getPriority() {
return getMessage(account.getUsername(), account.getPassword()); return 10;
} }
public static String getMessage(String username, String password) { @Override
final String message = '\u0000' + username + '\u0000' + password; public String getMechanism() {
return Base64.encodeToString(message.getBytes(Charset.defaultCharset()), Base64.NO_WRAP); return MECHANISM;
} }
@Override
public String getClientFirstMessage() {
return getMessage(account.getUsername(), account.getPassword());
}
public static String getMessage(String username, String password) {
final String message = '\u0000' + username + '\u0000' + password;
return Base64.encodeToString(message.getBytes(Charset.defaultCharset()), Base64.NO_WRAP);
}
} }

View File

@ -7,60 +7,63 @@ import eu.siacs.conversations.xml.TagWriter;
public abstract class SaslMechanism { public abstract class SaslMechanism {
final protected TagWriter tagWriter; final protected TagWriter tagWriter;
final protected Account account; final protected Account account;
final protected SecureRandom rng; final protected SecureRandom rng;
protected enum State { protected enum State {
INITIAL, INITIAL,
AUTH_TEXT_SENT, AUTH_TEXT_SENT,
RESPONSE_SENT, RESPONSE_SENT,
VALID_SERVER_RESPONSE, VALID_SERVER_RESPONSE,
} }
public static class AuthenticationException extends Exception { public static class AuthenticationException extends Exception {
public AuthenticationException(final String message) { public AuthenticationException(final String message) {
super(message); super(message);
} }
public AuthenticationException(final Exception inner) { public AuthenticationException(final Exception inner) {
super(inner); super(inner);
} }
public AuthenticationException(final String message, final Exception exception) { public AuthenticationException(final String message, final Exception exception) {
super(message,exception); super(message, exception);
} }
} }
public static class InvalidStateException extends AuthenticationException { public static class InvalidStateException extends AuthenticationException {
public InvalidStateException(final String message) { public InvalidStateException(final String message) {
super(message); super(message);
} }
public InvalidStateException(final State state) { public InvalidStateException(final State state) {
this("Invalid state: " + state.toString()); this("Invalid state: " + state.toString());
} }
} }
public SaslMechanism(final TagWriter tagWriter, final Account account, final SecureRandom rng) { public SaslMechanism(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
this.tagWriter = tagWriter; this.tagWriter = tagWriter;
this.account = account; this.account = account;
this.rng = rng; this.rng = rng;
} }
/** /**
* The priority is used to pin the authentication mechanism. If authentication fails, it MAY be retried with another * The priority is used to pin the authentication mechanism. If authentication fails, it MAY be retried with another
* mechanism of the same priority, but MUST NOT be tried with a mechanism of lower priority (to prevent downgrade * mechanism of the same priority, but MUST NOT be tried with a mechanism of lower priority (to prevent downgrade
* attacks). * attacks).
* @return An arbitrary int representing the priority *
*/ * @return An arbitrary int representing the priority
public abstract int getPriority(); */
public abstract int getPriority();
public abstract String getMechanism(); public abstract String getMechanism();
public String getClientFirstMessage() {
return ""; public String getClientFirstMessage() {
} return "";
public String getResponse(final String challenge) throws AuthenticationException { }
return "";
} public String getResponse(final String challenge) throws AuthenticationException {
return "";
}
} }

View File

@ -1,53 +1,74 @@
package eu.siacs.conversations.crypto.sasl; package eu.siacs.conversations.crypto.sasl;
import android.annotation.TargetApi;
import android.os.Build;
import android.util.Base64; import android.util.Base64;
import android.util.LruCache;
import com.google.common.base.Objects;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.bouncycastle.crypto.Digest; import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.macs.HMac; import org.bouncycastle.crypto.macs.HMac;
import org.bouncycastle.crypto.params.KeyParameter; import org.bouncycastle.crypto.params.KeyParameter;
import java.math.BigInteger;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.security.InvalidKeyException; import java.security.InvalidKeyException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.concurrent.ExecutionException;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.xml.TagWriter; import eu.siacs.conversations.xml.TagWriter;
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
abstract class ScramMechanism extends SaslMechanism { abstract class ScramMechanism extends SaslMechanism {
// TODO: When channel binding (SCRAM-SHA1-PLUS) is supported in future, generalize this to indicate support and/or usage. // TODO: When channel binding (SCRAM-SHA1-PLUS) is supported in future, generalize this to indicate support and/or usage.
private final static String GS2_HEADER = "n,,"; private final static String GS2_HEADER = "n,,";
private static final byte[] CLIENT_KEY_BYTES = "Client Key".getBytes(); private static final byte[] CLIENT_KEY_BYTES = "Client Key".getBytes();
private static final byte[] SERVER_KEY_BYTES = "Server Key".getBytes(); private static final byte[] SERVER_KEY_BYTES = "Server Key".getBytes();
private static final LruCache<String, KeyPair> CACHE;
static HMac HMAC;
static Digest DIGEST;
static { protected abstract HMac getHMAC();
CACHE = new LruCache<String, KeyPair>(10) {
protected KeyPair create(final String k) {
// Map keys are "bytesToHex(JID),bytesToHex(password),bytesToHex(salt),iterations,SASL-Mechanism".
// Changing any of these values forces a cache miss. `CryptoHelper.bytesToHex()'
// is applied to prevent commas in the strings breaking things.
final String[] kParts = k.split(",", 5);
try {
final byte[] saltedPassword, serverKey, clientKey;
saltedPassword = hi(CryptoHelper.hexToString(kParts[1]).getBytes(),
Base64.decode(CryptoHelper.hexToString(kParts[2]), Base64.DEFAULT), Integer.parseInt(kParts[3]));
serverKey = hmac(saltedPassword, SERVER_KEY_BYTES);
clientKey = hmac(saltedPassword, CLIENT_KEY_BYTES);
return new KeyPair(clientKey, serverKey); protected abstract Digest getDigest();
} catch (final InvalidKeyException | NumberFormatException e) {
return null; private static final Cache<CacheKey, KeyPair> CACHE = CacheBuilder.newBuilder().maximumSize(10).build();
}
} private static class CacheKey {
}; final String algorithm;
final String password;
final String salt;
final int iterations;
private CacheKey(String algorithm, String password, String salt, int iterations) {
this.algorithm = algorithm;
this.password = password;
this.salt = salt;
this.iterations = iterations;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CacheKey cacheKey = (CacheKey) o;
return iterations == cacheKey.iterations &&
Objects.equal(algorithm, cacheKey.algorithm) &&
Objects.equal(password, cacheKey.password) &&
Objects.equal(salt, cacheKey.salt);
}
@Override
public int hashCode() {
return Objects.hashCode(algorithm, password, salt, iterations);
}
}
private KeyPair getKeyPair(final String password, final String salt, final int iterations) throws ExecutionException {
return CACHE.get(new CacheKey(getHMAC().getAlgorithmName(), password, salt, iterations), () -> {
final byte[] saltedPassword, serverKey, clientKey;
saltedPassword = hi(password.getBytes(), Base64.decode(salt, Base64.DEFAULT), iterations);
serverKey = hmac(saltedPassword, SERVER_KEY_BYTES);
clientKey = hmac(saltedPassword, CLIENT_KEY_BYTES);
return new KeyPair(clientKey, serverKey);
});
} }
private final String clientNonce; private final String clientNonce;
@ -63,20 +84,21 @@ abstract class ScramMechanism extends SaslMechanism {
clientFirstMessageBare = ""; clientFirstMessageBare = "";
} }
private static synchronized byte[] hmac(final byte[] key, final byte[] input) private byte[] hmac(final byte[] key, final byte[] input) throws InvalidKeyException {
throws InvalidKeyException { final HMac hMac = getHMAC();
HMAC.init(new KeyParameter(key)); hMac.init(new KeyParameter(key));
HMAC.update(input, 0, input.length); hMac.update(input, 0, input.length);
final byte[] out = new byte[HMAC.getMacSize()]; final byte[] out = new byte[hMac.getMacSize()];
HMAC.doFinal(out, 0); hMac.doFinal(out, 0);
return out; return out;
} }
public static synchronized byte[] digest(byte[] bytes) { public byte[] digest(byte[] bytes) {
DIGEST.reset(); final Digest digest = getDigest();
DIGEST.update(bytes, 0, bytes.length); digest.reset();
final byte[] out = new byte[DIGEST.getDigestSize()]; digest.update(bytes, 0, bytes.length);
DIGEST.doFinal(out, 0); final byte[] out = new byte[digest.getDigestSize()];
digest.doFinal(out, 0);
return out; return out;
} }
@ -85,7 +107,7 @@ abstract class ScramMechanism extends SaslMechanism {
* pseudorandom function (PRF) and with dkLen == output length of * pseudorandom function (PRF) and with dkLen == output length of
* HMAC() == output length of H(). * HMAC() == output length of H().
*/ */
private static synchronized byte[] hi(final byte[] key, final byte[] salt, final int iterations) private byte[] hi(final byte[] key, final byte[] salt, final int iterations)
throws InvalidKeyException { throws InvalidKeyException {
byte[] u = hmac(key, CryptoHelper.concatenateByteArrays(salt, CryptoHelper.ONE)); byte[] u = hmac(key, CryptoHelper.concatenateByteArrays(salt, CryptoHelper.ONE));
byte[] out = u.clone(); byte[] out = u.clone();
@ -171,15 +193,10 @@ abstract class ScramMechanism extends SaslMechanism {
final byte[] authMessage = (clientFirstMessageBare + ',' + new String(serverFirstMessage) + ',' final byte[] authMessage = (clientFirstMessageBare + ',' + new String(serverFirstMessage) + ','
+ clientFinalMessageWithoutProof).getBytes(); + clientFinalMessageWithoutProof).getBytes();
// Map keys are "bytesToHex(JID),bytesToHex(password),bytesToHex(salt),iterations,SASL-Mechanism". final KeyPair keys;
final KeyPair keys = CACHE.get( try {
CryptoHelper.bytesToHex(CryptoHelper.saslPrep(account.getJid().asBareJid().toEscapedString()).getBytes()) + "," keys = getKeyPair(CryptoHelper.saslPrep(account.getPassword()), salt, iterationCount);
+ CryptoHelper.bytesToHex(CryptoHelper.saslPrep(account.getPassword()).getBytes()) + "," } catch (ExecutionException e) {
+ CryptoHelper.bytesToHex(salt.getBytes()) + ","
+ iterationCount + ","
+ getMechanism()
);
if (keys == null) {
throw new AuthenticationException("Invalid keys generated"); throw new AuthenticationException("Invalid keys generated");
} }
final byte[] clientSignature; final byte[] clientSignature;

View File

@ -1,5 +1,6 @@
package eu.siacs.conversations.crypto.sasl; package eu.siacs.conversations.crypto.sasl;
import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.digests.SHA1Digest; import org.bouncycastle.crypto.digests.SHA1Digest;
import org.bouncycastle.crypto.macs.HMac; import org.bouncycastle.crypto.macs.HMac;
@ -9,22 +10,30 @@ import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.xml.TagWriter; import eu.siacs.conversations.xml.TagWriter;
public class ScramSha1 extends ScramMechanism { public class ScramSha1 extends ScramMechanism {
static {
DIGEST = new SHA1Digest();
HMAC = new HMac(new SHA1Digest());
}
public ScramSha1(final TagWriter tagWriter, final Account account, final SecureRandom rng) { public static final String MECHANISM = "SCRAM-SHA-1";
super(tagWriter, account, rng);
}
@Override @Override
public int getPriority() { protected HMac getHMAC() {
return 20; return new HMac(new SHA1Digest());
} }
@Override @Override
public String getMechanism() { protected Digest getDigest() {
return "SCRAM-SHA-1"; return new SHA1Digest();
} }
public ScramSha1(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
super(tagWriter, account, rng);
}
@Override
public int getPriority() {
return 20;
}
@Override
public String getMechanism() {
return MECHANISM;
}
} }

View File

@ -1,5 +1,6 @@
package eu.siacs.conversations.crypto.sasl; package eu.siacs.conversations.crypto.sasl;
import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.digests.SHA256Digest; import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.crypto.macs.HMac; import org.bouncycastle.crypto.macs.HMac;
@ -9,22 +10,30 @@ import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.xml.TagWriter; import eu.siacs.conversations.xml.TagWriter;
public class ScramSha256 extends ScramMechanism { public class ScramSha256 extends ScramMechanism {
static {
DIGEST = new SHA256Digest();
HMAC = new HMac(new SHA256Digest());
}
public ScramSha256(final TagWriter tagWriter, final Account account, final SecureRandom rng) { public static final String MECHANISM = "SCRAM-SHA-256";
super(tagWriter, account, rng);
}
@Override @Override
public int getPriority() { protected HMac getHMAC() {
return 25; return new HMac(new SHA256Digest());
} }
@Override @Override
public String getMechanism() { protected Digest getDigest() {
return "SCRAM-SHA-256"; return new SHA256Digest();
} }
public ScramSha256(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
super(tagWriter, account, rng);
}
@Override
public int getPriority() {
return 25;
}
@Override
public String getMechanism() {
return MECHANISM;
}
} }

View File

@ -0,0 +1,39 @@
package eu.siacs.conversations.crypto.sasl;
import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.digests.SHA512Digest;
import org.bouncycastle.crypto.macs.HMac;
import java.security.SecureRandom;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.xml.TagWriter;
public class ScramSha512 extends ScramMechanism {
public static final String MECHANISM = "SCRAM-SHA-512";
@Override
protected HMac getHMAC() {
return new HMac(new SHA512Digest());
}
@Override
protected Digest getDigest() {
return new SHA512Digest();
}
public ScramSha512(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
super(tagWriter, account, rng);
}
@Override
public int getPriority() {
return 30;
}
@Override
public String getMechanism() {
return MECHANISM;
}
}

View File

@ -10,69 +10,69 @@ import java.util.NoSuchElementException;
* A tokenizer for GS2 header strings * A tokenizer for GS2 header strings
*/ */
public final class Tokenizer implements Iterator<String>, Iterable<String> { public final class Tokenizer implements Iterator<String>, Iterable<String> {
private final List<String> parts; private final List<String> parts;
private int index; private int index;
public Tokenizer(final byte[] challenge) { public Tokenizer(final byte[] challenge) {
final String challengeString = new String(challenge); final String challengeString = new String(challenge);
parts = new ArrayList<>(Arrays.asList(challengeString.split(","))); parts = new ArrayList<>(Arrays.asList(challengeString.split(",")));
// Trim parts. // Trim parts.
for (int i = 0; i < parts.size(); i++) { for (int i = 0; i < parts.size(); i++) {
parts.set(i, parts.get(i).trim()); parts.set(i, parts.get(i).trim());
} }
index = 0; index = 0;
} }
/** /**
* Returns true if there is at least one more element, false otherwise. * Returns true if there is at least one more element, false otherwise.
* *
* @see #next * @see #next
*/ */
@Override @Override
public boolean hasNext() { public boolean hasNext() {
return parts.size() != index + 1; return parts.size() != index + 1;
} }
/** /**
* Returns the next object and advances the iterator. * Returns the next object and advances the iterator.
* *
* @return the next object. * @return the next object.
* @throws java.util.NoSuchElementException if there are no more elements. * @throws java.util.NoSuchElementException if there are no more elements.
* @see #hasNext * @see #hasNext
*/ */
@Override @Override
public String next() { public String next() {
if (hasNext()) { if (hasNext()) {
return parts.get(index++); return parts.get(index++);
} else { } else {
throw new NoSuchElementException("No such element. Size is: " + parts.size()); throw new NoSuchElementException("No such element. Size is: " + parts.size());
} }
} }
/** /**
* Removes the last object returned by {@code next} from the collection. * Removes the last object returned by {@code next} from the collection.
* This method can only be called once between each call to {@code next}. * This method can only be called once between each call to {@code next}.
* *
* @throws UnsupportedOperationException if removing is not supported by the collection being * @throws UnsupportedOperationException if removing is not supported by the collection being
* iterated. * iterated.
* @throws IllegalStateException if {@code next} has not been called, or {@code remove} has * @throws IllegalStateException if {@code next} has not been called, or {@code remove} has
* already been called after the last call to {@code next}. * already been called after the last call to {@code next}.
*/ */
@Override @Override
public void remove() { public void remove() {
if(index <= 0) { if (index <= 0) {
throw new IllegalStateException("You can't delete an element before first next() method call"); throw new IllegalStateException("You can't delete an element before first next() method call");
} }
parts.remove(--index); parts.remove(--index);
} }
/** /**
* Returns an {@link java.util.Iterator} for the elements in this object. * Returns an {@link java.util.Iterator} for the elements in this object.
* *
* @return An {@code Iterator} instance. * @return An {@code Iterator} instance.
*/ */
@Override @Override
public Iterator<String> iterator() { public Iterator<String> iterator() {
return parts.iterator(); return parts.iterator();
} }
} }

View File

@ -158,8 +158,11 @@ public class MucOptions {
} }
public boolean allowInvites() { public boolean allowInvites() {
final Field field = getRoomInfoForm().getFieldByName("muc#roomconfig_allowinvites"); final Field allowInvitesField = getRoomInfoForm().getFieldByName("muc#roomconfig_allowinvites");
return field != null && "1".equals(field.getValue()); final boolean allowInvites = allowInvitesField != null && "1".equals(allowInvitesField.getValue());
final Field allowMemberInvitesField = getRoomInfoForm().getFieldByName("muc#roomconfig_allowmemberinvites");
final boolean allowMemberInvites = allowMemberInvitesField != null && "1".equals(allowMemberInvitesField.getValue());
return allowInvites || allowMemberInvites;
} }
public boolean canChangeSubject() { public boolean canChangeSubject() {

View File

@ -77,7 +77,7 @@ public class FileBackend {
private static final String FILE_PROVIDER = ".files"; private static final String FILE_PROVIDER = ".files";
private static final float IGNORE_PADDING = 0.15f; private static final float IGNORE_PADDING = 0.15f;
private XmppConnectionService mXmppConnectionService; private final XmppConnectionService mXmppConnectionService;
public FileBackend(XmppConnectionService service) { public FileBackend(XmppConnectionService service) {
this.mXmppConnectionService = service; this.mXmppConnectionService = service;

View File

@ -2,6 +2,8 @@ package eu.siacs.conversations.services;
import android.util.Log; import android.util.Log;
import org.jetbrains.annotations.NotNull;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
@ -615,6 +617,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
} }
} }
@NotNull
@Override @Override
public String toString() { public String toString() {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();

View File

@ -182,7 +182,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
private Toast messageLoaderToast; private Toast messageLoaderToast;
private ConversationsActivity activity; private ConversationsActivity activity;
private boolean reInitRequiredOnStart = true; private boolean reInitRequiredOnStart = true;
private OnClickListener clickToMuc = new OnClickListener() { private final OnClickListener clickToMuc = new OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
@ -192,14 +192,14 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
startActivity(intent); startActivity(intent);
} }
}; };
private OnClickListener leaveMuc = new OnClickListener() { private final OnClickListener leaveMuc = new OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
activity.xmppConnectionService.archiveConversation(conversation); activity.xmppConnectionService.archiveConversation(conversation);
} }
}; };
private OnClickListener joinMuc = new OnClickListener() { private final OnClickListener joinMuc = new OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
@ -207,7 +207,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
} }
}; };
private OnClickListener acceptJoin = new OnClickListener() { private final OnClickListener acceptJoin = new OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
conversation.setAttribute("accept_non_anonymous", true); conversation.setAttribute("accept_non_anonymous", true);
@ -216,7 +216,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
} }
}; };
private OnClickListener enterPassword = new OnClickListener() { private final OnClickListener enterPassword = new OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
@ -231,7 +231,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
}); });
} }
}; };
private OnScrollListener mOnScrollListener = new OnScrollListener() { private final OnScrollListener mOnScrollListener = new OnScrollListener() {
@Override @Override
public void onScrollStateChanged(AbsListView view, int scrollState) { public void onScrollStateChanged(AbsListView view, int scrollState) {
@ -310,7 +310,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
} }
} }
}; };
private EditMessage.OnCommitContentListener mEditorContentListener = new EditMessage.OnCommitContentListener() { private final EditMessage.OnCommitContentListener mEditorContentListener = new EditMessage.OnCommitContentListener() {
@Override @Override
public boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags, Bundle opts, String[] contentMimeTypes) { public boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags, Bundle opts, String[] contentMimeTypes) {
// try to get permission to read the image, if applicable // try to get permission to read the image, if applicable
@ -333,7 +333,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
} }
}; };
private Message selectedMessage; private Message selectedMessage;
private OnClickListener mEnableAccountListener = new OnClickListener() { private final OnClickListener mEnableAccountListener = new OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
final Account account = conversation == null ? null : conversation.getAccount(); final Account account = conversation == null ? null : conversation.getAccount();
@ -343,7 +343,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
} }
} }
}; };
private OnClickListener mUnblockClickListener = new OnClickListener() { private final OnClickListener mUnblockClickListener = new OnClickListener() {
@Override @Override
public void onClick(final View v) { public void onClick(final View v) {
v.post(() -> v.setVisibility(View.INVISIBLE)); v.post(() -> v.setVisibility(View.INVISIBLE));
@ -354,8 +354,8 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
} }
} }
}; };
private OnClickListener mBlockClickListener = this::showBlockSubmenu; private final OnClickListener mBlockClickListener = this::showBlockSubmenu;
private OnClickListener mAddBackClickListener = new OnClickListener() { private final OnClickListener mAddBackClickListener = new OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
@ -366,8 +366,8 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
} }
} }
}; };
private View.OnLongClickListener mLongPressBlockListener = this::showBlockSubmenu; private final View.OnLongClickListener mLongPressBlockListener = this::showBlockSubmenu;
private OnClickListener mAllowPresenceSubscription = new OnClickListener() { private final OnClickListener mAllowPresenceSubscription = new OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
final Contact contact = conversation == null ? null : conversation.getContact(); final Contact contact = conversation == null ? null : conversation.getContact();
@ -400,8 +400,8 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
updateSnackBar(conversation); updateSnackBar(conversation);
} }
}; };
private AtomicBoolean mSendingPgpMessage = new AtomicBoolean(false); private final AtomicBoolean mSendingPgpMessage = new AtomicBoolean(false);
private OnEditorActionListener mEditorActionListener = (v, actionId, event) -> { private final OnEditorActionListener mEditorActionListener = (v, actionId, event) -> {
if (actionId == EditorInfo.IME_ACTION_SEND) { if (actionId == EditorInfo.IME_ACTION_SEND) {
InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null && imm.isFullscreenMode()) { if (imm != null && imm.isFullscreenMode()) {
@ -413,7 +413,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
return false; return false;
} }
}; };
private OnClickListener mScrollButtonListener = new OnClickListener() { private final OnClickListener mScrollButtonListener = new OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
@ -421,7 +421,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
setSelection(binding.messagesView.getCount() - 1, true); setSelection(binding.messagesView.getCount() - 1, true);
} }
}; };
private OnClickListener mSendButtonListener = new OnClickListener() { private final OnClickListener mSendButtonListener = new OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
@ -517,7 +517,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
private static Conversation getConversation(Activity activity, @IdRes int res) { private static Conversation getConversation(Activity activity, @IdRes int res) {
final Fragment fragment = activity.getFragmentManager().findFragmentById(res); final Fragment fragment = activity.getFragmentManager().findFragmentById(res);
if (fragment != null && fragment instanceof ConversationFragment) { if (fragment instanceof ConversationFragment) {
return ((ConversationFragment) fragment).getConversation(); return ((ConversationFragment) fragment).getConversation();
} else { } else {
return null; return null;
@ -527,11 +527,11 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
public static ConversationFragment get(Activity activity) { public static ConversationFragment get(Activity activity) {
FragmentManager fragmentManager = activity.getFragmentManager(); FragmentManager fragmentManager = activity.getFragmentManager();
Fragment fragment = fragmentManager.findFragmentById(R.id.main_fragment); Fragment fragment = fragmentManager.findFragmentById(R.id.main_fragment);
if (fragment != null && fragment instanceof ConversationFragment) { if (fragment instanceof ConversationFragment) {
return (ConversationFragment) fragment; return (ConversationFragment) fragment;
} else { } else {
fragment = fragmentManager.findFragmentById(R.id.secondary_fragment); fragment = fragmentManager.findFragmentById(R.id.secondary_fragment);
return fragment != null && fragment instanceof ConversationFragment ? (ConversationFragment) fragment : null; return fragment instanceof ConversationFragment ? (ConversationFragment) fragment : null;
} }
} }
@ -986,7 +986,8 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
menuCall.setVisible(false); menuCall.setVisible(false);
menuOngoingCall.setVisible(false); menuOngoingCall.setVisible(false);
} else { } else {
final Optional<OngoingRtpSession> ongoingRtpSession = activity.xmppConnectionService.getJingleConnectionManager().getOngoingRtpConnection(conversation.getContact()); final XmppConnectionService service = activity.xmppConnectionService;
final Optional<OngoingRtpSession> ongoingRtpSession = service == null ? Optional.absent() : service.getJingleConnectionManager().getOngoingRtpConnection(conversation.getContact());
if (ongoingRtpSession.isPresent()) { if (ongoingRtpSession.isPresent()) {
menuOngoingCall.setVisible(true); menuOngoingCall.setVisible(true);
menuCall.setVisible(false); menuCall.setVisible(false);
@ -998,7 +999,6 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke
} }
menuContactDetails.setVisible(!this.conversation.withSelf()); menuContactDetails.setVisible(!this.conversation.withSelf());
menuMucDetails.setVisible(false); menuMucDetails.setVisible(false);
final XmppConnectionService service = activity.xmppConnectionService;
menuInviteContact.setVisible(service != null && service.findConferenceServer(conversation.getAccount()) != null); menuInviteContact.setVisible(service != null && service.findConferenceServer(conversation.getAccount()) != null);
} }
if (conversation.isMuted()) { if (conversation.isMuted()) {

View File

@ -48,6 +48,7 @@ import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Toast;
import com.google.common.collect.Collections2; import com.google.common.collect.Collections2;
@ -371,7 +372,10 @@ public class ConversationsOverviewFragment extends XmppFragment {
private void selectAccountToStartEasyInvite() { private void selectAccountToStartEasyInvite() {
final List<Account> accounts = EasyOnboardingInvite.getSupportingAccounts(activity.xmppConnectionService); final List<Account> accounts = EasyOnboardingInvite.getSupportingAccounts(activity.xmppConnectionService);
if (accounts.size() == 1) { if (accounts.size() == 0) {
//This can technically happen if opening the menu item races with accounts reconnecting or something
Toast.makeText(getActivity(),R.string.no_active_accounts_support_this, Toast.LENGTH_LONG).show();
} else if (accounts.size() == 1) {
openEasyInviteScreen(accounts.get(0)); openEasyInviteScreen(accounts.get(0));
} else { } else {
final AtomicReference<Account> selectedAccount = new AtomicReference<>(accounts.get(0)); final AtomicReference<Account> selectedAccount = new AtomicReference<>(accounts.get(0));

View File

@ -39,6 +39,7 @@ import android.text.TextWatcher;
import android.view.ContextMenu; import android.view.ContextMenu;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.EditText; import android.widget.EditText;
@ -130,7 +131,8 @@ public class SearchActivity extends XmppActivity implements TextWatcher, OnSearc
} }
@Override @Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { public void onCreateContextMenu(final ContextMenu menu, final View v, ContextMenu.ContextMenuInfo menuInfo) {
v.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0));
AdapterView.AdapterContextMenuInfo acmi = (AdapterView.AdapterContextMenuInfo) menuInfo; AdapterView.AdapterContextMenuInfo acmi = (AdapterView.AdapterContextMenuInfo) menuInfo;
final Message message = this.messages.get(acmi.position); final Message message = this.messages.get(acmi.position);
this.selectedMessageReference = new WeakReference<>(message); this.selectedMessageReference = new WeakReference<>(message);

View File

@ -368,12 +368,13 @@ public abstract class XmppActivity extends ActionBarActivity {
public void selectPresence(final Conversation conversation, final PresenceSelector.OnPresenceSelected listener) { public void selectPresence(final Conversation conversation, final PresenceSelector.OnPresenceSelected listener) {
final Contact contact = conversation.getContact(); final Contact contact = conversation.getContact();
if (!contact.showInRoster()) { if (contact.showInRoster() || contact.isSelf()) {
showAddToRosterDialog(conversation.getContact());
} else {
final Presences presences = contact.getPresences(); final Presences presences = contact.getPresences();
if (presences.size() == 0) { if (presences.size() == 0) {
if (!contact.getOption(Contact.Options.TO) if (contact.isSelf()) {
conversation.setNextCounterpart(null);
listener.onPresenceSelected();
} else if (!contact.getOption(Contact.Options.TO)
&& !contact.getOption(Contact.Options.ASKING) && !contact.getOption(Contact.Options.ASKING)
&& contact.getAccount().getStatus() == Account.State.ONLINE) { && contact.getAccount().getStatus() == Account.State.ONLINE) {
showAskForPresenceDialog(contact); showAskForPresenceDialog(contact);
@ -391,6 +392,8 @@ public abstract class XmppActivity extends ActionBarActivity {
} else { } else {
PresenceSelector.showPresenceSelectionDialog(this, conversation, listener); PresenceSelector.showPresenceSelectionDialog(this, conversation, listener);
} }
} else {
showAddToRosterDialog(conversation.getContact());
} }
} }

View File

@ -29,7 +29,7 @@ public class SSLSocketHelper {
final Collection<String> supportedProtocols = new LinkedList<>( final Collection<String> supportedProtocols = new LinkedList<>(
Arrays.asList(sslSocket.getSupportedProtocols())); Arrays.asList(sslSocket.getSupportedProtocols()));
supportedProtocols.remove("SSLv3"); supportedProtocols.remove("SSLv3");
supportProtocols = supportedProtocols.toArray(new String[supportedProtocols.size()]); supportProtocols = supportedProtocols.toArray(new String[0]);
sslSocket.setEnabledProtocols(supportProtocols); sslSocket.setEnabledProtocols(supportProtocols);

View File

@ -64,6 +64,7 @@ import eu.siacs.conversations.crypto.sasl.Plain;
import eu.siacs.conversations.crypto.sasl.SaslMechanism; import eu.siacs.conversations.crypto.sasl.SaslMechanism;
import eu.siacs.conversations.crypto.sasl.ScramSha1; import eu.siacs.conversations.crypto.sasl.ScramSha1;
import eu.siacs.conversations.crypto.sasl.ScramSha256; import eu.siacs.conversations.crypto.sasl.ScramSha256;
import eu.siacs.conversations.crypto.sasl.ScramSha512;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.ServiceDiscoveryResult; import eu.siacs.conversations.entities.ServiceDiscoveryResult;
@ -106,32 +107,29 @@ public class XmppConnection implements Runnable {
private static final int PACKET_IQ = 0; private static final int PACKET_IQ = 0;
private static final int PACKET_MESSAGE = 1; private static final int PACKET_MESSAGE = 1;
private static final int PACKET_PRESENCE = 2; private static final int PACKET_PRESENCE = 2;
public final OnIqPacketReceived registrationResponseListener = new OnIqPacketReceived() { public final OnIqPacketReceived registrationResponseListener = (account, packet) -> {
@Override if (packet.getType() == IqPacket.TYPE.RESULT) {
public void onIqPacketReceived(Account account, IqPacket packet) { account.setOption(Account.OPTION_REGISTER, false);
if (packet.getType() == IqPacket.TYPE.RESULT) { Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": successfully registered new account on server");
account.setOption(Account.OPTION_REGISTER, false); throw new StateChangingError(Account.State.REGISTRATION_SUCCESSFUL);
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": successfully registered new account on server"); } else {
throw new StateChangingError(Account.State.REGISTRATION_SUCCESSFUL); final List<String> PASSWORD_TOO_WEAK_MSGS = Arrays.asList(
} else { "The password is too weak",
final List<String> PASSWORD_TOO_WEAK_MSGS = Arrays.asList( "Please use a longer password.");
"The password is too weak", Element error = packet.findChild("error");
"Please use a longer password."); Account.State state = Account.State.REGISTRATION_FAILED;
Element error = packet.findChild("error"); if (error != null) {
Account.State state = Account.State.REGISTRATION_FAILED; if (error.hasChild("conflict")) {
if (error != null) { state = Account.State.REGISTRATION_CONFLICT;
if (error.hasChild("conflict")) { } else if (error.hasChild("resource-constraint")
state = Account.State.REGISTRATION_CONFLICT; && "wait".equals(error.getAttribute("type"))) {
} else if (error.hasChild("resource-constraint") state = Account.State.REGISTRATION_PLEASE_WAIT;
&& "wait".equals(error.getAttribute("type"))) { } else if (error.hasChild("not-acceptable")
state = Account.State.REGISTRATION_PLEASE_WAIT; && PASSWORD_TOO_WEAK_MSGS.contains(error.findChildContent("text"))) {
} else if (error.hasChild("not-acceptable") state = Account.State.REGISTRATION_PASSWORD_TOO_WEAK;
&& PASSWORD_TOO_WEAK_MSGS.contains(error.findChildContent("text"))) {
state = Account.State.REGISTRATION_PASSWORD_TOO_WEAK;
}
} }
throw new StateChangingError(state);
} }
throw new StateChangingError(state);
} }
}; };
protected final Account account; protected final Account account;
@ -159,10 +157,10 @@ public class XmppConnection implements Runnable {
private long lastSessionStarted = 0; private long lastSessionStarted = 0;
private long lastDiscoStarted = 0; private long lastDiscoStarted = 0;
private boolean isMamPreferenceAlways = false; private boolean isMamPreferenceAlways = false;
private AtomicInteger mPendingServiceDiscoveries = new AtomicInteger(0); private final AtomicInteger mPendingServiceDiscoveries = new AtomicInteger(0);
private AtomicBoolean mWaitForDisco = new AtomicBoolean(true); private final AtomicBoolean mWaitForDisco = new AtomicBoolean(true);
private AtomicBoolean mWaitingForSmCatchup = new AtomicBoolean(false); private final AtomicBoolean mWaitingForSmCatchup = new AtomicBoolean(false);
private AtomicInteger mSmCatchupMessageCounter = new AtomicInteger(0); private final AtomicInteger mSmCatchupMessageCounter = new AtomicInteger(0);
private boolean mInteractive = false; private boolean mInteractive = false;
private int attempt = 0; private int attempt = 0;
private OnPresencePacketReceived presenceListener = null; private OnPresencePacketReceived presenceListener = null;
@ -744,7 +742,7 @@ public class XmppConnection implements Runnable {
} }
} }
private void processMessage(final Tag currentTag) throws XmlPullParserException, IOException { private void processMessage(final Tag currentTag) throws IOException {
final MessagePacket packet = (MessagePacket) processPacket(currentTag, PACKET_MESSAGE); final MessagePacket packet = (MessagePacket) processPacket(currentTag, PACKET_MESSAGE);
if (!packet.valid()) { if (!packet.valid()) {
Log.e(Config.LOGTAG, "encountered invalid message from='" + packet.getFrom() + "' to='" + packet.getTo() + "'"); Log.e(Config.LOGTAG, "encountered invalid message from='" + packet.getFrom() + "' to='" + packet.getTo() + "'");
@ -753,7 +751,7 @@ public class XmppConnection implements Runnable {
this.messageListener.onMessagePacketReceived(account, packet); this.messageListener.onMessagePacketReceived(account, packet);
} }
private void processPresence(final Tag currentTag) throws XmlPullParserException, IOException { private void processPresence(final Tag currentTag) throws IOException {
PresencePacket packet = (PresencePacket) processPacket(currentTag, PACKET_PRESENCE); PresencePacket packet = (PresencePacket) processPacket(currentTag, PACKET_PRESENCE);
if (!packet.valid()) { if (!packet.valid()) {
Log.e(Config.LOGTAG, "encountered invalid presence from='" + packet.getFrom() + "' to='" + packet.getTo() + "'"); Log.e(Config.LOGTAG, "encountered invalid presence from='" + packet.getFrom() + "' to='" + packet.getTo() + "'");
@ -807,7 +805,7 @@ public class XmppConnection implements Runnable {
return sslSocket; return sslSocket;
} }
private void processStreamFeatures(final Tag currentTag) throws XmlPullParserException, IOException { private void processStreamFeatures(final Tag currentTag) throws IOException {
this.streamFeatures = tagReader.readElement(currentTag); this.streamFeatures = tagReader.readElement(currentTag);
final boolean isSecure = features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS || account.isOnion(); final boolean isSecure = features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS || account.isOnion();
final boolean needsBinding = !isBound && !account.isOptionSet(Account.OPTION_REGISTER); final boolean needsBinding = !isBound && !account.isOptionSet(Account.OPTION_REGISTER);
@ -843,20 +841,21 @@ public class XmppConnection implements Runnable {
} }
private void authenticate() throws IOException { private void authenticate() throws IOException {
final List<String> mechanisms = extractMechanisms(streamFeatures final List<String> mechanisms = extractMechanisms(streamFeatures.findChild("mechanisms"));
.findChild("mechanisms"));
final Element auth = new Element("auth", Namespace.SASL); final Element auth = new Element("auth", Namespace.SASL);
if (mechanisms.contains("EXTERNAL") && account.getPrivateKeyAlias() != null) { if (mechanisms.contains(External.MECHANISM) && account.getPrivateKeyAlias() != null) {
saslMechanism = new External(tagWriter, account, mXmppConnectionService.getRNG()); saslMechanism = new External(tagWriter, account, mXmppConnectionService.getRNG());
} else if (mechanisms.contains("SCRAM-SHA-256")) { } else if (mechanisms.contains(ScramSha512.MECHANISM)) {
saslMechanism = new ScramSha512(tagWriter, account, mXmppConnectionService.getRNG());
} else if (mechanisms.contains(ScramSha256.MECHANISM)) {
saslMechanism = new ScramSha256(tagWriter, account, mXmppConnectionService.getRNG()); saslMechanism = new ScramSha256(tagWriter, account, mXmppConnectionService.getRNG());
} else if (mechanisms.contains("SCRAM-SHA-1")) { } else if (mechanisms.contains(ScramSha1.MECHANISM)) {
saslMechanism = new ScramSha1(tagWriter, account, mXmppConnectionService.getRNG()); saslMechanism = new ScramSha1(tagWriter, account, mXmppConnectionService.getRNG());
} else if (mechanisms.contains("PLAIN") && !account.getJid().getDomain().toEscapedString().equals("nimbuzz.com")) { } else if (mechanisms.contains(Plain.MECHANISM) && !account.getJid().getDomain().toEscapedString().equals("nimbuzz.com")) {
saslMechanism = new Plain(tagWriter, account); saslMechanism = new Plain(tagWriter, account);
} else if (mechanisms.contains("DIGEST-MD5")) { } else if (mechanisms.contains(DigestMd5.MECHANISM)) {
saslMechanism = new DigestMd5(tagWriter, account, mXmppConnectionService.getRNG()); saslMechanism = new DigestMd5(tagWriter, account, mXmppConnectionService.getRNG());
} else if (mechanisms.contains("ANONYMOUS")) { } else if (mechanisms.contains(Anonymous.MECHANISM)) {
saslMechanism = new Anonymous(tagWriter, account, mXmppConnectionService.getRNG()); saslMechanism = new Anonymous(tagWriter, account, mXmppConnectionService.getRNG());
} }
if (saslMechanism != null) { if (saslMechanism != null) {
@ -1238,27 +1237,27 @@ public class XmppConnection implements Runnable {
request.setTo(account.getDomain()); request.setTo(account.getDomain());
request.addChild("query", Namespace.DISCO_ITEMS).setAttribute("node", Namespace.COMMANDS); request.addChild("query", Namespace.DISCO_ITEMS).setAttribute("node", Namespace.COMMANDS);
sendIqPacket(request, (account, response) -> { sendIqPacket(request, (account, response) -> {
if (response.getType() == IqPacket.TYPE.RESULT) { if (response.getType() == IqPacket.TYPE.RESULT) {
final Element query = response.findChild("query",Namespace.DISCO_ITEMS); final Element query = response.findChild("query", Namespace.DISCO_ITEMS);
if (query == null) { if (query == null) {
return; return;
} }
final HashMap<String, Jid> commands = new HashMap<>(); final HashMap<String, Jid> commands = new HashMap<>();
for(final Element child : query.getChildren()) { for (final Element child : query.getChildren()) {
if ("item".equals(child.getName())) { if ("item".equals(child.getName())) {
final String node = child.getAttribute("node"); final String node = child.getAttribute("node");
final Jid jid = child.getAttributeAsJid("jid"); final Jid jid = child.getAttributeAsJid("jid");
if (node != null && jid != null) { if (node != null && jid != null) {
commands.put(node, jid); commands.put(node, jid);
} }
} }
} }
Log.d(Config.LOGTAG,commands.toString()); Log.d(Config.LOGTAG, commands.toString());
synchronized (this.commands) { synchronized (this.commands) {
this.commands.clear(); this.commands.clear();
this.commands.putAll(commands); this.commands.putAll(commands);
} }
} }
}); });
} }
@ -1297,7 +1296,7 @@ public class XmppConnection implements Runnable {
iq.query("http://jabber.org/protocol/disco#items"); iq.query("http://jabber.org/protocol/disco#items");
this.sendIqPacket(iq, (account, packet) -> { this.sendIqPacket(iq, (account, packet) -> {
if (packet.getType() == IqPacket.TYPE.RESULT) { if (packet.getType() == IqPacket.TYPE.RESULT) {
HashSet<Jid> items = new HashSet<Jid>(); final HashSet<Jid> items = new HashSet<>();
final List<Element> elements = packet.query().getChildren(); final List<Element> elements = packet.query().getChildren();
for (final Element element : elements) { for (final Element element : elements) {
if (element.getName().equals("item")) { if (element.getName().equals("item")) {
@ -1325,23 +1324,19 @@ public class XmppConnection implements Runnable {
private void sendEnableCarbons() { private void sendEnableCarbons() {
final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
iq.addChild("enable", "urn:xmpp:carbons:2"); iq.addChild("enable", "urn:xmpp:carbons:2");
this.sendIqPacket(iq, new OnIqPacketReceived() { this.sendIqPacket(iq, (account, packet) -> {
if (!packet.hasChild("error")) {
@Override Log.d(Config.LOGTAG, account.getJid().asBareJid()
public void onIqPacketReceived(final Account account, final IqPacket packet) { + ": successfully enabled carbons");
if (!packet.hasChild("error")) { features.carbonsEnabled = true;
Log.d(Config.LOGTAG, account.getJid().asBareJid() } else {
+ ": successfully enabled carbons"); Log.d(Config.LOGTAG, account.getJid().asBareJid()
features.carbonsEnabled = true; + ": error enableing carbons " + packet.toString());
} else {
Log.d(Config.LOGTAG, account.getJid().asBareJid()
+ ": error enableing carbons " + packet.toString());
}
} }
}); });
} }
private void processStreamError(final Tag currentTag) throws XmlPullParserException, IOException { private void processStreamError(final Tag currentTag) throws IOException {
final Element streamError = tagReader.readElement(currentTag); final Element streamError = tagReader.readElement(currentTag);
if (streamError == null) { if (streamError == null) {
return; return;
@ -1594,8 +1589,8 @@ public class XmppConnection implements Runnable {
} }
public List<String> getMucServersWithholdAccount() { public List<String> getMucServersWithholdAccount() {
List<String> servers = getMucServers(); final List<String> servers = getMucServers();
servers.remove(account.getDomain()); servers.remove(account.getDomain().toEscapedString());
return servers; return servers;
} }
@ -1766,7 +1761,7 @@ public class XmppConnection implements Runnable {
} }
} }
private class StateChangingError extends Error { private static class StateChangingError extends Error {
private final Account.State state; private final Account.State state;
public StateChangingError(Account.State state) { public StateChangingError(Account.State state) {
@ -1774,7 +1769,7 @@ public class XmppConnection implements Runnable {
} }
} }
private class StateChangingException extends IOException { private static class StateChangingException extends IOException {
private final Account.State state; private final Account.State state;
public StateChangingException(Account.State state) { public StateChangingException(Account.State state) {

View File

@ -143,14 +143,16 @@ public class SessionDescription {
final ArrayListMultimap<String, String> mediaAttributes = ArrayListMultimap.create(); final ArrayListMultimap<String, String> mediaAttributes = ArrayListMultimap.create();
final String ufrag = transport.getAttribute("ufrag"); final String ufrag = transport.getAttribute("ufrag");
final String pwd = transport.getAttribute("pwd"); final String pwd = transport.getAttribute("pwd");
if (!Strings.isNullOrEmpty(ufrag)) { if (Strings.isNullOrEmpty(ufrag)) {
mediaAttributes.put("ice-ufrag", ufrag); throw new IllegalArgumentException("Transport element is missing required ufrag attribute");
} }
checkNoWhitespace(ufrag, "ufrag value must not contain any whitespaces"); checkNoWhitespace(ufrag, "ufrag value must not contain any whitespaces");
if (!Strings.isNullOrEmpty(pwd)) { mediaAttributes.put("ice-ufrag", ufrag);
mediaAttributes.put("ice-pwd", pwd); if (Strings.isNullOrEmpty(pwd)) {
throw new IllegalArgumentException("Transport element is missing required pwd attribute");
} }
checkNoWhitespace(pwd, "pwd value must not contain any whitespaces"); checkNoWhitespace(pwd, "pwd value must not contain any whitespaces");
mediaAttributes.put("ice-pwd", pwd);
mediaAttributes.put("ice-options", HARDCODED_ICE_OPTIONS); mediaAttributes.put("ice-options", HARDCODED_ICE_OPTIONS);
final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint(); final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint();
if (fingerprint != null) { if (fingerprint != null) {

View File

@ -14,6 +14,7 @@ import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.UUID;
import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xml.Namespace; import eu.siacs.conversations.xml.Namespace;
@ -93,6 +94,7 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
if (pair.length == 2 && "candidate".equals(pair[0])) { if (pair.length == 2 && "candidate".equals(pair[0])) {
final String[] segments = pair[1].split(" "); final String[] segments = pair[1].split(" ");
if (segments.length >= 6) { if (segments.length >= 6) {
final String id = UUID.randomUUID().toString();
final String foundation = segments[0]; final String foundation = segments[0];
final String component = segments[1]; final String component = segments[1];
final String transport = segments[2].toLowerCase(Locale.ROOT); final String transport = segments[2].toLowerCase(Locale.ROOT);
@ -109,6 +111,7 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
candidate.setAttribute("generation", additional.get("generation")); candidate.setAttribute("generation", additional.get("generation"));
candidate.setAttribute("rel-addr", additional.get("raddr")); candidate.setAttribute("rel-addr", additional.get("raddr"));
candidate.setAttribute("rel-port", additional.get("rport")); candidate.setAttribute("rel-port", additional.get("rport"));
candidate.setAttribute("id", id);
candidate.setAttribute("ip", connectionAddress); candidate.setAttribute("ip", connectionAddress);
candidate.setAttribute("port", port); candidate.setAttribute("port", port);
candidate.setAttribute("priority", priority); candidate.setAttribute("priority", priority);

View File

@ -4,7 +4,9 @@
<string name="action_add">Nová konverzace</string> <string name="action_add">Nová konverzace</string>
<string name="action_accounts">Nastavení účtů</string> <string name="action_accounts">Nastavení účtů</string>
<string name="action_account">Nastavení účtu</string> <string name="action_account">Nastavení účtu</string>
<string name="action_end_conversation">Zavřít konverzaci</string>
<string name="action_contact_details">Detaily kontaktu</string> <string name="action_contact_details">Detaily kontaktu</string>
<string name="action_muc_details">Detaily skupinového chatu</string>
<string name="action_add_account">Přidat účet</string> <string name="action_add_account">Přidat účet</string>
<string name="action_edit_contact">Upravit jméno</string> <string name="action_edit_contact">Upravit jméno</string>
<string name="action_add_phone_book">Přidat do adresáře</string> <string name="action_add_phone_book">Přidat do adresáře</string>
@ -17,6 +19,7 @@
<string name="title_activity_settings">Nastavení</string> <string name="title_activity_settings">Nastavení</string>
<string name="title_activity_sharewith">Sdílet s konverzací</string> <string name="title_activity_sharewith">Sdílet s konverzací</string>
<string name="title_activity_start_conversation">Začít konverzaci</string> <string name="title_activity_start_conversation">Začít konverzaci</string>
<string name="title_activity_choose_contact">Vybrat kontakt</string>
<string name="title_activity_block_list">Seznam blokovaných</string> <string name="title_activity_block_list">Seznam blokovaných</string>
<string name="just_now">právě teď</string> <string name="just_now">právě teď</string>
<string name="minute_ago">před minutou</string> <string name="minute_ago">před minutou</string>
@ -30,14 +33,17 @@
<string name="moderator">Moderátor</string> <string name="moderator">Moderátor</string>
<string name="participant">Účastník</string> <string name="participant">Účastník</string>
<string name="visitor">Návštěvník</string> <string name="visitor">Návštěvník</string>
<string name="remove_contact_text">Přejete si odstranit %s ze seznamu kontaktů? Předešlé rozhovory nebudou odstraněny.</string>
<string name="block_contact_text">Chcete zablokovat příjem zpráv od %s?</string> <string name="block_contact_text">Chcete zablokovat příjem zpráv od %s?</string>
<string name="unblock_contact_text">Chcete odblokovat příjem zpráv od %s?</string> <string name="unblock_contact_text">Chcete odblokovat příjem zpráv od %s?</string>
<string name="block_domain_text">Zablokovat všechny kontakty z %s?</string> <string name="block_domain_text">Zablokovat všechny kontakty z %s?</string>
<string name="unblock_domain_text">Odblokovat všechny kontakty z %s?</string> <string name="unblock_domain_text">Odblokovat všechny kontakty z %s?</string>
<string name="contact_blocked">Kontakty zablokovány</string> <string name="contact_blocked">Kontakty zablokovány</string>
<string name="remove_bookmark_text">Přejete si odstranit %s ze záložek? Předešlé rozhovory pod záložkou nebudou odstraněny.</string>
<string name="register_account">Registrovat nový účet na serveru</string> <string name="register_account">Registrovat nový účet na serveru</string>
<string name="change_password_on_server">Změnit heslo na serveru</string> <string name="change_password_on_server">Změnit heslo na serveru</string>
<string name="share_with">Sdílet s...</string> <string name="share_with">Sdílet s...</string>
<string name="start_conversation">Začít konverzaci</string>
<string name="invite_contact">Pozvat kontakt</string> <string name="invite_contact">Pozvat kontakt</string>
<string name="contacts">Kontakty</string> <string name="contacts">Kontakty</string>
<string name="cancel">Zrušit</string> <string name="cancel">Zrušit</string>
@ -144,9 +150,14 @@
<string name="server_info_unavailable">nedostupný</string> <string name="server_info_unavailable">nedostupný</string>
<string name="missing_public_keys">Chybí oznámení o veřejném klíči</string> <string name="missing_public_keys">Chybí oznámení o veřejném klíči</string>
<string name="last_seen_now">právě spatřen</string> <string name="last_seen_now">právě spatřen</string>
<string name="last_seen_min">naposledy spatřen před minutou</string>
<string name="last_seen_mins">naposledy spatřen před %d minutami</string> <string name="last_seen_mins">naposledy spatřen před %d minutami</string>
<string name="last_seen_hour">naposledy spatřen před hodinou</string>
<string name="last_seen_hours">naposledy spatřen před %d hodinami</string> <string name="last_seen_hours">naposledy spatřen před %d hodinami</string>
<string name="last_seen_day">naposledy spatřen včera</string>
<string name="last_seen_days">naposledy spatřen před %d dny</string> <string name="last_seen_days">naposledy spatřen před %d dny</string>
<string name="install_openkeychain">Šifrovaná zpráva. Nainstalujte OpenKeychain pro její dešifrování.</string>
<string name="openpgp_messages_found">Nalezeny nové OpenPGP šifrované zprávy</string>
<string name="openpgp_key_id">OpenPGP ID klíče</string> <string name="openpgp_key_id">OpenPGP ID klíče</string>
<string name="omemo_fingerprint">OMEMO otisk</string> <string name="omemo_fingerprint">OMEMO otisk</string>
<string name="omemo_fingerprint_x509">v\\OMEMO otisk</string> <string name="omemo_fingerprint_x509">v\\OMEMO otisk</string>
@ -160,6 +171,7 @@
<string name="bookmarks">Záložky</string> <string name="bookmarks">Záložky</string>
<string name="search">Hledat</string> <string name="search">Hledat</string>
<string name="enter_contact">Vložit kontakt</string> <string name="enter_contact">Vložit kontakt</string>
<string name="delete_contact">Smazat kontakt</string>
<string name="view_contact_details">Zobrazit detaily kontaktu</string> <string name="view_contact_details">Zobrazit detaily kontaktu</string>
<string name="block_contact">Zablokovat kontakt</string> <string name="block_contact">Zablokovat kontakt</string>
<string name="unblock_contact">Odblokovat kontakt</string> <string name="unblock_contact">Odblokovat kontakt</string>

View File

@ -951,4 +951,5 @@
<string name="invite_to_app">Einladung zu Conversations</string> <string name="invite_to_app">Einladung zu Conversations</string>
<string name="unable_to_parse_invite">Einladung kann nicht gelesen werden</string> <string name="unable_to_parse_invite">Einladung kann nicht gelesen werden</string>
<string name="server_does_not_support_easy_onboarding_invites">Server unterstützt keine Generierung von Einladungen</string> <string name="server_does_not_support_easy_onboarding_invites">Server unterstützt keine Generierung von Einladungen</string>
<string name="no_active_accounts_support_this">Keine aktiven Konten unterstützen diese Funktion</string>
</resources> </resources>

View File

@ -951,4 +951,5 @@
<string name="invite_to_app">Convida a Conversations</string> <string name="invite_to_app">Convida a Conversations</string>
<string name="unable_to_parse_invite">Non se puido enviar o convite</string> <string name="unable_to_parse_invite">Non se puido enviar o convite</string>
<string name="server_does_not_support_easy_onboarding_invites">O servidor non soporta a creación de convites</string> <string name="server_does_not_support_easy_onboarding_invites">O servidor non soporta a creación de convites</string>
<string name="no_active_accounts_support_this">Ningunha conta activa soporta esta función</string>
</resources> </resources>

View File

@ -948,4 +948,7 @@
<string name="failed_deliveries">Recapiti falliti</string> <string name="failed_deliveries">Recapiti falliti</string>
<string name="more_options">Altre opzioni</string> <string name="more_options">Altre opzioni</string>
<string name="no_application_found">Nessuna applicazione trovata</string> <string name="no_application_found">Nessuna applicazione trovata</string>
<string name="invite_to_app">Invita su Conversations</string>
<string name="unable_to_parse_invite">Impossibile analizzare l\'invito</string>
<string name="server_does_not_support_easy_onboarding_invites">Il server non supporta la generazione di inviti</string>
</resources> </resources>

View File

@ -970,4 +970,8 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż
<string name="failed_deliveries">Nie dostarczone wiadomości</string> <string name="failed_deliveries">Nie dostarczone wiadomości</string>
<string name="more_options">Więcej ustawień</string> <string name="more_options">Więcej ustawień</string>
<string name="no_application_found">Nie znaleziono żadnej aplikacji</string> <string name="no_application_found">Nie znaleziono żadnej aplikacji</string>
<string name="invite_to_app">Zaproś do Conversations</string>
<string name="unable_to_parse_invite">Nie można przetworzyć zaproszenia</string>
<string name="server_does_not_support_easy_onboarding_invites">Serwer nie wspiera tworzenia zaproszeń</string>
<string name="no_active_accounts_support_this">Nie ma aktywnych kont wspierających tę funkcję</string>
</resources> </resources>

View File

@ -947,4 +947,9 @@
</plurals> </plurals>
<string name="failed_deliveries">Entregas não efetuadas</string> <string name="failed_deliveries">Entregas não efetuadas</string>
<string name="more_options">Mais opções</string> <string name="more_options">Mais opções</string>
<string name="no_application_found">Não foi encontrado nenhum aplicativo</string>
<string name="invite_to_app">Convidar para o Conversations</string>
<string name="unable_to_parse_invite">Não foi possível processar o convite</string>
<string name="server_does_not_support_easy_onboarding_invites">O servidor não suporta a criação de convites</string>
<string name="no_active_accounts_support_this">Nenhuma conta ativa suporta esse recurso</string>
</resources> </resources>

View File

@ -961,4 +961,5 @@
<string name="invite_to_app">Invitați la Conversations</string> <string name="invite_to_app">Invitați la Conversations</string>
<string name="unable_to_parse_invite">Nu s-a putut procesa invitația</string> <string name="unable_to_parse_invite">Nu s-a putut procesa invitația</string>
<string name="server_does_not_support_easy_onboarding_invites">Serverul nu suportă generarea de invitații</string> <string name="server_does_not_support_easy_onboarding_invites">Serverul nu suportă generarea de invitații</string>
<string name="no_active_accounts_support_this">Nici un cont activ nu suporta această caracteristică</string>
</resources> </resources>

View File

@ -974,4 +974,5 @@
<string name="invite_to_app">Conversations\'a davet et</string> <string name="invite_to_app">Conversations\'a davet et</string>
<string name="unable_to_parse_invite">Davet iletilemedi</string> <string name="unable_to_parse_invite">Davet iletilemedi</string>
<string name="server_does_not_support_easy_onboarding_invites">Sunucu, davet oluşturulmasını desteklemiyor</string> <string name="server_does_not_support_easy_onboarding_invites">Sunucu, davet oluşturulmasını desteklemiyor</string>
<string name="no_active_accounts_support_this">Bu özelliği destekleyen aktif bir hesap yok</string>
</resources> </resources>

View File

@ -953,4 +953,5 @@
<string name="invite_to_app">Invite to Conversations</string> <string name="invite_to_app">Invite to Conversations</string>
<string name="unable_to_parse_invite">Unable to parse invite</string> <string name="unable_to_parse_invite">Unable to parse invite</string>
<string name="server_does_not_support_easy_onboarding_invites">Server does not support generating invites</string> <string name="server_does_not_support_easy_onboarding_invites">Server does not support generating invites</string>
<string name="no_active_accounts_support_this">No active accounts support this feature</string>
</resources> </resources>