Switch payload encryption to AES-GCM

Andreas Straub created

This also ensures that the IV is generated with proper randomness.

Change summary

src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java        | 28 
src/main/java/eu/siacs/conversations/crypto/axolotl/CryptoFailedException.java |  7 
src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java    | 28 
3 files changed, 46 insertions(+), 17 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java 🔗

@@ -4,6 +4,7 @@ import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.util.Log;
 
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
 import org.whispersystems.libaxolotl.AxolotlAddress;
 import org.whispersystems.libaxolotl.DuplicateMessageException;
 import org.whispersystems.libaxolotl.IdentityKey;
@@ -30,6 +31,7 @@ import org.whispersystems.libaxolotl.state.SessionRecord;
 import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
 import org.whispersystems.libaxolotl.util.KeyHelper;
 
+import java.security.Security;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -678,6 +680,9 @@ public class AxolotlService {
 	}
 
 	public AxolotlService(Account account, XmppConnectionService connectionService) {
+		if (Security.getProvider("BC") == null) {
+			Security.addProvider(new BouncyCastleProvider());
+		}
 		this.mXmppConnectionService = connectionService;
 		this.account = account;
 		this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService);
@@ -1050,11 +1055,17 @@ public class AxolotlService {
 		final String content;
 		if (message.hasFileOnRemoteHost()) {
 				content = message.getFileParams().url.toString();
-			} else {
-				content = message.getBody();
-			}
-		final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(message.getContact().getJid().toBareJid(),
-				getOwnDeviceId(), content);
+		} else {
+			content = message.getBody();
+		}
+		final XmppAxolotlMessage axolotlMessage;
+		try {
+			axolotlMessage = new XmppAxolotlMessage(message.getContact().getJid().toBareJid(),
+					getOwnDeviceId(), content);
+		} catch (CryptoFailedException e) {
+			Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to encrypt message: " + e.getMessage());
+			return null;
+		}
 
 		if(findSessionsforContact(message.getContact()).isEmpty()) {
 			return null;
@@ -1143,7 +1154,12 @@ public class AxolotlService {
 				byte[] payloadKey = session.processReceiving(header);
 				if (payloadKey != null) {
 					Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Got payload key from axolotl header. Decrypting message...");
-					plaintextMessage = message.decrypt(session, payloadKey, session.getFingerprint());
+					try{
+						plaintextMessage = message.decrypt(session, payloadKey, session.getFingerprint());
+					} catch (CryptoFailedException e) {
+						Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to decrypt message: " + e.getMessage());
+						break;
+					}
 				}
 				Integer preKeyId = session.getPreKeyId();
 				if (preKeyId != null) {

src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java 🔗

@@ -6,6 +6,8 @@ import android.util.Base64;
 import java.security.InvalidAlgorithmParameterException;
 import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.SecureRandom;
 import java.util.HashSet;
 import java.util.Set;
 
@@ -107,26 +109,30 @@ public class XmppAxolotlMessage {
 		}
 	}
 
-	public XmppAxolotlMessage(Jid from, int sourceDeviceId, String plaintext) {
+	public XmppAxolotlMessage(Jid from, int sourceDeviceId, String plaintext) throws CryptoFailedException{
 		this.from = from;
 		this.sourceDeviceId = sourceDeviceId;
 		this.headers = new HashSet<>();
 		this.encrypt(plaintext);
 	}
 
-	private void encrypt(String plaintext) {
+	private void encrypt(String plaintext) throws CryptoFailedException {
 		try {
 			KeyGenerator generator = KeyGenerator.getInstance("AES");
 			generator.init(128);
 			SecretKey secretKey = generator.generateKey();
-			Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
-			cipher.init(Cipher.ENCRYPT_MODE, secretKey);
+			SecureRandom random = new SecureRandom();
+			this.iv = new byte[16];
+			random.nextBytes(iv);
+			IvParameterSpec ivSpec = new IvParameterSpec(iv);
+			Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", "BC");
+			cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec);
 			this.innerKey = secretKey.getEncoded();
-			this.iv = cipher.getIV();
 			this.ciphertext = cipher.doFinal(plaintext.getBytes());
 		} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
-				| IllegalBlockSizeException | BadPaddingException e) {
-
+				| IllegalBlockSizeException | BadPaddingException | NoSuchProviderException
+				| InvalidAlgorithmParameterException e) {
+			throw new CryptoFailedException(e);
 		}
 	}
 
@@ -174,11 +180,11 @@ public class XmppAxolotlMessage {
 	}
 
 
-	public XmppAxolotlPlaintextMessage decrypt(AxolotlService.XmppAxolotlSession session, byte[] key, String fingerprint) {
+	public XmppAxolotlPlaintextMessage decrypt(AxolotlService.XmppAxolotlSession session, byte[] key, String fingerprint) throws CryptoFailedException {
 		XmppAxolotlPlaintextMessage plaintextMessage = null;
 		try {
 
-			Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
+			Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", "BC");
 			SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
 			IvParameterSpec ivSpec = new IvParameterSpec(iv);
 
@@ -189,8 +195,8 @@ public class XmppAxolotlMessage {
 
 		} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
 				| InvalidAlgorithmParameterException | IllegalBlockSizeException
-				| BadPaddingException e) {
-			throw new AssertionError(e);
+				| BadPaddingException | NoSuchProviderException e) {
+			throw new CryptoFailedException(e);
 		}
 		return plaintextMessage;
 	}