Add SCRAM-SHA-2 support
This commit is contained in:
		
							parent
							
								
									9bdd2bf1ae
								
							
						
					
					
						commit
						bfc2cffc2f
					
				| @ -0,0 +1,228 @@ | |||||||
|  | package eu.siacs.conversations.crypto.sasl; | ||||||
|  | 
 | ||||||
|  | import android.annotation.TargetApi; | ||||||
|  | import android.os.Build; | ||||||
|  | import android.util.Base64; | ||||||
|  | import android.util.LruCache; | ||||||
|  | 
 | ||||||
|  | import org.bouncycastle.crypto.Digest; | ||||||
|  | import org.bouncycastle.crypto.macs.HMac; | ||||||
|  | import org.bouncycastle.crypto.params.KeyParameter; | ||||||
|  | 
 | ||||||
|  | import java.math.BigInteger; | ||||||
|  | import java.nio.charset.Charset; | ||||||
|  | import java.security.InvalidKeyException; | ||||||
|  | import java.security.SecureRandom; | ||||||
|  | 
 | ||||||
|  | import eu.siacs.conversations.entities.Account; | ||||||
|  | import eu.siacs.conversations.utils.CryptoHelper; | ||||||
|  | import eu.siacs.conversations.xml.TagWriter; | ||||||
|  | 
 | ||||||
|  | @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1) | ||||||
|  | abstract class ScramMechanism extends SaslMechanism { | ||||||
|  | 	// 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 String clientFirstMessageBare; | ||||||
|  | 	private final String clientNonce; | ||||||
|  | 	private byte[] serverSignature = null; | ||||||
|  | 	static HMac HMAC; | ||||||
|  | 	static Digest DIGEST; | ||||||
|  | 	private static final byte[] CLIENT_KEY_BYTES = "Client Key".getBytes(); | ||||||
|  | 	private static final byte[] SERVER_KEY_BYTES = "Server Key".getBytes(); | ||||||
|  | 
 | ||||||
|  | 	private static class KeyPair { | ||||||
|  | 		final byte[] clientKey; | ||||||
|  | 		final byte[] serverKey; | ||||||
|  | 
 | ||||||
|  | 		KeyPair(final byte[] clientKey, final byte[] serverKey) { | ||||||
|  | 			this.clientKey = clientKey; | ||||||
|  | 			this.serverKey = serverKey; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	static { | ||||||
|  | 		CACHE = new LruCache<String, KeyPair>(10) { | ||||||
|  | 			protected KeyPair create(final String k) { | ||||||
|  | 				// Map keys are "bytesToHex(JID),bytesToHex(password),bytesToHex(salt),iterations". | ||||||
|  | 				// 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(",", 4); | ||||||
|  | 				try { | ||||||
|  | 					final byte[] saltedPassword, serverKey, clientKey; | ||||||
|  | 					saltedPassword = hi(CryptoHelper.hexToString(kparts[1]).getBytes(), | ||||||
|  | 							Base64.decode(CryptoHelper.hexToString(kparts[2]), Base64.DEFAULT), Integer.valueOf(kparts[3])); | ||||||
|  | 					serverKey = hmac(saltedPassword, SERVER_KEY_BYTES); | ||||||
|  | 					clientKey = hmac(saltedPassword, CLIENT_KEY_BYTES); | ||||||
|  | 
 | ||||||
|  | 					return new KeyPair(clientKey, serverKey); | ||||||
|  | 				} catch (final InvalidKeyException | NumberFormatException e) { | ||||||
|  | 					return null; | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		}; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private static final LruCache<String, KeyPair> CACHE; | ||||||
|  | 
 | ||||||
|  | 	protected State state = State.INITIAL; | ||||||
|  | 
 | ||||||
|  | 	ScramMechanism(final TagWriter tagWriter, final Account account, final SecureRandom rng) { | ||||||
|  | 		super(tagWriter, account, rng); | ||||||
|  | 
 | ||||||
|  | 		// This nonce should be different for each authentication attempt. | ||||||
|  | 		clientNonce = new BigInteger(100, this.rng).toString(32); | ||||||
|  | 		clientFirstMessageBare = ""; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	public String getClientFirstMessage() { | ||||||
|  | 		if (clientFirstMessageBare.isEmpty() && state == State.INITIAL) { | ||||||
|  | 			clientFirstMessageBare = "n=" + CryptoHelper.saslEscape(CryptoHelper.saslPrep(account.getUsername())) + | ||||||
|  | 				",r=" + this.clientNonce; | ||||||
|  | 			state = State.AUTH_TEXT_SENT; | ||||||
|  | 		} | ||||||
|  | 		return Base64.encodeToString( | ||||||
|  | 				(GS2_HEADER + clientFirstMessageBare).getBytes(Charset.defaultCharset()), | ||||||
|  | 				Base64.NO_WRAP); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	public String getResponse(final String challenge) throws AuthenticationException { | ||||||
|  | 		switch (state) { | ||||||
|  | 			case AUTH_TEXT_SENT: | ||||||
|  | 				if (challenge == null) { | ||||||
|  | 					throw new AuthenticationException("challenge can not be null"); | ||||||
|  | 				} | ||||||
|  | 				byte[] serverFirstMessage = Base64.decode(challenge, Base64.DEFAULT); | ||||||
|  | 				final Tokenizer tokenizer = new Tokenizer(serverFirstMessage); | ||||||
|  | 				String nonce = ""; | ||||||
|  | 				int iterationCount = -1; | ||||||
|  | 				String salt = ""; | ||||||
|  | 				for (final String token : tokenizer) { | ||||||
|  | 					if (token.charAt(1) == '=') { | ||||||
|  | 						switch (token.charAt(0)) { | ||||||
|  | 							case 'i': | ||||||
|  | 								try { | ||||||
|  | 									iterationCount = Integer.parseInt(token.substring(2)); | ||||||
|  | 								} catch (final NumberFormatException e) { | ||||||
|  | 									throw new AuthenticationException(e); | ||||||
|  | 								} | ||||||
|  | 								break; | ||||||
|  | 							case 's': | ||||||
|  | 								salt = token.substring(2); | ||||||
|  | 								break; | ||||||
|  | 							case 'r': | ||||||
|  | 								nonce = token.substring(2); | ||||||
|  | 								break; | ||||||
|  | 							case 'm': | ||||||
|  | 								/* | ||||||
|  | 								 * RFC 5802: | ||||||
|  | 								 * m: This attribute is reserved for future extensibility.  In this | ||||||
|  | 								 * version of SCRAM, its presence in a client or a server message | ||||||
|  | 								 * MUST cause authentication failure when the attribute is parsed by | ||||||
|  | 								 * the other end. | ||||||
|  | 								 */ | ||||||
|  | 								throw new AuthenticationException("Server sent reserved token: `m'"); | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				if (iterationCount < 0) { | ||||||
|  | 					throw new AuthenticationException("Server did not send iteration count"); | ||||||
|  | 				} | ||||||
|  | 				if (nonce.isEmpty() || !nonce.startsWith(clientNonce)) { | ||||||
|  | 					throw new AuthenticationException("Server nonce does not contain client nonce: " + nonce); | ||||||
|  | 				} | ||||||
|  | 				if (salt.isEmpty()) { | ||||||
|  | 					throw new AuthenticationException("Server sent empty salt"); | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				final String clientFinalMessageWithoutProof = "c=" + Base64.encodeToString( | ||||||
|  | 						GS2_HEADER.getBytes(), Base64.NO_WRAP) + ",r=" + nonce; | ||||||
|  | 				final byte[] authMessage = (clientFirstMessageBare + ',' + new String(serverFirstMessage) + ',' | ||||||
|  | 						+ clientFinalMessageWithoutProof).getBytes(); | ||||||
|  | 
 | ||||||
|  | 				// Map keys are "bytesToHex(JID),bytesToHex(password),bytesToHex(salt),iterations". | ||||||
|  | 				final KeyPair keys = CACHE.get( | ||||||
|  | 						CryptoHelper.bytesToHex(account.getJid().toBareJid().toString().getBytes()) + "," | ||||||
|  | 						+ CryptoHelper.bytesToHex(account.getPassword().getBytes()) + "," | ||||||
|  | 						+ CryptoHelper.bytesToHex(salt.getBytes()) + "," | ||||||
|  | 						+ String.valueOf(iterationCount) | ||||||
|  | 						); | ||||||
|  | 				if (keys == null) { | ||||||
|  | 					throw new AuthenticationException("Invalid keys generated"); | ||||||
|  | 				} | ||||||
|  | 				final byte[] clientSignature; | ||||||
|  | 				try { | ||||||
|  | 					serverSignature = hmac(keys.serverKey, authMessage); | ||||||
|  | 					final byte[] storedKey = digest(keys.clientKey); | ||||||
|  | 
 | ||||||
|  | 					clientSignature = hmac(storedKey, authMessage); | ||||||
|  | 
 | ||||||
|  | 				} catch (final InvalidKeyException e) { | ||||||
|  | 					throw new AuthenticationException(e); | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				final byte[] clientProof = new byte[keys.clientKey.length]; | ||||||
|  | 
 | ||||||
|  | 				for (int i = 0; i < clientProof.length; i++) { | ||||||
|  | 					clientProof[i] = (byte) (keys.clientKey[i] ^ clientSignature[i]); | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 				final String clientFinalMessage = clientFinalMessageWithoutProof + ",p=" + | ||||||
|  | 					Base64.encodeToString(clientProof, Base64.NO_WRAP); | ||||||
|  | 				state = State.RESPONSE_SENT; | ||||||
|  | 				return Base64.encodeToString(clientFinalMessage.getBytes(), Base64.NO_WRAP); | ||||||
|  | 			case RESPONSE_SENT: | ||||||
|  | 				try { | ||||||
|  | 					final String clientCalculatedServerFinalMessage = "v=" + | ||||||
|  | 						Base64.encodeToString(serverSignature, Base64.NO_WRAP); | ||||||
|  | 					if (!clientCalculatedServerFinalMessage.equals(new String(Base64.decode(challenge, Base64.DEFAULT)))) { | ||||||
|  | 						throw new Exception(); | ||||||
|  | 					} | ||||||
|  | 					state = State.VALID_SERVER_RESPONSE; | ||||||
|  | 					return ""; | ||||||
|  | 				} catch(Exception e) { | ||||||
|  | 					throw new AuthenticationException("Server final message does not match calculated final message"); | ||||||
|  | 				} | ||||||
|  | 			default: | ||||||
|  | 				throw new InvalidStateException(state); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	private static synchronized byte[] hmac(final byte[] key, final byte[] input) | ||||||
|  | 		throws InvalidKeyException { | ||||||
|  | 		HMAC.init(new KeyParameter(key)); | ||||||
|  | 		HMAC.update(input, 0, input.length); | ||||||
|  | 		final byte[] out = new byte[HMAC.getMacSize()]; | ||||||
|  | 		HMAC.doFinal(out, 0); | ||||||
|  | 		return out; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	public static synchronized byte[] digest(byte[] bytes) { | ||||||
|  | 		DIGEST.reset(); | ||||||
|  | 		DIGEST.update(bytes, 0, bytes.length); | ||||||
|  | 		final byte[] out = new byte[DIGEST.getDigestSize()]; | ||||||
|  | 		DIGEST.doFinal(out, 0); | ||||||
|  | 		return out; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/* | ||||||
|  | 	 * Hi() is, essentially, PBKDF2 [RFC2898] with HMAC() as the | ||||||
|  | 	 * pseudorandom function (PRF) and with dkLen == output length of | ||||||
|  | 	 * HMAC() == output length of H(). | ||||||
|  | 	 */ | ||||||
|  | 	private static synchronized byte[] hi(final byte[] key, final byte[] salt, final int iterations) | ||||||
|  | 		throws InvalidKeyException { | ||||||
|  | 		byte[] u = hmac(key, CryptoHelper.concatenateByteArrays(salt, CryptoHelper.ONE)); | ||||||
|  | 		byte[] out = u.clone(); | ||||||
|  | 		for (int i = 1; i < iterations; i++) { | ||||||
|  | 			u = hmac(key, u); | ||||||
|  | 			for (int j = 0; j < u.length; j++) { | ||||||
|  | 				out[j] ^= u[j]; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		return out; | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @ -1,77 +1,21 @@ | |||||||
| package eu.siacs.conversations.crypto.sasl; | package eu.siacs.conversations.crypto.sasl; | ||||||
| 
 | 
 | ||||||
| import android.util.Base64; |  | ||||||
| import android.util.LruCache; |  | ||||||
| 
 |  | ||||||
| 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; | ||||||
| import org.bouncycastle.crypto.params.KeyParameter; |  | ||||||
| 
 | 
 | ||||||
| import java.math.BigInteger; |  | ||||||
| import java.nio.charset.Charset; |  | ||||||
| import java.security.InvalidKeyException; |  | ||||||
| import java.security.SecureRandom; | import java.security.SecureRandom; | ||||||
| 
 | 
 | ||||||
| import eu.siacs.conversations.entities.Account; | import eu.siacs.conversations.entities.Account; | ||||||
| import eu.siacs.conversations.utils.CryptoHelper; |  | ||||||
| import eu.siacs.conversations.xml.TagWriter; | import eu.siacs.conversations.xml.TagWriter; | ||||||
| 
 | 
 | ||||||
| public class ScramSha1 extends SaslMechanism { | public class ScramSha1 extends ScramMechanism { | ||||||
| 	// TODO: When channel binding (SCRAM-SHA1-PLUS) is supported in future, generalize this to indicate support and/or usage. |  | ||||||
| 	final private static String GS2_HEADER = "n,,"; |  | ||||||
| 	private String clientFirstMessageBare; |  | ||||||
| 	final private String clientNonce; |  | ||||||
| 	private byte[] serverSignature = null; |  | ||||||
| 	private static HMac HMAC; |  | ||||||
| 	private static Digest DIGEST; |  | ||||||
| 	private static final byte[] CLIENT_KEY_BYTES = "Client Key".getBytes(); |  | ||||||
| 	private static final byte[] SERVER_KEY_BYTES = "Server Key".getBytes(); |  | ||||||
| 
 |  | ||||||
| 	public static class KeyPair { |  | ||||||
| 		final public byte[] clientKey; |  | ||||||
| 		final public byte[] serverKey; |  | ||||||
| 
 |  | ||||||
| 		public KeyPair(final byte[] clientKey, final byte[] serverKey) { |  | ||||||
| 			this.clientKey = clientKey; |  | ||||||
| 			this.serverKey = serverKey; |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	private static final LruCache<String, KeyPair> CACHE; |  | ||||||
| 
 |  | ||||||
| 	static { | 	static { | ||||||
| 		DIGEST = new SHA1Digest(); | 		DIGEST = new SHA1Digest(); | ||||||
| 		HMAC = new HMac(new SHA1Digest()); | 		HMAC = new HMac(new SHA1Digest()); | ||||||
| 		CACHE = new LruCache<String, KeyPair>(10) { |  | ||||||
| 			protected KeyPair create(final String k) { |  | ||||||
| 				// Map keys are "bytesToHex(JID),bytesToHex(password),bytesToHex(salt),iterations". |  | ||||||
| 				// 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(",", 4); |  | ||||||
| 				try { |  | ||||||
| 					final byte[] saltedPassword, serverKey, clientKey; |  | ||||||
| 					saltedPassword = hi(CryptoHelper.hexToString(kparts[1]).getBytes(), |  | ||||||
| 							Base64.decode(CryptoHelper.hexToString(kparts[2]), Base64.DEFAULT), Integer.valueOf(kparts[3])); |  | ||||||
| 					serverKey = hmac(saltedPassword, SERVER_KEY_BYTES); |  | ||||||
| 					clientKey = hmac(saltedPassword, CLIENT_KEY_BYTES); |  | ||||||
| 
 |  | ||||||
| 					return new KeyPair(clientKey, serverKey); |  | ||||||
| 				} catch (final InvalidKeyException | NumberFormatException e) { |  | ||||||
| 					return null; |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 		}; |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	private State state = State.INITIAL; |  | ||||||
| 
 |  | ||||||
| 	public ScramSha1(final TagWriter tagWriter, final Account account, final SecureRandom rng) { | 	public ScramSha1(final TagWriter tagWriter, final Account account, final SecureRandom rng) { | ||||||
| 		super(tagWriter, account, rng); | 		super(tagWriter, account, rng); | ||||||
| 
 |  | ||||||
| 		// This nonce should be different for each authentication attempt. |  | ||||||
| 		clientNonce = new BigInteger(100, this.rng).toString(32); |  | ||||||
| 		clientFirstMessageBare = ""; |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	@Override | 	@Override | ||||||
| @ -83,156 +27,4 @@ public class ScramSha1 extends SaslMechanism { | |||||||
| 	public String getMechanism() { | 	public String getMechanism() { | ||||||
| 		return "SCRAM-SHA-1"; | 		return "SCRAM-SHA-1"; | ||||||
| 	} | 	} | ||||||
| 
 |  | ||||||
| 	@Override |  | ||||||
| 	public String getClientFirstMessage() { |  | ||||||
| 		if (clientFirstMessageBare.isEmpty() && state == State.INITIAL) { |  | ||||||
| 			clientFirstMessageBare = "n=" + CryptoHelper.saslEscape(CryptoHelper.saslPrep(account.getUsername())) + |  | ||||||
| 				",r=" + this.clientNonce; |  | ||||||
| 			state = State.AUTH_TEXT_SENT; |  | ||||||
| 		} |  | ||||||
| 		return Base64.encodeToString( |  | ||||||
| 				(GS2_HEADER + clientFirstMessageBare).getBytes(Charset.defaultCharset()), |  | ||||||
| 				Base64.NO_WRAP); |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	@Override |  | ||||||
| 	public String getResponse(final String challenge) throws AuthenticationException { |  | ||||||
| 		switch (state) { |  | ||||||
| 			case AUTH_TEXT_SENT: |  | ||||||
| 				if (challenge == null) { |  | ||||||
| 					throw new AuthenticationException("challenge can not be null"); |  | ||||||
| 				} |  | ||||||
| 				byte[] serverFirstMessage = Base64.decode(challenge, Base64.DEFAULT); |  | ||||||
| 				final Tokenizer tokenizer = new Tokenizer(serverFirstMessage); |  | ||||||
| 				String nonce = ""; |  | ||||||
| 				int iterationCount = -1; |  | ||||||
| 				String salt = ""; |  | ||||||
| 				for (final String token : tokenizer) { |  | ||||||
| 					if (token.charAt(1) == '=') { |  | ||||||
| 						switch (token.charAt(0)) { |  | ||||||
| 							case 'i': |  | ||||||
| 								try { |  | ||||||
| 									iterationCount = Integer.parseInt(token.substring(2)); |  | ||||||
| 								} catch (final NumberFormatException e) { |  | ||||||
| 									throw new AuthenticationException(e); |  | ||||||
| 								} |  | ||||||
| 								break; |  | ||||||
| 							case 's': |  | ||||||
| 								salt = token.substring(2); |  | ||||||
| 								break; |  | ||||||
| 							case 'r': |  | ||||||
| 								nonce = token.substring(2); |  | ||||||
| 								break; |  | ||||||
| 							case 'm': |  | ||||||
| 								/* |  | ||||||
| 								 * RFC 5802: |  | ||||||
| 								 * m: This attribute is reserved for future extensibility.  In this |  | ||||||
| 								 * version of SCRAM, its presence in a client or a server message |  | ||||||
| 								 * MUST cause authentication failure when the attribute is parsed by |  | ||||||
| 								 * the other end. |  | ||||||
| 								 */ |  | ||||||
| 								throw new AuthenticationException("Server sent reserved token: `m'"); |  | ||||||
| 						} |  | ||||||
| 					} |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				if (iterationCount < 0) { |  | ||||||
| 					throw new AuthenticationException("Server did not send iteration count"); |  | ||||||
| 				} |  | ||||||
| 				if (nonce.isEmpty() || !nonce.startsWith(clientNonce)) { |  | ||||||
| 					throw new AuthenticationException("Server nonce does not contain client nonce: " + nonce); |  | ||||||
| 				} |  | ||||||
| 				if (salt.isEmpty()) { |  | ||||||
| 					throw new AuthenticationException("Server sent empty salt"); |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				final String clientFinalMessageWithoutProof = "c=" + Base64.encodeToString( |  | ||||||
| 						GS2_HEADER.getBytes(), Base64.NO_WRAP) + ",r=" + nonce; |  | ||||||
| 				final byte[] authMessage = (clientFirstMessageBare + ',' + new String(serverFirstMessage) + ',' |  | ||||||
| 						+ clientFinalMessageWithoutProof).getBytes(); |  | ||||||
| 
 |  | ||||||
| 				// Map keys are "bytesToHex(JID),bytesToHex(password),bytesToHex(salt),iterations". |  | ||||||
| 				final KeyPair keys = CACHE.get( |  | ||||||
| 						CryptoHelper.bytesToHex(account.getJid().toBareJid().toString().getBytes()) + "," |  | ||||||
| 						+ CryptoHelper.bytesToHex(account.getPassword().getBytes()) + "," |  | ||||||
| 						+ CryptoHelper.bytesToHex(salt.getBytes()) + "," |  | ||||||
| 						+ String.valueOf(iterationCount) |  | ||||||
| 						); |  | ||||||
| 				if (keys == null) { |  | ||||||
| 					throw new AuthenticationException("Invalid keys generated"); |  | ||||||
| 				} |  | ||||||
| 				final byte[] clientSignature; |  | ||||||
| 				try { |  | ||||||
| 					serverSignature = hmac(keys.serverKey, authMessage); |  | ||||||
| 					final byte[] storedKey = digest(keys.clientKey); |  | ||||||
| 
 |  | ||||||
| 					clientSignature = hmac(storedKey, authMessage); |  | ||||||
| 
 |  | ||||||
| 				} catch (final InvalidKeyException e) { |  | ||||||
| 					throw new AuthenticationException(e); |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 				final byte[] clientProof = new byte[keys.clientKey.length]; |  | ||||||
| 
 |  | ||||||
| 				for (int i = 0; i < clientProof.length; i++) { |  | ||||||
| 					clientProof[i] = (byte) (keys.clientKey[i] ^ clientSignature[i]); |  | ||||||
| 				} |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 				final String clientFinalMessage = clientFinalMessageWithoutProof + ",p=" + |  | ||||||
| 					Base64.encodeToString(clientProof, Base64.NO_WRAP); |  | ||||||
| 				state = State.RESPONSE_SENT; |  | ||||||
| 				return Base64.encodeToString(clientFinalMessage.getBytes(), Base64.NO_WRAP); |  | ||||||
| 			case RESPONSE_SENT: |  | ||||||
| 				try { |  | ||||||
| 					final String clientCalculatedServerFinalMessage = "v=" + |  | ||||||
| 							Base64.encodeToString(serverSignature, Base64.NO_WRAP); |  | ||||||
| 					if (!clientCalculatedServerFinalMessage.equals(new String(Base64.decode(challenge, Base64.DEFAULT)))) { |  | ||||||
| 						throw new Exception(); |  | ||||||
| 					}; |  | ||||||
| 					state = State.VALID_SERVER_RESPONSE; |  | ||||||
| 					return ""; |  | ||||||
| 				} catch(Exception e) { |  | ||||||
| 					throw new AuthenticationException("Server final message does not match calculated final message"); |  | ||||||
| 				} |  | ||||||
| 			default: |  | ||||||
| 				throw new InvalidStateException(state); |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	public static synchronized byte[] hmac(final byte[] key, final byte[] input) |  | ||||||
| 		throws InvalidKeyException { |  | ||||||
| 		HMAC.init(new KeyParameter(key)); |  | ||||||
| 		HMAC.update(input, 0, input.length); |  | ||||||
| 		final byte[] out = new byte[HMAC.getMacSize()]; |  | ||||||
| 		HMAC.doFinal(out, 0); |  | ||||||
| 		return out; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	public static synchronized byte[] digest(byte[] bytes) { |  | ||||||
| 		DIGEST.reset(); |  | ||||||
| 		DIGEST.update(bytes, 0, bytes.length); |  | ||||||
| 		final byte[] out = new byte[DIGEST.getDigestSize()]; |  | ||||||
| 		DIGEST.doFinal(out, 0); |  | ||||||
| 		return out; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	/* |  | ||||||
| 	 * Hi() is, essentially, PBKDF2 [RFC2898] with HMAC() as the |  | ||||||
| 	 * pseudorandom function (PRF) and with dkLen == output length of |  | ||||||
| 	 * HMAC() == output length of H(). |  | ||||||
| 	 */ |  | ||||||
| 	private static synchronized byte[] hi(final byte[] key, final byte[] salt, final int iterations) |  | ||||||
| 		throws InvalidKeyException { |  | ||||||
| 		byte[] u = hmac(key, CryptoHelper.concatenateByteArrays(salt, CryptoHelper.ONE)); |  | ||||||
| 		byte[] out = u.clone(); |  | ||||||
| 		for (int i = 1; i < iterations; i++) { |  | ||||||
| 			u = hmac(key, u); |  | ||||||
| 			for (int j = 0; j < u.length; j++) { |  | ||||||
| 				out[j] ^= u[j]; |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 		return out; |  | ||||||
| 	} |  | ||||||
| } | } | ||||||
|  | |||||||
| @ -0,0 +1,30 @@ | |||||||
|  | package eu.siacs.conversations.crypto.sasl; | ||||||
|  | 
 | ||||||
|  | import org.bouncycastle.crypto.digests.SHA256Digest; | ||||||
|  | import org.bouncycastle.crypto.macs.HMac; | ||||||
|  | 
 | ||||||
|  | import java.security.SecureRandom; | ||||||
|  | 
 | ||||||
|  | import eu.siacs.conversations.entities.Account; | ||||||
|  | import eu.siacs.conversations.xml.TagWriter; | ||||||
|  | 
 | ||||||
|  | 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) { | ||||||
|  | 		super(tagWriter, account, rng); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	public int getPriority() { | ||||||
|  | 		return 25; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	@Override | ||||||
|  | 	public String getMechanism() { | ||||||
|  | 		return "SCRAM-SHA-256"; | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @ -13,8 +13,6 @@ import android.util.Log; | |||||||
| import android.util.Pair; | import android.util.Pair; | ||||||
| import android.util.SparseArray; | import android.util.SparseArray; | ||||||
| 
 | 
 | ||||||
| import org.json.JSONException; |  | ||||||
| import org.json.JSONObject; |  | ||||||
| import org.xmlpull.v1.XmlPullParserException; | import org.xmlpull.v1.XmlPullParserException; | ||||||
| 
 | 
 | ||||||
| import java.io.ByteArrayInputStream; | import java.io.ByteArrayInputStream; | ||||||
| @ -26,8 +24,8 @@ import java.net.IDN; | |||||||
| import java.net.InetAddress; | import java.net.InetAddress; | ||||||
| import java.net.InetSocketAddress; | import java.net.InetSocketAddress; | ||||||
| import java.net.Socket; | import java.net.Socket; | ||||||
| import java.net.UnknownHostException; |  | ||||||
| import java.net.URL; | import java.net.URL; | ||||||
|  | import java.net.UnknownHostException; | ||||||
| import java.security.KeyManagementException; | import java.security.KeyManagementException; | ||||||
| import java.security.NoSuchAlgorithmException; | import java.security.NoSuchAlgorithmException; | ||||||
| import java.security.Principal; | import java.security.Principal; | ||||||
| @ -61,6 +59,7 @@ import eu.siacs.conversations.crypto.sasl.External; | |||||||
| import eu.siacs.conversations.crypto.sasl.Plain; | 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.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; | ||||||
| @ -855,6 +854,8 @@ public class XmppConnection implements Runnable { | |||||||
| 		auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl"); | 		auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl"); | ||||||
| 		if (mechanisms.contains("EXTERNAL") && account.getPrivateKeyAlias() != null) { | 		if (mechanisms.contains("EXTERNAL") && account.getPrivateKeyAlias() != null) { | ||||||
| 			saslMechanism = new External(tagWriter, account, mXmppConnectionService.getRNG()); | 			saslMechanism = new External(tagWriter, account, mXmppConnectionService.getRNG()); | ||||||
|  | 		} else if (mechanisms.contains("SCRAM-SHA-256")) { | ||||||
|  | 			saslMechanism = new ScramSha256(tagWriter, account, mXmppConnectionService.getRNG()); | ||||||
| 		} else if (mechanisms.contains("SCRAM-SHA-1")) { | 		} else if (mechanisms.contains("SCRAM-SHA-1")) { | ||||||
| 			saslMechanism = new ScramSha1(tagWriter, account, mXmppConnectionService.getRNG()); | 			saslMechanism = new ScramSha1(tagWriter, account, mXmppConnectionService.getRNG()); | ||||||
| 		} else if (mechanisms.contains("PLAIN")) { | 		} else if (mechanisms.contains("PLAIN")) { | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user
	 Sam Whited
						Sam Whited