mirror of
https://github.com/oxen-io/session-android.git
synced 2024-12-01 05:55:18 +00:00
267 lines
11 KiB
Java
267 lines
11 KiB
Java
/**
|
|
* Copyright (C) 2011 Whisper Systems
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
package org.thoughtcrime.securesms.crypto;
|
|
|
|
import java.io.IOException;
|
|
import java.math.BigInteger;
|
|
import java.security.InvalidAlgorithmParameterException;
|
|
import java.security.InvalidKeyException;
|
|
import java.security.MessageDigest;
|
|
import java.security.NoSuchAlgorithmException;
|
|
|
|
import javax.crypto.BadPaddingException;
|
|
import javax.crypto.Cipher;
|
|
import javax.crypto.IllegalBlockSizeException;
|
|
import javax.crypto.NoSuchPaddingException;
|
|
import javax.crypto.spec.IvParameterSpec;
|
|
import javax.crypto.spec.SecretKeySpec;
|
|
|
|
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
|
|
import org.bouncycastle.crypto.agreement.ECDHBasicAgreement;
|
|
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
|
|
import org.thoughtcrime.securesms.database.keys.InvalidKeyIdException;
|
|
import org.thoughtcrime.securesms.database.keys.LocalKeyRecord;
|
|
import org.thoughtcrime.securesms.database.keys.RemoteKeyRecord;
|
|
import org.thoughtcrime.securesms.database.keys.SessionKey;
|
|
import org.thoughtcrime.securesms.database.keys.SessionRecord;
|
|
import org.thoughtcrime.securesms.protocol.Message;
|
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
|
import org.thoughtcrime.securesms.util.Conversions;
|
|
|
|
import android.content.Context;
|
|
import android.util.Log;
|
|
|
|
/**
|
|
* This is where the session encryption magic happens. Implements a compressed version of the OTR protocol.
|
|
*
|
|
* @author Moxie Marlinspike
|
|
*/
|
|
|
|
public class SessionCipher {
|
|
|
|
public static final Object CIPHER_LOCK = new Object();
|
|
|
|
public static final int CIPHER_KEY_LENGTH = 16;
|
|
public static final int MAC_KEY_LENGTH = 20;
|
|
|
|
public static final int ENCRYPTED_MESSAGE_OVERHEAD = Message.HEADER_LENGTH + MessageMac.MAC_LENGTH;
|
|
// public static final int ENCRYPTED_SINGLE_MESSAGE_BODY_MAX_SIZE = SmsTransportDetails.SINGLE_MESSAGE_MAX_BYTES - ENCRYPTED_MESSAGE_OVERHEAD;
|
|
|
|
private final LocalKeyRecord localRecord;
|
|
private final RemoteKeyRecord remoteRecord;
|
|
private final SessionRecord sessionRecord;
|
|
private final MasterSecret masterSecret;
|
|
private final TransportDetails transportDetails;
|
|
|
|
public SessionCipher(Context context, MasterSecret masterSecret, Recipient recipient, TransportDetails transportDetails) {
|
|
Log.w("SessionCipher", "Constructing session cipher...");
|
|
this.masterSecret = masterSecret;
|
|
this.localRecord = new LocalKeyRecord(context, masterSecret, recipient);
|
|
this.remoteRecord = new RemoteKeyRecord(context, recipient);
|
|
this.sessionRecord = new SessionRecord(context, masterSecret, recipient);
|
|
this.transportDetails = transportDetails;
|
|
}
|
|
|
|
public byte[] encryptMessage(byte[] messageText) {
|
|
Log.w("SessionCipher", "Encrypting message...");
|
|
try {
|
|
int localId = localRecord.getCurrentKeyPair().getId();
|
|
int remoteId = remoteRecord.getCurrentRemoteKey().getId();
|
|
SessionKey sessionKey = getSessionKey(Cipher.ENCRYPT_MODE, localId, remoteId);
|
|
byte[]paddedMessage = transportDetails.getPaddedMessageBody(messageText);
|
|
byte[]cipherText = getCiphertext(paddedMessage, sessionKey.getCipherKey());
|
|
byte[]message = buildMessageFromCiphertext(cipherText);
|
|
byte[]messageWithMac = MessageMac.buildMessageWithMac(message, sessionKey.getMacKey());
|
|
|
|
sessionRecord.setSessionKey(sessionKey);
|
|
sessionRecord.incrementCounter();
|
|
sessionRecord.save();
|
|
|
|
return transportDetails.encodeMessage(messageWithMac);
|
|
} catch (IllegalBlockSizeException e) {
|
|
throw new IllegalArgumentException(e);
|
|
} catch (BadPaddingException e) {
|
|
throw new IllegalArgumentException(e);
|
|
} catch (InvalidKeyIdException e) {
|
|
throw new IllegalArgumentException(e);
|
|
}
|
|
}
|
|
|
|
|
|
public byte[] decryptMessage(byte[] messageText) throws InvalidMessageException {
|
|
Log.w("SessionCipher", "Decrypting message...");
|
|
try {
|
|
byte[] decodedMessage = transportDetails.decodeMessage(messageText);
|
|
Message message = new Message(MessageMac.getMessageWithoutMac(decodedMessage));
|
|
SessionKey sessionKey = getSessionKey(Cipher.DECRYPT_MODE, message.getReceiverKeyId(), message.getSenderKeyId());
|
|
|
|
MessageMac.verifyMac(decodedMessage, sessionKey.getMacKey());
|
|
|
|
byte[] plaintextWithPadding = getPlaintext(message.getMessageText(), sessionKey.getCipherKey(), message.getCounter());
|
|
byte[] plaintext = transportDetails.stripPaddedMessage(plaintextWithPadding);
|
|
|
|
remoteRecord.updateCurrentRemoteKey(message.getNextKey());
|
|
remoteRecord.save();
|
|
|
|
localRecord.advanceKeyIfNecessary(message.getReceiverKeyId());
|
|
localRecord.save();
|
|
|
|
sessionRecord.setSessionKey(sessionKey);
|
|
sessionRecord.setSessionVersion(message.getHighestMutuallySupportedVersion());
|
|
sessionRecord.save();
|
|
|
|
return plaintext;
|
|
} catch (IOException e) {
|
|
throw new InvalidMessageException("Encoding Failure", e);
|
|
} catch (InvalidKeyIdException e) {
|
|
throw new InvalidMessageException("Bad Key ID", e);
|
|
} catch (InvalidMacException e) {
|
|
throw new InvalidMessageException("Bad MAC", e);
|
|
} catch (IllegalBlockSizeException e) {
|
|
throw new InvalidMessageException("assert", e);
|
|
} catch (BadPaddingException e) {
|
|
throw new InvalidMessageException("assert", e);
|
|
}
|
|
}
|
|
|
|
private SecretKeySpec deriveMacSecret(SecretKeySpec key) {
|
|
try {
|
|
MessageDigest md = MessageDigest.getInstance("SHA-1");
|
|
byte[] secret = md.digest(key.getEncoded());
|
|
|
|
return new SecretKeySpec(secret, "HmacSHA1");
|
|
} catch (NoSuchAlgorithmException e) {
|
|
throw new IllegalArgumentException("SHA-1 Not Supported!",e);
|
|
}
|
|
}
|
|
|
|
private byte[] buildMessageFromCiphertext(byte[] cipherText) {
|
|
Message message = new Message(localRecord.getCurrentKeyPair().getId(),
|
|
remoteRecord.getCurrentRemoteKey().getId(),
|
|
localRecord.getNextKeyPair().getPublicKey(),
|
|
sessionRecord.getCounter(),
|
|
cipherText, sessionRecord.getSessionVersion(), Message.SUPPORTED_VERSION);
|
|
|
|
return message.serialize();
|
|
}
|
|
|
|
|
|
private byte[] getPlaintext(byte[] cipherText, SecretKeySpec key, int counter) throws IllegalBlockSizeException, BadPaddingException {
|
|
Cipher cipher = getCipher(Cipher.DECRYPT_MODE, key, counter);
|
|
return cipher.doFinal(cipherText);
|
|
}
|
|
|
|
private byte[] getCiphertext(byte[] message, SecretKeySpec key) throws IllegalBlockSizeException, BadPaddingException {
|
|
Cipher cipher = getCipher(Cipher.ENCRYPT_MODE, key, sessionRecord.getCounter());
|
|
return cipher.doFinal(message);
|
|
}
|
|
|
|
private Cipher getCipher(int mode, SecretKeySpec key, int counter) {
|
|
try {
|
|
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
|
|
|
|
byte[] ivBytes = new byte[16];
|
|
Conversions.mediumToByteArray(ivBytes, 0, counter);
|
|
|
|
IvParameterSpec iv = new IvParameterSpec(ivBytes);
|
|
cipher.init(mode, key, iv);
|
|
|
|
return cipher;
|
|
} catch (NoSuchAlgorithmException e) {
|
|
throw new IllegalArgumentException("AES Not Supported!");
|
|
} catch (NoSuchPaddingException e) {
|
|
throw new IllegalArgumentException("NoPadding Not Supported!");
|
|
} catch (InvalidKeyException e) {
|
|
Log.w("SessionCipher", e);
|
|
throw new IllegalArgumentException("Invaid Key?");
|
|
} catch (InvalidAlgorithmParameterException e) {
|
|
Log.w("SessionCipher", e);
|
|
throw new IllegalArgumentException("Bad IV?");
|
|
}
|
|
}
|
|
|
|
private SecretKeySpec deriveCipherSecret(int mode, BigInteger sharedSecret, int localKeyId, int remoteKeyId) throws InvalidKeyIdException {
|
|
byte[] sharedSecretBytes = sharedSecret.toByteArray();
|
|
byte[] derivedBytes = deriveBytes(sharedSecretBytes, 16 * 2);
|
|
byte[] cipherSecret = new byte[16];
|
|
|
|
boolean isLowEnd = isLowEnd(localKeyId, remoteKeyId);
|
|
isLowEnd = (mode == Cipher.ENCRYPT_MODE ? isLowEnd : !isLowEnd);
|
|
|
|
if (isLowEnd) {
|
|
System.arraycopy(derivedBytes, 16, cipherSecret, 0, 16);
|
|
} else {
|
|
System.arraycopy(derivedBytes, 0, cipherSecret, 0, 16);
|
|
}
|
|
|
|
return new SecretKeySpec(cipherSecret, "AES");
|
|
}
|
|
|
|
private boolean isLowEnd(int localKeyId, int remoteKeyId) throws InvalidKeyIdException {
|
|
ECPublicKeyParameters localPublic = (ECPublicKeyParameters)localRecord.getKeyPairForId(localKeyId).getPublicKey().getKey();
|
|
ECPublicKeyParameters remotePublic = (ECPublicKeyParameters)remoteRecord.getKeyForId(remoteKeyId).getKey();
|
|
BigInteger local = localPublic.getQ().getX().toBigInteger();
|
|
BigInteger remote = remotePublic.getQ().getX().toBigInteger();
|
|
|
|
return local.compareTo(remote) < 0;
|
|
}
|
|
|
|
private byte[] deriveBytes(byte[] seed, int bytesNeeded) {
|
|
MessageDigest md;
|
|
|
|
try {
|
|
md = MessageDigest.getInstance("SHA-256");
|
|
} catch (NoSuchAlgorithmException e) {
|
|
Log.w("SessionCipher",e);
|
|
throw new IllegalArgumentException("SHA-256 Not Supported!");
|
|
}
|
|
|
|
int rounds = bytesNeeded / md.getDigestLength();
|
|
|
|
for (int i=1;i<=rounds;i++) {
|
|
byte[] roundBytes = Conversions.intToByteArray(i);
|
|
md.update(roundBytes);
|
|
md.update(seed);
|
|
}
|
|
|
|
return md.digest();
|
|
}
|
|
|
|
private SessionKey getSessionKey(int mode, int localKeyId, int remoteKeyId) throws InvalidKeyIdException {
|
|
Log.w("SessionCipher", "Getting session key for local: " + localKeyId + " remote: " + remoteKeyId);
|
|
SessionKey sessionKey = sessionRecord.getSessionKey(localKeyId, remoteKeyId);
|
|
if (sessionKey != null) return sessionKey;
|
|
|
|
BigInteger sharedSecret = calculateSharedSecret(localKeyId, remoteKeyId);
|
|
SecretKeySpec cipherKey = deriveCipherSecret(mode, sharedSecret, localKeyId, remoteKeyId);
|
|
SecretKeySpec macKey = deriveMacSecret(cipherKey);
|
|
|
|
return new SessionKey(localKeyId, remoteKeyId, cipherKey, macKey, masterSecret);
|
|
}
|
|
|
|
private BigInteger calculateSharedSecret(int localKeyId, int remoteKeyId) throws InvalidKeyIdException {
|
|
ECDHBasicAgreement agreement = new ECDHBasicAgreement();
|
|
AsymmetricCipherKeyPair localKeyPair = localRecord.getKeyPairForId(localKeyId).getKeyPair();
|
|
ECPublicKeyParameters remoteKey = remoteRecord.getKeyForId(remoteKeyId).getKey();
|
|
|
|
agreement.init(localKeyPair.getPrivate());
|
|
BigInteger secret = KeyUtil.calculateAgreement(agreement, remoteKey);
|
|
|
|
return secret;
|
|
}
|
|
}
|