mirror of
https://github.com/oxen-io/session-android.git
synced 2025-08-20 21:17:44 +00:00
Support for Axolotl protocol.
1) Split code into v1 and v2 message paths. 2) Do the Axolotl protocol for v2. 3) Switch all v2 entities to protobuf.
This commit is contained in:
54
library/protobuf/LocalStorageProtocol.proto
Normal file
54
library/protobuf/LocalStorageProtocol.proto
Normal file
@@ -0,0 +1,54 @@
|
||||
package textsecure;
|
||||
|
||||
option java_package = "org.whispersystems.textsecure.storage";
|
||||
option java_outer_classname = "StorageProtos";
|
||||
|
||||
message SessionStructure {
|
||||
message Chain {
|
||||
optional bytes senderEphemeral = 1;
|
||||
optional bytes senderEphemeralPrivate = 2;
|
||||
|
||||
message ChainKey {
|
||||
optional uint32 index = 1;
|
||||
optional bytes key = 2;
|
||||
}
|
||||
|
||||
optional ChainKey chainKey = 3;
|
||||
|
||||
message MessageKey {
|
||||
optional uint32 index = 1;
|
||||
optional bytes cipherKey = 2;
|
||||
optional bytes macKey = 3;
|
||||
}
|
||||
|
||||
repeated MessageKey messageKeys = 4;
|
||||
}
|
||||
|
||||
message PendingKeyExchange {
|
||||
optional uint32 sequence = 1;
|
||||
optional bytes localBaseKey = 2;
|
||||
optional bytes localBaseKeyPrivate = 3;
|
||||
optional bytes localEphemeralKey = 4;
|
||||
optional bytes localEphemeralKeyPrivate = 5;
|
||||
optional bytes localIdentityKey = 7;
|
||||
optional bytes localIdentityKeyPrivate = 8;
|
||||
}
|
||||
|
||||
message PendingPreKey {
|
||||
optional uint32 preKeyId = 1;
|
||||
optional bytes baseKey = 2;
|
||||
}
|
||||
|
||||
optional uint32 sessionVersion = 1;
|
||||
optional bytes localIdentityPublic = 2;
|
||||
optional bytes remoteIdentityPublic = 3;
|
||||
|
||||
optional bytes rootKey = 4;
|
||||
optional uint32 previousCounter = 5;
|
||||
|
||||
optional Chain senderChain = 6;
|
||||
repeated Chain receiverChains = 7;
|
||||
|
||||
optional PendingKeyExchange pendingKeyExchange = 8;
|
||||
optional PendingPreKey pendingPreKey = 9;
|
||||
}
|
@@ -1,3 +1,3 @@
|
||||
|
||||
all:
|
||||
protoc --java_out=../src/ IncomingPushMessageSignal.proto
|
||||
protoc --java_out=../src/ IncomingPushMessageSignal.proto WhisperTextProtocol.proto LocalStorageProtocol.proto
|
||||
|
25
library/protobuf/WhisperTextProtocol.proto
Normal file
25
library/protobuf/WhisperTextProtocol.proto
Normal file
@@ -0,0 +1,25 @@
|
||||
package textsecure;
|
||||
|
||||
option java_package = "org.whispersystems.textsecure.crypto.protocol";
|
||||
option java_outer_classname = "WhisperProtos";
|
||||
|
||||
message WhisperMessage {
|
||||
optional bytes ephemeralKey = 1;
|
||||
optional uint32 counter = 2;
|
||||
optional uint32 previousCounter = 3;
|
||||
optional bytes ciphertext = 4;
|
||||
}
|
||||
|
||||
message PreKeyWhisperMessage {
|
||||
optional uint32 preKeyId = 1;
|
||||
optional bytes baseKey = 2;
|
||||
optional bytes identityKey = 3;
|
||||
optional bytes message = 4; // WhisperMessage
|
||||
}
|
||||
|
||||
message KeyExchangeMessage {
|
||||
optional uint32 id = 1;
|
||||
optional bytes baseKey = 2;
|
||||
optional bytes ephemeralKey = 3;
|
||||
optional bytes identityKey = 4;
|
||||
}
|
@@ -47,8 +47,7 @@ public class IdentityKey implements Parcelable, SerializableKey {
|
||||
}
|
||||
};
|
||||
|
||||
public static final int SIZE = 1 + ECPublicKey.KEY_SIZE;
|
||||
private static final int CURRENT_VESION = 1;
|
||||
public static final int NIST_SIZE = 1 + ECPublicKey.KEY_SIZE;
|
||||
|
||||
private ECPublicKey publicKey;
|
||||
|
||||
@@ -73,19 +72,22 @@ public class IdentityKey implements Parcelable, SerializableKey {
|
||||
}
|
||||
|
||||
private void initializeFromSerialized(byte[] bytes, int offset) throws InvalidKeyException {
|
||||
int version = bytes[offset] & 0xff;
|
||||
|
||||
if (version > CURRENT_VESION)
|
||||
throw new InvalidKeyException("Unsupported key version: " + version);
|
||||
|
||||
this.publicKey = Curve.decodePoint(bytes, offset + 1);
|
||||
if ((bytes[offset] & 0xff) == 1) {
|
||||
this.publicKey = Curve.decodePoint(bytes, offset +1);
|
||||
} else {
|
||||
this.publicKey = Curve.decodePoint(bytes, offset);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] serialize() {
|
||||
byte[] versionBytes = {(byte)CURRENT_VESION};
|
||||
byte[] encodedKey = publicKey.serialize();
|
||||
if (publicKey.getType() == Curve.NIST_TYPE) {
|
||||
byte[] versionBytes = {0x01};
|
||||
byte[] encodedKey = publicKey.serialize();
|
||||
|
||||
return Util.combine(versionBytes, encodedKey);
|
||||
return Util.combine(versionBytes, encodedKey);
|
||||
} else {
|
||||
return publicKey.serialize();
|
||||
}
|
||||
}
|
||||
|
||||
public String getFingerprint() {
|
||||
|
@@ -1,92 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2013 Open 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.whispersystems.textsecure.crypto;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.ecc.Curve;
|
||||
import org.whispersystems.textsecure.storage.CanonicalRecipientAddress;
|
||||
import org.whispersystems.textsecure.storage.LocalKeyRecord;
|
||||
import org.whispersystems.textsecure.storage.RemoteKeyRecord;
|
||||
import org.whispersystems.textsecure.storage.SessionRecord;
|
||||
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
/**
|
||||
* Helper class for generating key pairs and calculating ECDH agreements.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class KeyUtil {
|
||||
|
||||
public static void abortSessionFor(Context context, CanonicalRecipientAddress recipient) {
|
||||
//XXX Obviously we should probably do something more thorough here eventually.
|
||||
Log.w("KeyUtil", "Aborting session, deleting keys...");
|
||||
LocalKeyRecord.delete(context, recipient);
|
||||
RemoteKeyRecord.delete(context, recipient);
|
||||
SessionRecord.delete(context, recipient);
|
||||
}
|
||||
|
||||
public static boolean isSessionFor(Context context, CanonicalRecipientAddress recipient) {
|
||||
Log.w("KeyUtil", "Checking session...");
|
||||
return
|
||||
(LocalKeyRecord.hasRecord(context, recipient)) &&
|
||||
(RemoteKeyRecord.hasRecord(context, recipient)) &&
|
||||
(SessionRecord.hasSession(context, recipient));
|
||||
}
|
||||
|
||||
public static boolean isNonPrekeySessionFor(Context context, MasterSecret masterSecret, CanonicalRecipientAddress recipient) {
|
||||
return isSessionFor(context, recipient) &&
|
||||
!(new SessionRecord(context, masterSecret, recipient).isPrekeyBundleRequired());
|
||||
}
|
||||
|
||||
public static boolean isIdentityKeyFor(Context context,
|
||||
MasterSecret masterSecret,
|
||||
CanonicalRecipientAddress recipient)
|
||||
{
|
||||
return isSessionFor(context, recipient) &&
|
||||
new SessionRecord(context, masterSecret, recipient).getIdentityKey() != null;
|
||||
}
|
||||
|
||||
public static LocalKeyRecord initializeRecordFor(Context context,
|
||||
MasterSecret masterSecret,
|
||||
CanonicalRecipientAddress recipient,
|
||||
int sessionVersion)
|
||||
{
|
||||
Log.w("KeyUtil", "Initializing local key pairs...");
|
||||
try {
|
||||
SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
|
||||
int initialId = secureRandom.nextInt(4094) + 1;
|
||||
|
||||
KeyPair currentPair = new KeyPair(initialId, Curve.generateKeyPairForSession(sessionVersion), masterSecret);
|
||||
KeyPair nextPair = new KeyPair(initialId + 1, Curve.generateKeyPairForSession(sessionVersion), masterSecret);
|
||||
LocalKeyRecord record = new LocalKeyRecord(context, masterSecret, recipient);
|
||||
|
||||
record.setCurrentKeyPair(currentPair);
|
||||
record.setNextKeyPair(nextPair);
|
||||
record.save();
|
||||
|
||||
return record;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -1,85 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
* Copyright (C) 2013 Open 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.whispersystems.textsecure.crypto;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.SessionCipher.SessionCipherContext;
|
||||
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
|
||||
import org.whispersystems.textsecure.storage.CanonicalRecipientAddress;
|
||||
|
||||
/**
|
||||
* Parses and serializes the encrypted message format.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class MessageCipher {
|
||||
|
||||
private final Context context;
|
||||
private final MasterSecret masterSecret;
|
||||
private final IdentityKeyPair localIdentityKey;
|
||||
|
||||
public MessageCipher(Context context, MasterSecret masterSecret, IdentityKeyPair localIdentityKey) {
|
||||
this.context = context.getApplicationContext();
|
||||
this.masterSecret = masterSecret;
|
||||
this.localIdentityKey = localIdentityKey;
|
||||
}
|
||||
|
||||
public CiphertextMessage encrypt(CanonicalRecipientAddress recipient, byte[] paddedBody) {
|
||||
synchronized (SessionCipher.CIPHER_LOCK) {
|
||||
SessionCipher sessionCipher = new SessionCipher();
|
||||
SessionCipherContext sessionContext = sessionCipher.getEncryptionContext(context, masterSecret, localIdentityKey, recipient);
|
||||
byte[] ciphertextBody = sessionCipher.encrypt(sessionContext, paddedBody);
|
||||
|
||||
return new CiphertextMessage(sessionContext, ciphertextBody);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] decrypt(CanonicalRecipientAddress recipient, byte[] ciphertext)
|
||||
throws InvalidMessageException
|
||||
{
|
||||
synchronized (SessionCipher.CIPHER_LOCK) {
|
||||
try {
|
||||
CiphertextMessage message = new CiphertextMessage(ciphertext);
|
||||
|
||||
int messageVersion = message.getCurrentVersion();
|
||||
int senderKeyId = message.getSenderKeyId();
|
||||
int receiverKeyId = message.getReceiverKeyId();
|
||||
PublicKey nextRemoteKey = new PublicKey(message.getNextKeyBytes());
|
||||
int counter = message.getCounter();
|
||||
byte[] body = message.getBody();
|
||||
|
||||
SessionCipher sessionCipher = new SessionCipher();
|
||||
SessionCipherContext sessionContext = sessionCipher.getDecryptionContext(context, masterSecret,
|
||||
localIdentityKey,
|
||||
recipient, senderKeyId,
|
||||
receiverKeyId,
|
||||
nextRemoteKey,
|
||||
counter,
|
||||
messageVersion);
|
||||
|
||||
message.verifyMac(sessionContext);
|
||||
|
||||
return sessionCipher.decrypt(sessionContext, body);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,68 +0,0 @@
|
||||
/**
|
||||
* 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.whispersystems.textsecure.crypto;
|
||||
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.whispersystems.textsecure.util.Hex;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
public class MessageMac {
|
||||
|
||||
public static final int MAC_LENGTH = 10;
|
||||
|
||||
public static byte[] calculateMac(byte[] message, int offset, int length, SecretKeySpec macKey) {
|
||||
try {
|
||||
Mac mac = Mac.getInstance("HmacSHA1");
|
||||
mac.init(macKey);
|
||||
|
||||
assert(mac.getMacLength() >= MAC_LENGTH);
|
||||
|
||||
mac.update(message, offset, length);
|
||||
byte[] macBytes = mac.doFinal();
|
||||
byte[] truncatedMacBytes = new byte[MAC_LENGTH];
|
||||
System.arraycopy(macBytes, 0, truncatedMacBytes, 0, truncatedMacBytes.length);
|
||||
|
||||
return truncatedMacBytes;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void verifyMac(byte[] message, int offset, int length,
|
||||
byte[] receivedMac, SecretKeySpec macKey)
|
||||
throws InvalidMacException
|
||||
{
|
||||
byte[] localMac = calculateMac(message, offset, length, macKey);
|
||||
|
||||
Log.w("MessageMac", "Local Mac: " + Hex.toString(localMac));
|
||||
Log.w("MessageMac", "Remot Mac: " + Hex.toString(receivedMac));
|
||||
|
||||
if (!Arrays.equals(localMac, receivedMac)) {
|
||||
throw new InvalidMacException("MAC on message does not match calculated MAC.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -16,345 +16,31 @@
|
||||
*/
|
||||
package org.whispersystems.textsecure.crypto;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
|
||||
import org.whispersystems.textsecure.crypto.kdf.DerivedSecrets;
|
||||
import android.content.Context;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
|
||||
import org.whispersystems.textsecure.storage.CanonicalRecipientAddress;
|
||||
import org.whispersystems.textsecure.storage.InvalidKeyIdException;
|
||||
import org.whispersystems.textsecure.storage.LocalKeyRecord;
|
||||
import org.whispersystems.textsecure.storage.RemoteKeyRecord;
|
||||
import org.whispersystems.textsecure.storage.SessionKey;
|
||||
import org.whispersystems.textsecure.storage.SessionRecord;
|
||||
import org.whispersystems.textsecure.util.Conversions;
|
||||
import org.whispersystems.textsecure.storage.SessionRecordV1;
|
||||
import org.whispersystems.textsecure.storage.SessionRecordV2;
|
||||
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
public abstract class SessionCipher {
|
||||
|
||||
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;
|
||||
protected static final Object SESSION_LOCK = new Object();
|
||||
|
||||
/**
|
||||
* This is where the session encryption magic happens. Implements a compressed version of the OTR protocol.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
public abstract CiphertextMessage encrypt(byte[] paddedMessage);
|
||||
public abstract byte[] decrypt(byte[] decodedMessage) throws InvalidMessageException;
|
||||
|
||||
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 SessionCipherContext getEncryptionContext(Context context,
|
||||
MasterSecret masterSecret,
|
||||
IdentityKeyPair localIdentityKey,
|
||||
CanonicalRecipientAddress recipient)
|
||||
public static SessionCipher createFor(Context context, MasterSecret masterSecret,
|
||||
CanonicalRecipientAddress recipient)
|
||||
{
|
||||
try {
|
||||
KeyRecords records = getKeyRecords(context, masterSecret, recipient);
|
||||
int localKeyId = records.getLocalKeyRecord().getCurrentKeyPair().getId();
|
||||
int remoteKeyId = records.getRemoteKeyRecord().getCurrentRemoteKey().getId();
|
||||
int sessionVersion = records.getSessionRecord().getSessionVersion();
|
||||
SessionKey sessionKey = getSessionKey(masterSecret, Cipher.ENCRYPT_MODE, sessionVersion, localIdentityKey, records, localKeyId, remoteKeyId);
|
||||
PublicKey nextKey = records.getLocalKeyRecord().getNextKeyPair().getPublicKey();
|
||||
int counter = records.getSessionRecord().getCounter();
|
||||
|
||||
|
||||
return new SessionCipherContext(records, sessionKey, localKeyId, remoteKeyId,
|
||||
nextKey, counter, sessionVersion);
|
||||
} catch (InvalidKeyIdException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public SessionCipherContext getDecryptionContext(Context context, MasterSecret masterSecret,
|
||||
IdentityKeyPair localIdentityKey,
|
||||
CanonicalRecipientAddress recipient,
|
||||
int senderKeyId, int recipientKeyId,
|
||||
PublicKey nextKey, int counter,
|
||||
int messageVersion)
|
||||
throws InvalidMessageException
|
||||
{
|
||||
try {
|
||||
KeyRecords records = getKeyRecords(context, masterSecret, recipient);
|
||||
|
||||
if (messageVersion < records.getSessionRecord().getNegotiatedSessionVersion()) {
|
||||
throw new InvalidMessageException("Message version: " + messageVersion +
|
||||
" but negotiated session version: " +
|
||||
records.getSessionRecord().getNegotiatedSessionVersion());
|
||||
}
|
||||
|
||||
SessionKey sessionKey = getSessionKey(masterSecret, Cipher.DECRYPT_MODE, messageVersion,
|
||||
localIdentityKey, records, recipientKeyId, senderKeyId);
|
||||
|
||||
return new SessionCipherContext(records, sessionKey, senderKeyId,
|
||||
recipientKeyId, nextKey, counter,
|
||||
messageVersion);
|
||||
} catch (InvalidKeyIdException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] encrypt(SessionCipherContext context, byte[] paddedMessageBody) {
|
||||
Log.w("SessionCipher", "Encrypting message...");
|
||||
try {
|
||||
byte[]cipherText = getCiphertext(paddedMessageBody, context.getSessionKey().getCipherKey(), context.getSessionRecord().getCounter());
|
||||
|
||||
context.getSessionRecord().setSessionKey(context.getSessionKey());
|
||||
context.getSessionRecord().incrementCounter();
|
||||
context.getSessionRecord().save();
|
||||
|
||||
return cipherText;
|
||||
} catch (IllegalBlockSizeException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
} catch (BadPaddingException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] decrypt(SessionCipherContext context, byte[] decodedCiphertext)
|
||||
throws InvalidMessageException
|
||||
{
|
||||
Log.w("SessionCipher", "Decrypting message...");
|
||||
try {
|
||||
byte[] plaintextWithPadding = getPlaintext(decodedCiphertext,
|
||||
context.getSessionKey().getCipherKey(),
|
||||
context.getCounter());
|
||||
|
||||
context.getRemoteKeyRecord().updateCurrentRemoteKey(context.getNextKey());
|
||||
context.getRemoteKeyRecord().save();
|
||||
|
||||
context.getLocalKeyRecord().advanceKeyIfNecessary(context.getRecipientKeyId());
|
||||
context.getLocalKeyRecord().save();
|
||||
|
||||
context.getSessionRecord().setSessionKey(context.getSessionKey());
|
||||
context.getSessionRecord().setPrekeyBundleRequired(false);
|
||||
context.getSessionRecord().save();
|
||||
|
||||
return plaintextWithPadding;
|
||||
} catch (IllegalBlockSizeException e) {
|
||||
throw new InvalidMessageException("assert", e);
|
||||
} catch (BadPaddingException e) {
|
||||
throw new InvalidMessageException("assert", e);
|
||||
}
|
||||
}
|
||||
|
||||
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, int counter)
|
||||
throws IllegalBlockSizeException, BadPaddingException
|
||||
{
|
||||
Cipher cipher = getCipher(Cipher.ENCRYPT_MODE, key, counter);
|
||||
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 (java.security.InvalidKeyException e) {
|
||||
Log.w("SessionCipher", e);
|
||||
throw new IllegalArgumentException("Invaid Key?");
|
||||
} catch (InvalidAlgorithmParameterException e) {
|
||||
Log.w("SessionCipher", e);
|
||||
throw new IllegalArgumentException("Bad IV?");
|
||||
}
|
||||
}
|
||||
|
||||
private SessionKey getSessionKey(MasterSecret masterSecret, int mode,
|
||||
int messageVersion,
|
||||
IdentityKeyPair localIdentityKey,
|
||||
KeyRecords records,
|
||||
int localKeyId, int remoteKeyId)
|
||||
throws InvalidKeyIdException, InvalidKeyException
|
||||
{
|
||||
Log.w("SessionCipher", "Getting session key for local: " + localKeyId + " remote: " + remoteKeyId);
|
||||
SessionKey sessionKey = records.getSessionRecord().getSessionKey(mode, localKeyId, remoteKeyId);
|
||||
|
||||
if (sessionKey != null)
|
||||
return sessionKey;
|
||||
|
||||
DerivedSecrets derivedSecrets = calculateSharedSecret(messageVersion, mode, localIdentityKey,
|
||||
records, localKeyId, remoteKeyId);
|
||||
|
||||
return new SessionKey(mode, localKeyId, remoteKeyId, derivedSecrets.getCipherKey(),
|
||||
derivedSecrets.getMacKey(), masterSecret);
|
||||
}
|
||||
|
||||
private DerivedSecrets calculateSharedSecret(int messageVersion, int mode,
|
||||
IdentityKeyPair localIdentityKey,
|
||||
KeyRecords records,
|
||||
int localKeyId, int remoteKeyId)
|
||||
throws InvalidKeyIdException, InvalidKeyException
|
||||
{
|
||||
KeyPair localKeyPair = records.getLocalKeyRecord().getKeyPairForId(localKeyId);
|
||||
ECPublicKey remoteKey = records.getRemoteKeyRecord().getKeyForId(remoteKeyId).getKey();
|
||||
IdentityKey remoteIdentityKey = records.getSessionRecord().getIdentityKey();
|
||||
boolean isLowEnd = isLowEnd(records, localKeyId, remoteKeyId);
|
||||
|
||||
isLowEnd = (mode == Cipher.ENCRYPT_MODE ? isLowEnd : !isLowEnd);
|
||||
|
||||
if (isInitiallyExchangedKeys(records, localKeyId, remoteKeyId) &&
|
||||
messageVersion >= CiphertextMessage.DHE3_INTRODUCED_VERSION)
|
||||
{
|
||||
return SharedSecretCalculator.calculateSharedSecret(isLowEnd,
|
||||
localKeyPair, localKeyId, localIdentityKey,
|
||||
remoteKey, remoteKeyId, remoteIdentityKey);
|
||||
if (SessionRecordV2.hasSession(context, masterSecret, recipient)) {
|
||||
return new SessionCipherV2(context, masterSecret, recipient);
|
||||
} else if (SessionRecordV1.hasSession(context, recipient)) {
|
||||
return new SessionCipherV1(context, masterSecret, recipient);
|
||||
} else {
|
||||
return SharedSecretCalculator.calculateSharedSecret(messageVersion, isLowEnd,
|
||||
localKeyPair, localKeyId,
|
||||
remoteKey, remoteKeyId);
|
||||
throw new AssertionError("Attempt to initialize cipher for non-existing session.");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isLowEnd(KeyRecords records, int localKeyId, int remoteKeyId)
|
||||
throws InvalidKeyIdException
|
||||
{
|
||||
ECPublicKey localPublic = records.getLocalKeyRecord().getKeyPairForId(localKeyId).getPublicKey().getKey();
|
||||
ECPublicKey remotePublic = records.getRemoteKeyRecord().getKeyForId(remoteKeyId).getKey();
|
||||
|
||||
return localPublic.compareTo(remotePublic) < 0;
|
||||
}
|
||||
|
||||
private boolean isInitiallyExchangedKeys(KeyRecords records, int localKeyId, int remoteKeyId)
|
||||
throws InvalidKeyIdException
|
||||
{
|
||||
byte[] localFingerprint = records.getSessionRecord().getLocalFingerprint();
|
||||
byte[] remoteFingerprint = records.getSessionRecord().getRemoteFingerprint();
|
||||
|
||||
return Arrays.equals(localFingerprint, records.getLocalKeyRecord().getKeyPairForId(localKeyId).getPublicKey().getFingerprintBytes()) &&
|
||||
Arrays.equals(remoteFingerprint, records.getRemoteKeyRecord().getKeyForId(remoteKeyId).getFingerprintBytes());
|
||||
}
|
||||
|
||||
private KeyRecords getKeyRecords(Context context, MasterSecret masterSecret,
|
||||
CanonicalRecipientAddress recipient)
|
||||
{
|
||||
LocalKeyRecord localKeyRecord = new LocalKeyRecord(context, masterSecret, recipient);
|
||||
RemoteKeyRecord remoteKeyRecord = new RemoteKeyRecord(context, recipient);
|
||||
SessionRecord sessionRecord = new SessionRecord(context, masterSecret, recipient);
|
||||
return new KeyRecords(localKeyRecord, remoteKeyRecord, sessionRecord);
|
||||
}
|
||||
|
||||
private static class KeyRecords {
|
||||
|
||||
private final LocalKeyRecord localKeyRecord;
|
||||
private final RemoteKeyRecord remoteKeyRecord;
|
||||
private final SessionRecord sessionRecord;
|
||||
|
||||
public KeyRecords(LocalKeyRecord localKeyRecord, RemoteKeyRecord remoteKeyRecord, SessionRecord sessionRecord) {
|
||||
this.localKeyRecord = localKeyRecord;
|
||||
this.remoteKeyRecord = remoteKeyRecord;
|
||||
this.sessionRecord = sessionRecord;
|
||||
}
|
||||
|
||||
private LocalKeyRecord getLocalKeyRecord() {
|
||||
return localKeyRecord;
|
||||
}
|
||||
|
||||
private RemoteKeyRecord getRemoteKeyRecord() {
|
||||
return remoteKeyRecord;
|
||||
}
|
||||
|
||||
private SessionRecord getSessionRecord() {
|
||||
return sessionRecord;
|
||||
}
|
||||
}
|
||||
|
||||
public static class SessionCipherContext {
|
||||
|
||||
private final LocalKeyRecord localKeyRecord;
|
||||
private final RemoteKeyRecord remoteKeyRecord;
|
||||
private final SessionRecord sessionRecord;
|
||||
private final SessionKey sessionKey;
|
||||
private final int senderKeyId;
|
||||
private final int recipientKeyId;
|
||||
private final PublicKey nextKey;
|
||||
private final int counter;
|
||||
private final int messageVersion;
|
||||
|
||||
public SessionCipherContext(KeyRecords records,
|
||||
SessionKey sessionKey,
|
||||
int senderKeyId,
|
||||
int receiverKeyId,
|
||||
PublicKey nextKey,
|
||||
int counter,
|
||||
int messageVersion)
|
||||
{
|
||||
this.localKeyRecord = records.getLocalKeyRecord();
|
||||
this.remoteKeyRecord = records.getRemoteKeyRecord();
|
||||
this.sessionRecord = records.getSessionRecord();
|
||||
this.sessionKey = sessionKey;
|
||||
this.senderKeyId = senderKeyId;
|
||||
this.recipientKeyId = receiverKeyId;
|
||||
this.nextKey = nextKey;
|
||||
this.counter = counter;
|
||||
this.messageVersion = messageVersion;
|
||||
}
|
||||
|
||||
public LocalKeyRecord getLocalKeyRecord() {
|
||||
return localKeyRecord;
|
||||
}
|
||||
|
||||
public RemoteKeyRecord getRemoteKeyRecord() {
|
||||
return remoteKeyRecord;
|
||||
}
|
||||
|
||||
public SessionRecord getSessionRecord() {
|
||||
return sessionRecord;
|
||||
}
|
||||
|
||||
public SessionKey getSessionKey() {
|
||||
return sessionKey;
|
||||
}
|
||||
|
||||
public PublicKey getNextKey() {
|
||||
return nextKey;
|
||||
}
|
||||
|
||||
public int getCounter() {
|
||||
return counter;
|
||||
}
|
||||
|
||||
public int getSenderKeyId() {
|
||||
return senderKeyId;
|
||||
}
|
||||
|
||||
public int getRecipientKeyId() {
|
||||
return recipientKeyId;
|
||||
}
|
||||
|
||||
public int getMessageVersion() {
|
||||
return messageVersion;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -0,0 +1,325 @@
|
||||
package org.whispersystems.textsecure.crypto;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.ecc.Curve;
|
||||
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
|
||||
import org.whispersystems.textsecure.crypto.kdf.DerivedSecrets;
|
||||
import org.whispersystems.textsecure.crypto.kdf.NKDF;
|
||||
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
|
||||
import org.whispersystems.textsecure.crypto.protocol.WhisperMessageV1;
|
||||
import org.whispersystems.textsecure.storage.CanonicalRecipientAddress;
|
||||
import org.whispersystems.textsecure.storage.InvalidKeyIdException;
|
||||
import org.whispersystems.textsecure.storage.LocalKeyRecord;
|
||||
import org.whispersystems.textsecure.storage.RemoteKeyRecord;
|
||||
import org.whispersystems.textsecure.storage.SessionKey;
|
||||
import org.whispersystems.textsecure.storage.SessionRecordV1;
|
||||
import org.whispersystems.textsecure.util.Conversions;
|
||||
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
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;
|
||||
|
||||
public class SessionCipherV1 extends SessionCipher {
|
||||
|
||||
private final Context context;
|
||||
private final MasterSecret masterSecret;
|
||||
private final CanonicalRecipientAddress recipient;
|
||||
|
||||
public SessionCipherV1(Context context, MasterSecret masterSecret,
|
||||
CanonicalRecipientAddress recipient)
|
||||
{
|
||||
this.context = context;
|
||||
this.masterSecret = masterSecret;
|
||||
this.recipient = recipient;
|
||||
}
|
||||
|
||||
public CiphertextMessage encrypt(byte[] paddedMessageBody) {
|
||||
synchronized (SESSION_LOCK) {
|
||||
SessionCipherContext encryptionContext = getEncryptionContext();
|
||||
byte[] cipherText = getCiphertext(paddedMessageBody,
|
||||
encryptionContext.getSessionKey().getCipherKey(),
|
||||
encryptionContext.getSessionRecord().getCounter());
|
||||
|
||||
encryptionContext.getSessionRecord().setSessionKey(encryptionContext.getSessionKey());
|
||||
encryptionContext.getSessionRecord().incrementCounter();
|
||||
encryptionContext.getSessionRecord().save();
|
||||
|
||||
return new WhisperMessageV1(encryptionContext, cipherText);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] decrypt(byte[] decodedCiphertext) throws InvalidMessageException {
|
||||
synchronized (SESSION_LOCK) {
|
||||
WhisperMessageV1 message = new WhisperMessageV1(decodedCiphertext);
|
||||
SessionCipherContext decryptionContext = getDecryptionContext(message);
|
||||
|
||||
message.verifyMac(decryptionContext);
|
||||
|
||||
byte[] plaintextWithPadding = getPlaintext(message.getBody(),
|
||||
decryptionContext.getSessionKey().getCipherKey(),
|
||||
decryptionContext.getCounter());
|
||||
|
||||
decryptionContext.getRemoteKeyRecord().updateCurrentRemoteKey(decryptionContext.getNextKey());
|
||||
decryptionContext.getRemoteKeyRecord().save();
|
||||
|
||||
decryptionContext.getLocalKeyRecord().advanceKeyIfNecessary(decryptionContext.getRecipientKeyId());
|
||||
decryptionContext.getLocalKeyRecord().save();
|
||||
|
||||
decryptionContext.getSessionRecord().setSessionKey(decryptionContext.getSessionKey());
|
||||
decryptionContext.getSessionRecord().save();
|
||||
|
||||
return plaintextWithPadding;
|
||||
}
|
||||
}
|
||||
|
||||
private SessionCipherContext getEncryptionContext() {
|
||||
try {
|
||||
KeyRecords records = getKeyRecords(context, masterSecret, recipient);
|
||||
int localKeyId = records.getLocalKeyRecord().getCurrentKeyPair().getId();
|
||||
int remoteKeyId = records.getRemoteKeyRecord().getCurrentRemoteKey().getId();
|
||||
int sessionVersion = records.getSessionRecord().getSessionVersion();
|
||||
SessionKey sessionKey = getSessionKey(masterSecret, Cipher.ENCRYPT_MODE,
|
||||
records, localKeyId, remoteKeyId);
|
||||
PublicKey nextKey = records.getLocalKeyRecord().getNextKeyPair().getPublicKey();
|
||||
int counter = records.getSessionRecord().getCounter();
|
||||
|
||||
|
||||
return new SessionCipherContext(records, sessionKey, localKeyId, remoteKeyId,
|
||||
nextKey, counter, sessionVersion);
|
||||
} catch (InvalidKeyIdException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public SessionCipherContext getDecryptionContext(WhisperMessageV1 message)
|
||||
throws InvalidMessageException
|
||||
{
|
||||
try {
|
||||
KeyRecords records = getKeyRecords(context, masterSecret, recipient);
|
||||
int messageVersion = message.getCurrentVersion();
|
||||
int recipientKeyId = message.getReceiverKeyId();
|
||||
int senderKeyId = message.getSenderKeyId();
|
||||
PublicKey nextKey = new PublicKey(message.getNextKeyBytes());
|
||||
int counter = message.getCounter();
|
||||
|
||||
if (messageVersion < records.getSessionRecord().getSessionVersion()) {
|
||||
throw new InvalidMessageException("Message version: " + messageVersion +
|
||||
" but negotiated session version: " +
|
||||
records.getSessionRecord().getSessionVersion());
|
||||
}
|
||||
|
||||
SessionKey sessionKey = getSessionKey(masterSecret, Cipher.DECRYPT_MODE,
|
||||
records, recipientKeyId, senderKeyId);
|
||||
|
||||
return new SessionCipherContext(records, sessionKey, senderKeyId,
|
||||
recipientKeyId, nextKey, counter,
|
||||
messageVersion);
|
||||
} catch (InvalidKeyIdException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] getCiphertext(byte[] message, SecretKeySpec key, int counter) {
|
||||
try {
|
||||
Cipher cipher = getCipher(Cipher.ENCRYPT_MODE, key, counter);
|
||||
return cipher.doFinal(message);
|
||||
} catch (IllegalBlockSizeException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (BadPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] getPlaintext(byte[] cipherText, SecretKeySpec key, int counter) {
|
||||
try {
|
||||
Cipher cipher = getCipher(Cipher.DECRYPT_MODE, key, counter);
|
||||
return cipher.doFinal(cipherText);
|
||||
} catch (IllegalBlockSizeException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (BadPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
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 (java.security.InvalidKeyException e) {
|
||||
Log.w("SessionCipher", e);
|
||||
throw new IllegalArgumentException("Invaid Key?");
|
||||
} catch (InvalidAlgorithmParameterException e) {
|
||||
Log.w("SessionCipher", e);
|
||||
throw new IllegalArgumentException("Bad IV?");
|
||||
}
|
||||
}
|
||||
|
||||
private SessionKey getSessionKey(MasterSecret masterSecret, int mode,
|
||||
KeyRecords records,
|
||||
int localKeyId, int remoteKeyId)
|
||||
throws InvalidKeyIdException, InvalidKeyException
|
||||
{
|
||||
Log.w("SessionCipher", "Getting session key for local: " + localKeyId + " remote: " + remoteKeyId);
|
||||
SessionKey sessionKey = records.getSessionRecord().getSessionKey(mode, localKeyId, remoteKeyId);
|
||||
|
||||
if (sessionKey != null)
|
||||
return sessionKey;
|
||||
|
||||
DerivedSecrets derivedSecrets = calculateSharedSecret(mode, records, localKeyId, remoteKeyId);
|
||||
|
||||
return new SessionKey(mode, localKeyId, remoteKeyId, derivedSecrets.getCipherKey(),
|
||||
derivedSecrets.getMacKey(), masterSecret);
|
||||
}
|
||||
|
||||
private DerivedSecrets calculateSharedSecret(int mode, KeyRecords records,
|
||||
int localKeyId, int remoteKeyId)
|
||||
throws InvalidKeyIdException, InvalidKeyException
|
||||
{
|
||||
NKDF kdf = new NKDF();
|
||||
KeyPair localKeyPair = records.getLocalKeyRecord().getKeyPairForId(localKeyId);
|
||||
ECPublicKey remoteKey = records.getRemoteKeyRecord().getKeyForId(remoteKeyId).getKey();
|
||||
byte[] sharedSecret = Curve.calculateAgreement(remoteKey, localKeyPair.getPrivateKey());
|
||||
boolean isLowEnd = isLowEnd(records, localKeyId, remoteKeyId);
|
||||
|
||||
isLowEnd = (mode == Cipher.ENCRYPT_MODE ? isLowEnd : !isLowEnd);
|
||||
|
||||
return kdf.deriveSecrets(sharedSecret, isLowEnd);
|
||||
}
|
||||
|
||||
private boolean isLowEnd(KeyRecords records, int localKeyId, int remoteKeyId)
|
||||
throws InvalidKeyIdException
|
||||
{
|
||||
ECPublicKey localPublic = records.getLocalKeyRecord().getKeyPairForId(localKeyId).getPublicKey().getKey();
|
||||
ECPublicKey remotePublic = records.getRemoteKeyRecord().getKeyForId(remoteKeyId).getKey();
|
||||
|
||||
return localPublic.compareTo(remotePublic) < 0;
|
||||
}
|
||||
|
||||
private KeyRecords getKeyRecords(Context context, MasterSecret masterSecret,
|
||||
CanonicalRecipientAddress recipient)
|
||||
{
|
||||
LocalKeyRecord localKeyRecord = new LocalKeyRecord(context, masterSecret, recipient);
|
||||
RemoteKeyRecord remoteKeyRecord = new RemoteKeyRecord(context, recipient);
|
||||
SessionRecordV1 sessionRecord = new SessionRecordV1(context, masterSecret, recipient);
|
||||
return new KeyRecords(localKeyRecord, remoteKeyRecord, sessionRecord);
|
||||
}
|
||||
|
||||
private static class KeyRecords {
|
||||
|
||||
private final LocalKeyRecord localKeyRecord;
|
||||
private final RemoteKeyRecord remoteKeyRecord;
|
||||
private final SessionRecordV1 sessionRecord;
|
||||
|
||||
public KeyRecords(LocalKeyRecord localKeyRecord,
|
||||
RemoteKeyRecord remoteKeyRecord,
|
||||
SessionRecordV1 sessionRecord)
|
||||
{
|
||||
this.localKeyRecord = localKeyRecord;
|
||||
this.remoteKeyRecord = remoteKeyRecord;
|
||||
this.sessionRecord = sessionRecord;
|
||||
}
|
||||
|
||||
private LocalKeyRecord getLocalKeyRecord() {
|
||||
return localKeyRecord;
|
||||
}
|
||||
|
||||
private RemoteKeyRecord getRemoteKeyRecord() {
|
||||
return remoteKeyRecord;
|
||||
}
|
||||
|
||||
private SessionRecordV1 getSessionRecord() {
|
||||
return sessionRecord;
|
||||
}
|
||||
}
|
||||
|
||||
public static class SessionCipherContext {
|
||||
|
||||
private final LocalKeyRecord localKeyRecord;
|
||||
private final RemoteKeyRecord remoteKeyRecord;
|
||||
private final SessionRecordV1 sessionRecord;
|
||||
private final SessionKey sessionKey;
|
||||
private final int senderKeyId;
|
||||
private final int recipientKeyId;
|
||||
private final PublicKey nextKey;
|
||||
private final int counter;
|
||||
private final int messageVersion;
|
||||
|
||||
public SessionCipherContext(KeyRecords records,
|
||||
SessionKey sessionKey,
|
||||
int senderKeyId,
|
||||
int receiverKeyId,
|
||||
PublicKey nextKey,
|
||||
int counter,
|
||||
int messageVersion)
|
||||
{
|
||||
this.localKeyRecord = records.getLocalKeyRecord();
|
||||
this.remoteKeyRecord = records.getRemoteKeyRecord();
|
||||
this.sessionRecord = records.getSessionRecord();
|
||||
this.sessionKey = sessionKey;
|
||||
this.senderKeyId = senderKeyId;
|
||||
this.recipientKeyId = receiverKeyId;
|
||||
this.nextKey = nextKey;
|
||||
this.counter = counter;
|
||||
this.messageVersion = messageVersion;
|
||||
}
|
||||
|
||||
public LocalKeyRecord getLocalKeyRecord() {
|
||||
return localKeyRecord;
|
||||
}
|
||||
|
||||
public RemoteKeyRecord getRemoteKeyRecord() {
|
||||
return remoteKeyRecord;
|
||||
}
|
||||
|
||||
public SessionRecordV1 getSessionRecord() {
|
||||
return sessionRecord;
|
||||
}
|
||||
|
||||
public SessionKey getSessionKey() {
|
||||
return sessionKey;
|
||||
}
|
||||
|
||||
public PublicKey getNextKey() {
|
||||
return nextKey;
|
||||
}
|
||||
|
||||
public int getCounter() {
|
||||
return counter;
|
||||
}
|
||||
|
||||
public int getSenderKeyId() {
|
||||
return senderKeyId;
|
||||
}
|
||||
|
||||
public int getRecipientKeyId() {
|
||||
return recipientKeyId;
|
||||
}
|
||||
|
||||
public int getMessageVersion() {
|
||||
return messageVersion;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,201 @@
|
||||
package org.whispersystems.textsecure.crypto;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.ecc.Curve;
|
||||
import org.whispersystems.textsecure.crypto.ecc.ECKeyPair;
|
||||
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
|
||||
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
|
||||
import org.whispersystems.textsecure.crypto.protocol.PreKeyWhisperMessage;
|
||||
import org.whispersystems.textsecure.crypto.protocol.WhisperMessageV2;
|
||||
import org.whispersystems.textsecure.crypto.ratchet.ChainKey;
|
||||
import org.whispersystems.textsecure.crypto.ratchet.MessageKeys;
|
||||
import org.whispersystems.textsecure.crypto.ratchet.RootKey;
|
||||
import org.whispersystems.textsecure.storage.CanonicalRecipientAddress;
|
||||
import org.whispersystems.textsecure.storage.SessionRecordV2;
|
||||
import org.whispersystems.textsecure.util.Conversions;
|
||||
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
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;
|
||||
|
||||
public class SessionCipherV2 extends SessionCipher {
|
||||
|
||||
private final Context context;
|
||||
private final MasterSecret masterSecret;
|
||||
private final CanonicalRecipientAddress recipient;
|
||||
|
||||
public SessionCipherV2(Context context,
|
||||
MasterSecret masterSecret,
|
||||
CanonicalRecipientAddress recipient)
|
||||
{
|
||||
this.context = context;
|
||||
this.masterSecret = masterSecret;
|
||||
this.recipient = recipient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CiphertextMessage encrypt(byte[] paddedMessage) {
|
||||
synchronized (SESSION_LOCK) {
|
||||
SessionRecordV2 sessionRecord = getSessionRecord();
|
||||
ChainKey chainKey = sessionRecord.getSenderChainKey();
|
||||
MessageKeys messageKeys = chainKey.getMessageKeys();
|
||||
ECPublicKey senderEphemeral = sessionRecord.getSenderEphemeral();
|
||||
int previousCounter = sessionRecord.getPreviousCounter();
|
||||
|
||||
byte[] ciphertextBody = getCiphertext(messageKeys, paddedMessage);
|
||||
CiphertextMessage ciphertextMessage = new WhisperMessageV2(messageKeys.getMacKey(),
|
||||
senderEphemeral, chainKey.getIndex(),
|
||||
previousCounter, ciphertextBody);
|
||||
|
||||
if (sessionRecord.hasPendingPreKey()) {
|
||||
Pair<Integer, ECPublicKey> pendingPreKey = sessionRecord.getPendingPreKey();
|
||||
ciphertextMessage = new PreKeyWhisperMessage(pendingPreKey.first, pendingPreKey.second,
|
||||
sessionRecord.getLocalIdentityKey(),
|
||||
(WhisperMessageV2) ciphertextMessage);
|
||||
}
|
||||
|
||||
sessionRecord.setSenderChainKey(chainKey.getNextChainKey());
|
||||
sessionRecord.save();
|
||||
|
||||
return ciphertextMessage;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] decrypt(byte[] decodedMessage) throws InvalidMessageException {
|
||||
synchronized (SESSION_LOCK) {
|
||||
SessionRecordV2 sessionRecord = getSessionRecord();
|
||||
WhisperMessageV2 ciphertextMessage = new WhisperMessageV2(decodedMessage);
|
||||
ECPublicKey theirEphemeral = ciphertextMessage.getSenderEphemeral();
|
||||
int counter = ciphertextMessage.getCounter();
|
||||
ChainKey chainKey = getOrCreateChainKey(sessionRecord, theirEphemeral);
|
||||
MessageKeys messageKeys = getOrCreateMessageKeys(sessionRecord, theirEphemeral,
|
||||
chainKey, counter);
|
||||
|
||||
ciphertextMessage.verifyMac(messageKeys.getMacKey());
|
||||
|
||||
byte[] plaintext = getPlaintext(messageKeys, ciphertextMessage.getBody());
|
||||
|
||||
sessionRecord.clearPendingPreKey();
|
||||
sessionRecord.save();
|
||||
|
||||
return plaintext;
|
||||
}
|
||||
}
|
||||
|
||||
private ChainKey getOrCreateChainKey(SessionRecordV2 sessionRecord, ECPublicKey theirEphemeral)
|
||||
throws InvalidMessageException
|
||||
{
|
||||
try {
|
||||
if (sessionRecord.hasReceiverChain(theirEphemeral)) {
|
||||
return sessionRecord.getReceiverChainKey(theirEphemeral);
|
||||
} else {
|
||||
RootKey rootKey = sessionRecord.getRootKey();
|
||||
ECKeyPair ourEphemeral = sessionRecord.getSenderEphemeralPair();
|
||||
Pair<RootKey, ChainKey> receiverChain = rootKey.createChain(theirEphemeral, ourEphemeral);
|
||||
ECKeyPair ourNewEphemeral = Curve.generateKeyPairForType(Curve.DJB_TYPE);
|
||||
Pair<RootKey, ChainKey> senderChain = receiverChain.first.createChain(theirEphemeral, ourNewEphemeral);
|
||||
|
||||
sessionRecord.setRootKey(senderChain.first);
|
||||
sessionRecord.addReceiverChain(theirEphemeral, receiverChain.second);
|
||||
sessionRecord.setPreviousCounter(sessionRecord.getSenderChainKey().getIndex()-1);
|
||||
sessionRecord.setSenderChain(ourNewEphemeral, senderChain.second);
|
||||
|
||||
return receiverChain.second;
|
||||
}
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private MessageKeys getOrCreateMessageKeys(SessionRecordV2 sessionRecord,
|
||||
ECPublicKey theirEphemeral,
|
||||
ChainKey chainKey, int counter)
|
||||
throws InvalidMessageException
|
||||
{
|
||||
if (chainKey.getIndex() > counter) {
|
||||
if (sessionRecord.hasMessageKeys(theirEphemeral, counter)) {
|
||||
return sessionRecord.removeMessageKeys(theirEphemeral, counter);
|
||||
} else {
|
||||
throw new InvalidMessageException("Received message with old counter!");
|
||||
}
|
||||
}
|
||||
|
||||
if (chainKey.getIndex() - counter > 500) {
|
||||
throw new InvalidMessageException("Over 500 messages into the future!");
|
||||
}
|
||||
|
||||
while (chainKey.getIndex() < counter) {
|
||||
MessageKeys messageKeys = chainKey.getMessageKeys();
|
||||
sessionRecord.setMessageKeys(theirEphemeral, messageKeys);
|
||||
chainKey = chainKey.getNextChainKey();
|
||||
}
|
||||
|
||||
sessionRecord.setReceiverChainKey(theirEphemeral, chainKey.getNextChainKey());
|
||||
return chainKey.getMessageKeys();
|
||||
}
|
||||
|
||||
private byte[] getCiphertext(MessageKeys messageKeys, byte[] plaintext) {
|
||||
try {
|
||||
Cipher cipher = getCipher(Cipher.ENCRYPT_MODE,
|
||||
messageKeys.getCipherKey(),
|
||||
messageKeys.getCounter());
|
||||
|
||||
return cipher.doFinal(plaintext);
|
||||
} catch (IllegalBlockSizeException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (BadPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] getPlaintext(MessageKeys messageKeys, byte[] cipherText) {
|
||||
try {
|
||||
Cipher cipher = getCipher(Cipher.DECRYPT_MODE,
|
||||
messageKeys.getCipherKey(),
|
||||
messageKeys.getCounter());
|
||||
return cipher.doFinal(cipherText);
|
||||
} catch (IllegalBlockSizeException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (BadPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Cipher getCipher(int mode, SecretKeySpec key, int counter) {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
|
||||
|
||||
byte[] ivBytes = new byte[16];
|
||||
Conversions.intToByteArray(ivBytes, 0, counter);
|
||||
|
||||
IvParameterSpec iv = new IvParameterSpec(ivBytes);
|
||||
cipher.init(mode, key, iv);
|
||||
|
||||
return cipher;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (NoSuchPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (java.security.InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (InvalidAlgorithmParameterException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private SessionRecordV2 getSessionRecord() {
|
||||
return new SessionRecordV2(context, masterSecret, recipient);
|
||||
}
|
||||
|
||||
}
|
@@ -1,102 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2013 Open 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.whispersystems.textsecure.crypto;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.ecc.Curve;
|
||||
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
|
||||
import org.whispersystems.textsecure.crypto.kdf.DerivedSecrets;
|
||||
import org.whispersystems.textsecure.crypto.kdf.HKDF;
|
||||
import org.whispersystems.textsecure.crypto.kdf.KDF;
|
||||
import org.whispersystems.textsecure.crypto.kdf.NKDF;
|
||||
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
|
||||
import org.whispersystems.textsecure.util.Conversions;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class SharedSecretCalculator {
|
||||
|
||||
public static DerivedSecrets calculateSharedSecret(boolean isLowEnd, KeyPair localKeyPair,
|
||||
int localKeyId,
|
||||
IdentityKeyPair localIdentityKeyPair,
|
||||
ECPublicKey remoteKey,
|
||||
int remoteKeyId,
|
||||
IdentityKey remoteIdentityKey)
|
||||
throws InvalidKeyException
|
||||
{
|
||||
Log.w("SharedSecretCalculator", "Calculating shared secret with 3DHE agreement...");
|
||||
KDF kdf = new HKDF();
|
||||
List<byte[]> results = new LinkedList<byte[]>();
|
||||
|
||||
if (isSmaller(localKeyPair.getPublicKey().getKey(), remoteKey)) {
|
||||
results.add(Curve.calculateAgreement(remoteKey, localIdentityKeyPair.getPrivateKey()));
|
||||
results.add(Curve.calculateAgreement(remoteIdentityKey.getPublicKey(),
|
||||
localKeyPair.getPrivateKey()));
|
||||
} else {
|
||||
results.add(Curve.calculateAgreement(remoteIdentityKey.getPublicKey(),
|
||||
localKeyPair.getPrivateKey()));
|
||||
results.add(Curve.calculateAgreement(remoteKey, localIdentityKeyPair.getPrivateKey()));
|
||||
}
|
||||
|
||||
results.add(Curve.calculateAgreement(remoteKey, localKeyPair.getPrivateKey()));
|
||||
|
||||
return kdf.deriveSecrets(results, isLowEnd, getInfo(localKeyId, remoteKeyId));
|
||||
}
|
||||
|
||||
public static DerivedSecrets calculateSharedSecret(int messageVersion, boolean isLowEnd,
|
||||
KeyPair localKeyPair, int localKeyId,
|
||||
ECPublicKey remoteKey, int remoteKeyId)
|
||||
throws InvalidKeyException
|
||||
{
|
||||
Log.w("SharedSecretCalculator", "Calculating shared secret with standard agreement...");
|
||||
KDF kdf;
|
||||
|
||||
if (messageVersion >= CiphertextMessage.DHE3_INTRODUCED_VERSION) kdf = new HKDF();
|
||||
else kdf = new NKDF();
|
||||
|
||||
Log.w("SharedSecretCalculator", "Using kdf: " + kdf);
|
||||
|
||||
List<byte[]> results = new LinkedList<byte[]>();
|
||||
results.add(Curve.calculateAgreement(remoteKey, localKeyPair.getPrivateKey()));
|
||||
|
||||
return kdf.deriveSecrets(results, isLowEnd, getInfo(localKeyId, remoteKeyId));
|
||||
}
|
||||
|
||||
private static byte[] getInfo(int localKeyId, int remoteKeyId) {
|
||||
byte[] info = new byte[3 * 2];
|
||||
|
||||
if (localKeyId < remoteKeyId) {
|
||||
Conversions.mediumToByteArray(info, 0, localKeyId);
|
||||
Conversions.mediumToByteArray(info, 3, remoteKeyId);
|
||||
} else {
|
||||
Conversions.mediumToByteArray(info, 0, remoteKeyId);
|
||||
Conversions.mediumToByteArray(info, 3, localKeyId);
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
private static boolean isSmaller(ECPublicKey localPublic,
|
||||
ECPublicKey remotePublic)
|
||||
{
|
||||
return localPublic.compareTo(remotePublic) < 0;
|
||||
}
|
||||
|
||||
}
|
@@ -36,10 +36,10 @@ public class Curve {
|
||||
}
|
||||
|
||||
public static ECKeyPair generateKeyPairForSession(int messageVersion) {
|
||||
if (messageVersion >= CiphertextMessage.CURVE25519_INTRODUCED_VERSION) {
|
||||
return generateKeyPairForType(DJB_TYPE);
|
||||
} else {
|
||||
if (messageVersion <= CiphertextMessage.LEGACY_VERSION) {
|
||||
return generateKeyPairForType(NIST_TYPE);
|
||||
} else {
|
||||
return generateKeyPairForType(DJB_TYPE);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -27,7 +27,6 @@ public class ECKeyPair {
|
||||
this.privateKey = privateKey;
|
||||
}
|
||||
|
||||
|
||||
public ECPublicKey getPublicKey() {
|
||||
return publicKey;
|
||||
}
|
||||
|
@@ -26,50 +26,38 @@ import java.util.List;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class HKDF extends KDF {
|
||||
public class HKDF {
|
||||
|
||||
private static final int HASH_OUTPUT_SIZE = 32;
|
||||
private static final int KEY_MATERIAL_SIZE = 72;
|
||||
private static final int KEY_MATERIAL_SIZE = 64;
|
||||
|
||||
private static final int CIPHER_KEYS_OFFSET = 0;
|
||||
private static final int MAC_KEYS_OFFSET = 32;
|
||||
|
||||
@Override
|
||||
public DerivedSecrets deriveSecrets(List<byte[]> sharedSecret,
|
||||
boolean isLowEnd, byte[] info)
|
||||
{
|
||||
byte[] inputKeyMaterial = concatenateSharedSecrets(sharedSecret);
|
||||
byte[] salt = new byte[HASH_OUTPUT_SIZE];
|
||||
public DerivedSecrets deriveSecrets(byte[] inputKeyMaterial, byte[] info) {
|
||||
byte[] salt = new byte[HASH_OUTPUT_SIZE];
|
||||
return deriveSecrets(inputKeyMaterial, salt, info);
|
||||
}
|
||||
|
||||
public DerivedSecrets deriveSecrets(byte[] inputKeyMaterial, byte[] salt, byte[] info) {
|
||||
byte[] prk = extract(salt, inputKeyMaterial);
|
||||
byte[] okm = expand(prk, info, KEY_MATERIAL_SIZE);
|
||||
|
||||
SecretKeySpec cipherKey = deriveCipherKey(okm, isLowEnd);
|
||||
SecretKeySpec macKey = deriveMacKey(okm, isLowEnd);
|
||||
SecretKeySpec cipherKey = deriveCipherKey(okm);
|
||||
SecretKeySpec macKey = deriveMacKey(okm);
|
||||
|
||||
return new DerivedSecrets(cipherKey, macKey);
|
||||
}
|
||||
|
||||
private SecretKeySpec deriveCipherKey(byte[] okm, boolean isLowEnd) {
|
||||
byte[] cipherKey = new byte[16];
|
||||
|
||||
if (isLowEnd) {
|
||||
System.arraycopy(okm, CIPHER_KEYS_OFFSET + 0, cipherKey, 0, cipherKey.length);
|
||||
} else {
|
||||
System.arraycopy(okm, CIPHER_KEYS_OFFSET + 16, cipherKey, 0, cipherKey.length);
|
||||
}
|
||||
|
||||
private SecretKeySpec deriveCipherKey(byte[] okm) {
|
||||
byte[] cipherKey = new byte[32];
|
||||
System.arraycopy(okm, CIPHER_KEYS_OFFSET, cipherKey, 0, cipherKey.length);
|
||||
return new SecretKeySpec(cipherKey, "AES");
|
||||
}
|
||||
|
||||
private SecretKeySpec deriveMacKey(byte[] okm, boolean isLowEnd) {
|
||||
byte[] macKey = new byte[20];
|
||||
|
||||
if (isLowEnd) {
|
||||
System.arraycopy(okm, MAC_KEYS_OFFSET + 0, macKey, 0, macKey.length);
|
||||
} else {
|
||||
System.arraycopy(okm, MAC_KEYS_OFFSET + 20, macKey, 0, macKey.length);
|
||||
}
|
||||
|
||||
private SecretKeySpec deriveMacKey(byte[] okm) {
|
||||
byte[] macKey = new byte[32];
|
||||
System.arraycopy(okm, MAC_KEYS_OFFSET, macKey, 0, macKey.length);
|
||||
return new SecretKeySpec(macKey, "HmacSHA1");
|
||||
}
|
||||
|
||||
@@ -96,7 +84,9 @@ public class HKDF extends KDF {
|
||||
mac.init(new SecretKeySpec(prk, "HmacSHA256"));
|
||||
|
||||
mac.update(mixin);
|
||||
mac.update(info);
|
||||
if (info != null) {
|
||||
mac.update(info);
|
||||
}
|
||||
mac.update((byte)i);
|
||||
|
||||
byte[] stepResult = mac.doFinal();
|
||||
|
@@ -1,45 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2013 Open 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.whispersystems.textsecure.crypto.kdf;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class KDF {
|
||||
|
||||
public abstract DerivedSecrets deriveSecrets(List<byte[]> sharedSecret,
|
||||
boolean isLowEnd, byte[] info);
|
||||
|
||||
protected byte[] concatenateSharedSecrets(List<byte[]> sharedSecrets) {
|
||||
try {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
|
||||
for (byte[] sharedSecret : sharedSecrets) {
|
||||
baos.write(sharedSecret);
|
||||
}
|
||||
|
||||
return baos.toByteArray();
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -23,15 +23,15 @@ import org.whispersystems.textsecure.util.Conversions;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.List;
|
||||
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class NKDF extends KDF {
|
||||
public class NKDF {
|
||||
|
||||
@Override
|
||||
public DerivedSecrets deriveSecrets(List<byte[]> sharedSecret,
|
||||
boolean isLowEnd, byte[] info)
|
||||
public static final int LEGACY_CIPHER_KEY_LENGTH = 16;
|
||||
public static final int LEGACY_MAC_KEY_LENGTH = 20;
|
||||
|
||||
public DerivedSecrets deriveSecrets(byte[] sharedSecret, boolean isLowEnd)
|
||||
{
|
||||
SecretKeySpec cipherKey = deriveCipherSecret(isLowEnd, sharedSecret);
|
||||
SecretKeySpec macKey = deriveMacSecret(cipherKey);
|
||||
@@ -39,15 +39,14 @@ public class NKDF extends KDF {
|
||||
return new DerivedSecrets(cipherKey, macKey);
|
||||
}
|
||||
|
||||
private SecretKeySpec deriveCipherSecret(boolean isLowEnd, List<byte[]> sharedSecret) {
|
||||
byte[] sharedSecretBytes = concatenateSharedSecrets(sharedSecret);
|
||||
byte[] derivedBytes = deriveBytes(sharedSecretBytes, 16 * 2);
|
||||
byte[] cipherSecret = new byte[16];
|
||||
private SecretKeySpec deriveCipherSecret(boolean isLowEnd, byte[] sharedSecret) {
|
||||
byte[] derivedBytes = deriveBytes(sharedSecret, LEGACY_CIPHER_KEY_LENGTH * 2);
|
||||
byte[] cipherSecret = new byte[LEGACY_CIPHER_KEY_LENGTH];
|
||||
|
||||
if (isLowEnd) {
|
||||
System.arraycopy(derivedBytes, 16, cipherSecret, 0, 16);
|
||||
System.arraycopy(derivedBytes, LEGACY_CIPHER_KEY_LENGTH, cipherSecret, 0, LEGACY_CIPHER_KEY_LENGTH);
|
||||
} else {
|
||||
System.arraycopy(derivedBytes, 0, cipherSecret, 0, 16);
|
||||
System.arraycopy(derivedBytes, 0, cipherSecret, 0, LEGACY_CIPHER_KEY_LENGTH);
|
||||
}
|
||||
|
||||
return new SecretKeySpec(cipherSecret, "AES");
|
||||
@@ -84,6 +83,4 @@ public class NKDF extends KDF {
|
||||
|
||||
return md.digest();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@@ -1,145 +1,18 @@
|
||||
package org.whispersystems.textsecure.crypto.protocol;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.InvalidMacException;
|
||||
import org.whispersystems.textsecure.crypto.InvalidMessageException;
|
||||
import org.whispersystems.textsecure.crypto.MessageMac;
|
||||
import org.whispersystems.textsecure.crypto.PublicKey;
|
||||
import org.whispersystems.textsecure.crypto.SessionCipher;
|
||||
import org.whispersystems.textsecure.util.Conversions;
|
||||
public interface CiphertextMessage {
|
||||
|
||||
public class CiphertextMessage {
|
||||
public static final int LEGACY_VERSION = 1;
|
||||
public static final int CURRENT_VERSION = 2;
|
||||
|
||||
public static final int SUPPORTED_VERSION = 2;
|
||||
public static final int DHE3_INTRODUCED_VERSION = 2;
|
||||
public static final int CURVE25519_INTRODUCED_VERSION = 2;
|
||||
public static final int LEGACY_WHISPER_TYPE = 1;
|
||||
public static final int CURRENT_WHISPER_TYPE = 2;
|
||||
public static final int PREKEY_WHISPER_TYPE = 3;
|
||||
|
||||
static final int VERSION_LENGTH = 1;
|
||||
private static final int SENDER_KEY_ID_LENGTH = 3;
|
||||
private static final int RECEIVER_KEY_ID_LENGTH = 3;
|
||||
private static final int NEXT_KEY_LENGTH = PublicKey.KEY_SIZE;
|
||||
private static final int COUNTER_LENGTH = 3;
|
||||
private static final int HEADER_LENGTH = VERSION_LENGTH + SENDER_KEY_ID_LENGTH +
|
||||
RECEIVER_KEY_ID_LENGTH + COUNTER_LENGTH +
|
||||
NEXT_KEY_LENGTH;
|
||||
// This should be the worst case (worse than V2). So not always accurate, but good enough for padding.
|
||||
public static final int ENCRYPTED_MESSAGE_OVERHEAD = WhisperMessageV1.ENCRYPTED_MESSAGE_OVERHEAD;
|
||||
|
||||
static final int VERSION_OFFSET = 0;
|
||||
private static final int SENDER_KEY_ID_OFFSET = VERSION_OFFSET + VERSION_LENGTH;
|
||||
private static final int RECEIVER_KEY_ID_OFFSET = SENDER_KEY_ID_OFFSET + SENDER_KEY_ID_LENGTH;
|
||||
private static final int NEXT_KEY_OFFSET = RECEIVER_KEY_ID_OFFSET + RECEIVER_KEY_ID_LENGTH;
|
||||
private static final int COUNTER_OFFSET = NEXT_KEY_OFFSET + NEXT_KEY_LENGTH;
|
||||
private static final int BODY_OFFSET = COUNTER_OFFSET + COUNTER_LENGTH;
|
||||
public byte[] serialize();
|
||||
public int getType();
|
||||
|
||||
public static final int ENCRYPTED_MESSAGE_OVERHEAD = HEADER_LENGTH + MessageMac.MAC_LENGTH;
|
||||
|
||||
private final byte[] ciphertext;
|
||||
|
||||
public CiphertextMessage(SessionCipher.SessionCipherContext sessionContext, byte[] ciphertextBody) {
|
||||
this.ciphertext = new byte[HEADER_LENGTH + ciphertextBody.length + MessageMac.MAC_LENGTH];
|
||||
setVersion(sessionContext.getMessageVersion(), SUPPORTED_VERSION);
|
||||
setSenderKeyId(sessionContext.getSenderKeyId());
|
||||
setReceiverKeyId(sessionContext.getRecipientKeyId());
|
||||
setNextKeyBytes(sessionContext.getNextKey().serialize());
|
||||
setCounter(sessionContext.getCounter());
|
||||
setBody(ciphertextBody);
|
||||
setMac(MessageMac.calculateMac(ciphertext, 0, ciphertext.length - MessageMac.MAC_LENGTH,
|
||||
sessionContext.getSessionKey().getMacKey()));
|
||||
}
|
||||
|
||||
public CiphertextMessage(byte[] ciphertext) throws InvalidMessageException {
|
||||
this.ciphertext = ciphertext;
|
||||
|
||||
if (ciphertext.length < HEADER_LENGTH) {
|
||||
throw new InvalidMessageException("Not long enough for ciphertext header!");
|
||||
}
|
||||
|
||||
if (getCurrentVersion() > SUPPORTED_VERSION) {
|
||||
throw new InvalidMessageException("Unspported version: " + getCurrentVersion());
|
||||
}
|
||||
}
|
||||
|
||||
public void setVersion(int current, int supported) {
|
||||
ciphertext[VERSION_OFFSET] = Conversions.intsToByteHighAndLow(current, supported);
|
||||
}
|
||||
|
||||
public int getCurrentVersion() {
|
||||
return Conversions.highBitsToInt(ciphertext[VERSION_OFFSET]);
|
||||
}
|
||||
|
||||
public int getSupportedVersion() {
|
||||
return Conversions.lowBitsToInt(ciphertext[VERSION_OFFSET]);
|
||||
}
|
||||
|
||||
public void setSenderKeyId(int senderKeyId) {
|
||||
Conversions.mediumToByteArray(ciphertext, SENDER_KEY_ID_OFFSET, senderKeyId);
|
||||
}
|
||||
|
||||
public int getSenderKeyId() {
|
||||
return Conversions.byteArrayToMedium(ciphertext, SENDER_KEY_ID_OFFSET);
|
||||
}
|
||||
|
||||
public void setReceiverKeyId(int receiverKeyId) {
|
||||
Conversions.mediumToByteArray(ciphertext, RECEIVER_KEY_ID_OFFSET, receiverKeyId);
|
||||
}
|
||||
|
||||
public int getReceiverKeyId() {
|
||||
return Conversions.byteArrayToMedium(ciphertext, RECEIVER_KEY_ID_OFFSET);
|
||||
}
|
||||
|
||||
public void setNextKeyBytes(byte[] nextKey) {
|
||||
assert(nextKey.length == NEXT_KEY_LENGTH);
|
||||
System.arraycopy(nextKey, 0, ciphertext, NEXT_KEY_OFFSET, nextKey.length);
|
||||
}
|
||||
|
||||
public byte[] getNextKeyBytes() {
|
||||
byte[] nextKeyBytes = new byte[NEXT_KEY_LENGTH];
|
||||
System.arraycopy(ciphertext, NEXT_KEY_OFFSET, nextKeyBytes, 0, nextKeyBytes.length);
|
||||
|
||||
return nextKeyBytes;
|
||||
}
|
||||
|
||||
public void setCounter(int counter) {
|
||||
Conversions.mediumToByteArray(ciphertext, COUNTER_OFFSET, counter);
|
||||
}
|
||||
|
||||
public int getCounter() {
|
||||
return Conversions.byteArrayToMedium(ciphertext, COUNTER_OFFSET);
|
||||
}
|
||||
|
||||
public void setBody(byte[] body) {
|
||||
System.arraycopy(body, 0, ciphertext, BODY_OFFSET, body.length);
|
||||
}
|
||||
|
||||
public byte[] getBody() {
|
||||
byte[] body = new byte[ciphertext.length - HEADER_LENGTH - MessageMac.MAC_LENGTH];
|
||||
System.arraycopy(ciphertext, BODY_OFFSET, body, 0, body.length);
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
public void setMac(byte[] mac) {
|
||||
System.arraycopy(mac, 0, ciphertext, ciphertext.length-mac.length, mac.length);
|
||||
}
|
||||
|
||||
public byte[] getMac() {
|
||||
byte[] mac = new byte[MessageMac.MAC_LENGTH];
|
||||
System.arraycopy(ciphertext, ciphertext.length-mac.length, mac, 0, mac.length);
|
||||
|
||||
return mac;
|
||||
}
|
||||
|
||||
public byte[] serialize() {
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
public void verifyMac(SessionCipher.SessionCipherContext sessionContext)
|
||||
throws InvalidMessageException
|
||||
{
|
||||
try {
|
||||
MessageMac.verifyMac(this.ciphertext, 0, this.ciphertext.length - MessageMac.MAC_LENGTH,
|
||||
getMac(), sessionContext.getSessionKey().getMacKey());
|
||||
} catch (InvalidMacException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -1,108 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2013 Open 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.whispersystems.textsecure.crypto.protocol;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.IdentityKey;
|
||||
import org.whispersystems.textsecure.crypto.InvalidKeyException;
|
||||
import org.whispersystems.textsecure.crypto.InvalidMessageException;
|
||||
import org.whispersystems.textsecure.crypto.InvalidVersionException;
|
||||
import org.whispersystems.textsecure.crypto.PublicKey;
|
||||
import org.whispersystems.textsecure.util.Conversions;
|
||||
|
||||
/**
|
||||
* Class responsible for parsing and constructing PreKeyBundle messages.
|
||||
*
|
||||
* The class takes an existing encrypted message and bundles in the necessary
|
||||
* additional information for a prekeybundle, namely the addition of the local
|
||||
* identity key.
|
||||
*/
|
||||
public class PreKeyBundleMessage {
|
||||
|
||||
public static final int SUPPORTED_VERSION = CiphertextMessage.SUPPORTED_VERSION;
|
||||
|
||||
private static final int VERSION_LENGTH = CiphertextMessage.VERSION_LENGTH;
|
||||
private static final int IDENTITY_KEY_LENGTH = IdentityKey.SIZE;
|
||||
|
||||
private static final int VERSION_OFFSET = CiphertextMessage.VERSION_OFFSET;
|
||||
private static final int IDENTITY_KEY_OFFSET = VERSION_OFFSET + VERSION_LENGTH;
|
||||
|
||||
private final byte[] messageBytes;
|
||||
private final CiphertextMessage bundledMessage;
|
||||
private final IdentityKey identityKey;
|
||||
|
||||
public PreKeyBundleMessage(byte[] messageBytes)
|
||||
throws InvalidVersionException, InvalidKeyException
|
||||
{
|
||||
try {
|
||||
this.messageBytes = messageBytes;
|
||||
int messageVersion = Conversions.highBitsToInt(this.messageBytes[VERSION_OFFSET]);
|
||||
|
||||
if (messageVersion > CiphertextMessage.SUPPORTED_VERSION)
|
||||
throw new InvalidVersionException("Key exchange with version: " + messageVersion);
|
||||
|
||||
this.identityKey = new IdentityKey(messageBytes, IDENTITY_KEY_OFFSET);
|
||||
byte[] bundledMessageBytes = new byte[messageBytes.length - IDENTITY_KEY_LENGTH];
|
||||
|
||||
bundledMessageBytes[VERSION_OFFSET] = this.messageBytes[VERSION_OFFSET];
|
||||
System.arraycopy(messageBytes, IDENTITY_KEY_OFFSET+IDENTITY_KEY_LENGTH, bundledMessageBytes,
|
||||
VERSION_OFFSET+VERSION_LENGTH, bundledMessageBytes.length-VERSION_LENGTH);
|
||||
|
||||
this.bundledMessage = new CiphertextMessage(bundledMessageBytes);
|
||||
} catch (InvalidMessageException e) {
|
||||
throw new InvalidKeyException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public PreKeyBundleMessage(CiphertextMessage bundledMessage, IdentityKey identityKey) {
|
||||
this.bundledMessage = bundledMessage;
|
||||
this.identityKey = identityKey;
|
||||
this.messageBytes = new byte[IDENTITY_KEY_LENGTH + bundledMessage.serialize().length];
|
||||
|
||||
byte[] bundledMessageBytes = bundledMessage.serialize();
|
||||
byte[] identityKeyBytes = identityKey.serialize();
|
||||
|
||||
messageBytes[VERSION_OFFSET] = bundledMessageBytes[VERSION_OFFSET];
|
||||
System.arraycopy(identityKeyBytes, 0, messageBytes, IDENTITY_KEY_OFFSET, identityKeyBytes.length);
|
||||
System.arraycopy(bundledMessageBytes, VERSION_OFFSET+VERSION_LENGTH,
|
||||
messageBytes, IDENTITY_KEY_OFFSET+IDENTITY_KEY_LENGTH,
|
||||
bundledMessageBytes.length-VERSION_LENGTH);
|
||||
}
|
||||
|
||||
public byte[] serialize() {
|
||||
return this.messageBytes;
|
||||
}
|
||||
|
||||
public int getSupportedVersion() {
|
||||
return bundledMessage.getSupportedVersion();
|
||||
}
|
||||
|
||||
public IdentityKey getIdentityKey() {
|
||||
return identityKey;
|
||||
}
|
||||
|
||||
public PublicKey getPublicKey() throws InvalidKeyException {
|
||||
return new PublicKey(bundledMessage.getNextKeyBytes());
|
||||
}
|
||||
|
||||
public CiphertextMessage getBundledMessage() {
|
||||
return bundledMessage;
|
||||
}
|
||||
|
||||
public int getPreKeyId() {
|
||||
return bundledMessage.getReceiverKeyId();
|
||||
}
|
||||
}
|
@@ -0,0 +1,104 @@
|
||||
package org.whispersystems.textsecure.crypto.protocol;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.IdentityKey;
|
||||
import org.whispersystems.textsecure.crypto.InvalidKeyException;
|
||||
import org.whispersystems.textsecure.crypto.InvalidMessageException;
|
||||
import org.whispersystems.textsecure.crypto.InvalidVersionException;
|
||||
import org.whispersystems.textsecure.crypto.ecc.Curve;
|
||||
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
|
||||
import org.whispersystems.textsecure.util.Conversions;
|
||||
import org.whispersystems.textsecure.util.Util;
|
||||
|
||||
public class PreKeyWhisperMessage implements CiphertextMessage {
|
||||
|
||||
private final int version;
|
||||
private final int preKeyId;
|
||||
private final ECPublicKey baseKey;
|
||||
private final IdentityKey identityKey;
|
||||
private final WhisperMessageV2 message;
|
||||
private final byte[] serialized;
|
||||
|
||||
public PreKeyWhisperMessage(byte[] serialized)
|
||||
throws InvalidMessageException, InvalidVersionException
|
||||
{
|
||||
try {
|
||||
this.version = Conversions.lowBitsToInt(serialized[0]);
|
||||
|
||||
if (this.version > CiphertextMessage.CURRENT_VERSION) {
|
||||
throw new InvalidVersionException("Unknown version: " + this.version);
|
||||
}
|
||||
|
||||
WhisperProtos.PreKeyWhisperMessage preKeyWhisperMessage
|
||||
= WhisperProtos.PreKeyWhisperMessage.parseFrom(ByteString.copyFrom(serialized, 1,
|
||||
serialized.length-1));
|
||||
|
||||
if (!preKeyWhisperMessage.hasPreKeyId() ||
|
||||
!preKeyWhisperMessage.hasBaseKey() ||
|
||||
!preKeyWhisperMessage.hasIdentityKey() ||
|
||||
!preKeyWhisperMessage.hasMessage())
|
||||
{
|
||||
throw new InvalidMessageException("Incomplete message.");
|
||||
}
|
||||
|
||||
this.serialized = serialized;
|
||||
this.preKeyId = preKeyWhisperMessage.getPreKeyId();
|
||||
this.baseKey = Curve.decodePoint(preKeyWhisperMessage.getBaseKey().toByteArray(), 0);
|
||||
this.identityKey = new IdentityKey(Curve.decodePoint(preKeyWhisperMessage.getIdentityKey().toByteArray(), 0));
|
||||
this.message = new WhisperMessageV2(preKeyWhisperMessage.getMessage().toByteArray());
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public PreKeyWhisperMessage(int preKeyId, ECPublicKey baseKey, IdentityKey identityKey,
|
||||
WhisperMessageV2 message)
|
||||
{
|
||||
this.version = CiphertextMessage.CURRENT_VERSION;
|
||||
this.preKeyId = preKeyId;
|
||||
this.baseKey = baseKey;
|
||||
this.identityKey = identityKey;
|
||||
this.message = message;
|
||||
|
||||
byte[] versionBytes = {Conversions.intsToByteHighAndLow(CURRENT_VERSION, this.version)};
|
||||
byte[] messageBytes = WhisperProtos.PreKeyWhisperMessage.newBuilder()
|
||||
.setPreKeyId(preKeyId)
|
||||
.setBaseKey(ByteString.copyFrom(baseKey.serialize()))
|
||||
.setIdentityKey(ByteString.copyFrom(identityKey.serialize()))
|
||||
.setMessage(ByteString.copyFrom(message.serialize()))
|
||||
.build().toByteArray();
|
||||
|
||||
this.serialized = Util.combine(versionBytes, messageBytes);
|
||||
}
|
||||
|
||||
public IdentityKey getIdentityKey() {
|
||||
return identityKey;
|
||||
}
|
||||
|
||||
public int getPreKeyId() {
|
||||
return preKeyId;
|
||||
}
|
||||
|
||||
public ECPublicKey getBaseKey() {
|
||||
return baseKey;
|
||||
}
|
||||
|
||||
public WhisperMessageV2 getWhisperMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] serialize() {
|
||||
return serialized;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return CiphertextMessage.PREKEY_WHISPER_TYPE;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,187 @@
|
||||
package org.whispersystems.textsecure.crypto.protocol;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.InvalidMessageException;
|
||||
import org.whispersystems.textsecure.crypto.PublicKey;
|
||||
import org.whispersystems.textsecure.crypto.SessionCipherV1;
|
||||
import org.whispersystems.textsecure.util.Conversions;
|
||||
import org.whispersystems.textsecure.util.Hex;
|
||||
import org.whispersystems.textsecure.util.Util;
|
||||
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
|
||||
public class WhisperMessageV1 implements CiphertextMessage{
|
||||
|
||||
private static final int VERSION_LENGTH = 1;
|
||||
private static final int SENDER_KEY_ID_LENGTH = 3;
|
||||
private static final int RECEIVER_KEY_ID_LENGTH = 3;
|
||||
private static final int NEXT_KEY_LENGTH = PublicKey.KEY_SIZE;
|
||||
private static final int COUNTER_LENGTH = 3;
|
||||
private static final int HEADER_LENGTH = VERSION_LENGTH + SENDER_KEY_ID_LENGTH +
|
||||
RECEIVER_KEY_ID_LENGTH + COUNTER_LENGTH +
|
||||
NEXT_KEY_LENGTH;
|
||||
private static final int MAC_LENGTH = 10;
|
||||
|
||||
|
||||
private static final int VERSION_OFFSET = 0;
|
||||
private static final int SENDER_KEY_ID_OFFSET = VERSION_OFFSET + VERSION_LENGTH;
|
||||
private static final int RECEIVER_KEY_ID_OFFSET = SENDER_KEY_ID_OFFSET + SENDER_KEY_ID_LENGTH;
|
||||
private static final int NEXT_KEY_OFFSET = RECEIVER_KEY_ID_OFFSET + RECEIVER_KEY_ID_LENGTH;
|
||||
private static final int COUNTER_OFFSET = NEXT_KEY_OFFSET + NEXT_KEY_LENGTH;
|
||||
private static final int BODY_OFFSET = COUNTER_OFFSET + COUNTER_LENGTH;
|
||||
|
||||
static final int ENCRYPTED_MESSAGE_OVERHEAD = HEADER_LENGTH + MAC_LENGTH;
|
||||
|
||||
private final byte[] ciphertext;
|
||||
|
||||
public WhisperMessageV1(SessionCipherV1.SessionCipherContext sessionContext,
|
||||
byte[] ciphertextBody)
|
||||
{
|
||||
this.ciphertext = new byte[HEADER_LENGTH + ciphertextBody.length + MAC_LENGTH];
|
||||
setVersion(sessionContext.getMessageVersion(), CURRENT_VERSION);
|
||||
setSenderKeyId(sessionContext.getSenderKeyId());
|
||||
setReceiverKeyId(sessionContext.getRecipientKeyId());
|
||||
setNextKeyBytes(sessionContext.getNextKey().serialize());
|
||||
setCounter(sessionContext.getCounter());
|
||||
setBody(ciphertextBody);
|
||||
setMac(calculateMac(sessionContext.getSessionKey().getMacKey(),
|
||||
ciphertext, 0, ciphertext.length - MAC_LENGTH));
|
||||
}
|
||||
|
||||
public WhisperMessageV1(byte[] ciphertext) throws InvalidMessageException {
|
||||
this.ciphertext = ciphertext;
|
||||
|
||||
if (ciphertext.length < HEADER_LENGTH) {
|
||||
throw new InvalidMessageException("Not long enough for ciphertext header!");
|
||||
}
|
||||
|
||||
if (getCurrentVersion() > LEGACY_VERSION) {
|
||||
throw new InvalidMessageException("Received non-legacy version: " + getCurrentVersion());
|
||||
}
|
||||
}
|
||||
|
||||
public void setVersion(int current, int supported) {
|
||||
ciphertext[VERSION_OFFSET] = Conversions.intsToByteHighAndLow(current, supported);
|
||||
}
|
||||
|
||||
public int getCurrentVersion() {
|
||||
return Conversions.highBitsToInt(ciphertext[VERSION_OFFSET]);
|
||||
}
|
||||
|
||||
public int getSupportedVersion() {
|
||||
return Conversions.lowBitsToInt(ciphertext[VERSION_OFFSET]);
|
||||
}
|
||||
|
||||
public void setSenderKeyId(int senderKeyId) {
|
||||
Conversions.mediumToByteArray(ciphertext, SENDER_KEY_ID_OFFSET, senderKeyId);
|
||||
}
|
||||
|
||||
public int getSenderKeyId() {
|
||||
return Conversions.byteArrayToMedium(ciphertext, SENDER_KEY_ID_OFFSET);
|
||||
}
|
||||
|
||||
public void setReceiverKeyId(int receiverKeyId) {
|
||||
Conversions.mediumToByteArray(ciphertext, RECEIVER_KEY_ID_OFFSET, receiverKeyId);
|
||||
}
|
||||
|
||||
public int getReceiverKeyId() {
|
||||
return Conversions.byteArrayToMedium(ciphertext, RECEIVER_KEY_ID_OFFSET);
|
||||
}
|
||||
|
||||
public void setNextKeyBytes(byte[] nextKey) {
|
||||
assert(nextKey.length == NEXT_KEY_LENGTH);
|
||||
System.arraycopy(nextKey, 0, ciphertext, NEXT_KEY_OFFSET, nextKey.length);
|
||||
}
|
||||
|
||||
public byte[] getNextKeyBytes() {
|
||||
byte[] nextKeyBytes = new byte[NEXT_KEY_LENGTH];
|
||||
System.arraycopy(ciphertext, NEXT_KEY_OFFSET, nextKeyBytes, 0, nextKeyBytes.length);
|
||||
|
||||
return nextKeyBytes;
|
||||
}
|
||||
|
||||
public void setCounter(int counter) {
|
||||
Conversions.mediumToByteArray(ciphertext, COUNTER_OFFSET, counter);
|
||||
}
|
||||
|
||||
public int getCounter() {
|
||||
return Conversions.byteArrayToMedium(ciphertext, COUNTER_OFFSET);
|
||||
}
|
||||
|
||||
public void setBody(byte[] body) {
|
||||
System.arraycopy(body, 0, ciphertext, BODY_OFFSET, body.length);
|
||||
}
|
||||
|
||||
public byte[] getBody() {
|
||||
byte[] body = new byte[ciphertext.length - HEADER_LENGTH - MAC_LENGTH];
|
||||
System.arraycopy(ciphertext, BODY_OFFSET, body, 0, body.length);
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
public void setMac(byte[] mac) {
|
||||
System.arraycopy(mac, 0, ciphertext, ciphertext.length-mac.length, mac.length);
|
||||
}
|
||||
|
||||
public byte[] getMac() {
|
||||
byte[] mac = new byte[MAC_LENGTH];
|
||||
System.arraycopy(ciphertext, ciphertext.length-mac.length, mac, 0, mac.length);
|
||||
|
||||
return mac;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] serialize() {
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return CiphertextMessage.LEGACY_WHISPER_TYPE;
|
||||
}
|
||||
|
||||
public void verifyMac(SessionCipherV1.SessionCipherContext sessionContext)
|
||||
throws InvalidMessageException
|
||||
{
|
||||
verifyMac(sessionContext.getSessionKey().getMacKey(),
|
||||
this.ciphertext, 0, this.ciphertext.length - MAC_LENGTH, getMac());
|
||||
}
|
||||
|
||||
private byte[] calculateMac(SecretKeySpec macKey, byte[] message, int offset, int length) {
|
||||
try {
|
||||
Mac mac = Mac.getInstance("HmacSHA1");
|
||||
mac.init(macKey);
|
||||
|
||||
mac.update(message, offset, length);
|
||||
byte[] macBytes = mac.doFinal();
|
||||
|
||||
return Util.trim(macBytes, MAC_LENGTH);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyMac(SecretKeySpec macKey, byte[] message, int offset, int length,
|
||||
byte[] receivedMac)
|
||||
throws InvalidMessageException
|
||||
{
|
||||
byte[] localMac = calculateMac(macKey, message, offset, length);
|
||||
|
||||
Log.w("WhisperMessageV1", "Local Mac: " + Hex.toString(localMac));
|
||||
Log.w("WhisperMessageV1", "Remot Mac: " + Hex.toString(receivedMac));
|
||||
|
||||
if (!Arrays.equals(localMac, receivedMac)) {
|
||||
throw new InvalidMessageException("MAC on message does not match calculated MAC.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,132 @@
|
||||
package org.whispersystems.textsecure.crypto.protocol;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.InvalidKeyException;
|
||||
import org.whispersystems.textsecure.crypto.InvalidMessageException;
|
||||
import org.whispersystems.textsecure.crypto.ecc.Curve;
|
||||
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
|
||||
import org.whispersystems.textsecure.crypto.protocol.WhisperProtos.WhisperMessage;
|
||||
import org.whispersystems.textsecure.util.Conversions;
|
||||
import org.whispersystems.textsecure.util.Hex;
|
||||
import org.whispersystems.textsecure.util.Util;
|
||||
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class WhisperMessageV2 implements CiphertextMessage {
|
||||
|
||||
private static final int MAC_LENGTH = 8;
|
||||
|
||||
private final ECPublicKey senderEphemeral;
|
||||
private final int counter;
|
||||
private final int previousCounter;
|
||||
private final byte[] ciphertext;
|
||||
private final byte[] serialized;
|
||||
|
||||
public WhisperMessageV2(byte[] serialized) throws InvalidMessageException {
|
||||
try {
|
||||
byte[][] messageParts = Util.split(serialized, 1, serialized.length - 1 - MAC_LENGTH, MAC_LENGTH);
|
||||
byte version = messageParts[0][0];
|
||||
byte[] message = messageParts[1];
|
||||
byte[] mac = messageParts[2];
|
||||
|
||||
if (Conversions.highBitsToInt(version) != CURRENT_VERSION) {
|
||||
throw new InvalidMessageException("Unknown version: " + Conversions.lowBitsToInt(version));
|
||||
}
|
||||
|
||||
WhisperMessage whisperMessage = WhisperMessage.parseFrom(message);
|
||||
|
||||
if (!whisperMessage.hasCiphertext() ||
|
||||
!whisperMessage.hasCounter() ||
|
||||
!whisperMessage.hasEphemeralKey())
|
||||
{
|
||||
throw new InvalidMessageException("Incomplete message.");
|
||||
}
|
||||
|
||||
this.serialized = serialized;
|
||||
this.senderEphemeral = Curve.decodePoint(whisperMessage.getEphemeralKey().toByteArray(), 0);
|
||||
this.counter = whisperMessage.getCounter();
|
||||
this.previousCounter = whisperMessage.getPreviousCounter();
|
||||
this.ciphertext = whisperMessage.getCiphertext().toByteArray();
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public WhisperMessageV2(SecretKeySpec macKey, ECPublicKey senderEphemeral,
|
||||
int counter, int previousCounter, byte[] ciphertext)
|
||||
{
|
||||
byte[] version = {Conversions.intsToByteHighAndLow(CURRENT_VERSION, CURRENT_VERSION)};
|
||||
byte[] message = WhisperMessage.newBuilder()
|
||||
.setEphemeralKey(ByteString.copyFrom(senderEphemeral.serialize()))
|
||||
.setCounter(counter)
|
||||
.setPreviousCounter(previousCounter)
|
||||
.setCiphertext(ByteString.copyFrom(ciphertext))
|
||||
.build().toByteArray();
|
||||
byte[] mac = getMac(macKey, Util.combine(version, message));
|
||||
|
||||
this.serialized = Util.combine(version, message, mac);
|
||||
this.senderEphemeral = senderEphemeral;
|
||||
this.counter = counter;
|
||||
this.previousCounter = previousCounter;
|
||||
this.ciphertext = ciphertext;
|
||||
}
|
||||
|
||||
public ECPublicKey getSenderEphemeral() {
|
||||
return senderEphemeral;
|
||||
}
|
||||
|
||||
public int getCounter() {
|
||||
return counter;
|
||||
}
|
||||
|
||||
public byte[] getBody() {
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
public void verifyMac(SecretKeySpec macKey)
|
||||
throws InvalidMessageException
|
||||
{
|
||||
byte[][] parts = Util.split(serialized, serialized.length - MAC_LENGTH, MAC_LENGTH);
|
||||
byte[] ourMac = getMac(macKey, parts[0]);
|
||||
byte[] theirMac = parts[1];
|
||||
|
||||
if (!Arrays.equals(ourMac, theirMac)) {
|
||||
throw new InvalidMessageException("Bad Mac!");
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] getMac(SecretKeySpec macKey, byte[] serialized) {
|
||||
try {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(macKey);
|
||||
|
||||
byte[] fullMac = mac.doFinal(serialized);
|
||||
return Util.trim(fullMac, MAC_LENGTH);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (java.security.InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] serialize() {
|
||||
return serialized;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return CiphertextMessage.CURRENT_WHISPER_TYPE;
|
||||
}
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,58 @@
|
||||
package org.whispersystems.textsecure.crypto.ratchet;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.kdf.DerivedSecrets;
|
||||
import org.whispersystems.textsecure.crypto.kdf.HKDF;
|
||||
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class ChainKey {
|
||||
|
||||
private static final byte[] MESSAGE_KEY_SEED = {0x01};
|
||||
private static final byte[] CHAIN_KEY_SEED = {0x02};
|
||||
|
||||
private final byte[] key;
|
||||
private final int index;
|
||||
|
||||
public ChainKey(byte[] key, int index) {
|
||||
this.key = key;
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
public byte[] getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public int getIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
public ChainKey getNextChainKey() {
|
||||
byte[] nextKey = getBaseMaterial(CHAIN_KEY_SEED);
|
||||
return new ChainKey(nextKey, index + 1);
|
||||
}
|
||||
|
||||
public MessageKeys getMessageKeys() {
|
||||
HKDF kdf = new HKDF();
|
||||
byte[] inputKeyMaterial = getBaseMaterial(MESSAGE_KEY_SEED);
|
||||
DerivedSecrets keyMaterial = kdf.deriveSecrets(inputKeyMaterial, null);
|
||||
|
||||
return new MessageKeys(keyMaterial.getCipherKey(), keyMaterial.getMacKey(), index);
|
||||
}
|
||||
|
||||
private byte[] getBaseMaterial(byte[] seed) {
|
||||
try {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(key, "HmacSHA256"));
|
||||
|
||||
return mac.doFinal(seed);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,28 @@
|
||||
package org.whispersystems.textsecure.crypto.ratchet;
|
||||
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class MessageKeys {
|
||||
|
||||
private final SecretKeySpec cipherKey;
|
||||
private final SecretKeySpec macKey;
|
||||
private final int counter;
|
||||
|
||||
public MessageKeys(SecretKeySpec cipherKey, SecretKeySpec macKey, int counter) {
|
||||
this.cipherKey = cipherKey;
|
||||
this.macKey = macKey;
|
||||
this.counter = counter;
|
||||
}
|
||||
|
||||
public SecretKeySpec getCipherKey() {
|
||||
return cipherKey;
|
||||
}
|
||||
|
||||
public SecretKeySpec getMacKey() {
|
||||
return macKey;
|
||||
}
|
||||
|
||||
public int getCounter() {
|
||||
return counter;
|
||||
}
|
||||
}
|
@@ -0,0 +1,120 @@
|
||||
package org.whispersystems.textsecure.crypto.ratchet;
|
||||
|
||||
import android.util.Pair;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.IdentityKey;
|
||||
import org.whispersystems.textsecure.crypto.IdentityKeyPair;
|
||||
import org.whispersystems.textsecure.crypto.InvalidKeyException;
|
||||
import org.whispersystems.textsecure.crypto.ecc.Curve;
|
||||
import org.whispersystems.textsecure.crypto.ecc.ECKeyPair;
|
||||
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
|
||||
import org.whispersystems.textsecure.crypto.kdf.DerivedSecrets;
|
||||
import org.whispersystems.textsecure.crypto.kdf.HKDF;
|
||||
import org.whispersystems.textsecure.storage.SessionRecordV2;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
public class RatchetingSession {
|
||||
|
||||
public static void initializeSession(SessionRecordV2 sessionRecord,
|
||||
ECKeyPair ourBaseKey,
|
||||
ECPublicKey theirBaseKey,
|
||||
ECKeyPair ourEphemeralKey,
|
||||
ECPublicKey theirEphemeralKey,
|
||||
IdentityKeyPair ourIdentityKey,
|
||||
IdentityKey theirIdentityKey)
|
||||
throws InvalidKeyException
|
||||
{
|
||||
if (isAlice(ourBaseKey.getPublicKey(), theirBaseKey, ourEphemeralKey.getPublicKey(), theirEphemeralKey)) {
|
||||
initializeSessionAsAlice(sessionRecord, ourBaseKey, theirBaseKey, theirEphemeralKey,
|
||||
ourIdentityKey, theirIdentityKey);
|
||||
} else {
|
||||
initializeSessionAsBob(sessionRecord, ourBaseKey, theirBaseKey,
|
||||
ourEphemeralKey, ourIdentityKey, theirIdentityKey);
|
||||
}
|
||||
}
|
||||
|
||||
private static void initializeSessionAsAlice(SessionRecordV2 sessionRecord,
|
||||
ECKeyPair ourBaseKey, ECPublicKey theirBaseKey,
|
||||
ECPublicKey theirEphemeralKey,
|
||||
IdentityKeyPair ourIdentityKey,
|
||||
IdentityKey theirIdentityKey)
|
||||
throws InvalidKeyException
|
||||
{
|
||||
sessionRecord.setRemoteIdentityKey(theirIdentityKey);
|
||||
sessionRecord.setLocalIdentityKey(ourIdentityKey.getPublicKey());
|
||||
|
||||
ECKeyPair sendingKey = Curve.generateKeyPairForType(ourIdentityKey.getPublicKey().getPublicKey().getType());
|
||||
Pair<RootKey, ChainKey> receivingChain = calculate3DHE(ourBaseKey, theirBaseKey, ourIdentityKey, theirIdentityKey);
|
||||
Pair<RootKey, ChainKey> sendingChain = receivingChain.first.createChain(theirEphemeralKey, sendingKey);
|
||||
|
||||
sessionRecord.addReceiverChain(theirEphemeralKey, receivingChain.second);
|
||||
sessionRecord.setSenderChain(sendingKey, sendingChain.second);
|
||||
sessionRecord.setRootKey(sendingChain.first);
|
||||
}
|
||||
|
||||
private static void initializeSessionAsBob(SessionRecordV2 sessionRecord,
|
||||
ECKeyPair ourBaseKey, ECPublicKey theirBaseKey,
|
||||
ECKeyPair ourEphemeralKey,
|
||||
IdentityKeyPair ourIdentityKey,
|
||||
IdentityKey theirIdentityKey)
|
||||
throws InvalidKeyException
|
||||
{
|
||||
sessionRecord.setRemoteIdentityKey(theirIdentityKey);
|
||||
sessionRecord.setLocalIdentityKey(ourIdentityKey.getPublicKey());
|
||||
|
||||
Pair<RootKey, ChainKey> sendingChain = calculate3DHE(ourBaseKey, theirBaseKey,
|
||||
ourIdentityKey, theirIdentityKey);
|
||||
|
||||
sessionRecord.setSenderChain(ourEphemeralKey, sendingChain.second);
|
||||
sessionRecord.setRootKey(sendingChain.first);
|
||||
}
|
||||
|
||||
public static Pair<RootKey, ChainKey> calculate3DHE(ECKeyPair ourEphemeral, ECPublicKey theirEphemeral,
|
||||
IdentityKeyPair ourIdentity, IdentityKey theirIdentity)
|
||||
throws InvalidKeyException
|
||||
{
|
||||
try {
|
||||
ByteArrayOutputStream secrets = new ByteArrayOutputStream();
|
||||
|
||||
if (isLowEnd(ourEphemeral.getPublicKey(), theirEphemeral)) {
|
||||
secrets.write(Curve.calculateAgreement(theirEphemeral, ourIdentity.getPrivateKey()));
|
||||
secrets.write(Curve.calculateAgreement(theirIdentity.getPublicKey(), ourEphemeral.getPrivateKey()));
|
||||
} else {
|
||||
secrets.write(Curve.calculateAgreement(theirIdentity.getPublicKey(), ourEphemeral.getPrivateKey()));
|
||||
secrets.write(Curve.calculateAgreement(theirEphemeral, ourIdentity.getPrivateKey()));
|
||||
}
|
||||
|
||||
secrets.write(Curve.calculateAgreement(theirEphemeral, ourEphemeral.getPrivateKey()));
|
||||
|
||||
DerivedSecrets derivedSecrets = new HKDF().deriveSecrets(secrets.toByteArray(),
|
||||
"WhisperText".getBytes());
|
||||
|
||||
return new Pair<RootKey, ChainKey>(new RootKey(derivedSecrets.getCipherKey().getEncoded()),
|
||||
new ChainKey(derivedSecrets.getMacKey().getEncoded(), 0));
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isAlice(ECPublicKey ourBaseKey, ECPublicKey theirBaseKey,
|
||||
ECPublicKey ourEphemeralKey, ECPublicKey theirEphemeralKey)
|
||||
{
|
||||
if (ourEphemeralKey.equals(ourBaseKey)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (theirEphemeralKey.equals(theirBaseKey)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return isLowEnd(ourBaseKey, theirBaseKey);
|
||||
}
|
||||
|
||||
private static boolean isLowEnd(ECPublicKey ourKey, ECPublicKey theirKey) {
|
||||
return ourKey.compareTo(theirKey) < 0;
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -0,0 +1,38 @@
|
||||
package org.whispersystems.textsecure.crypto.ratchet;
|
||||
|
||||
import android.util.Pair;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.InvalidKeyException;
|
||||
import org.whispersystems.textsecure.crypto.ecc.Curve;
|
||||
import org.whispersystems.textsecure.crypto.ecc.ECKeyPair;
|
||||
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
|
||||
import org.whispersystems.textsecure.crypto.kdf.DerivedSecrets;
|
||||
import org.whispersystems.textsecure.crypto.kdf.HKDF;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
public class RootKey {
|
||||
|
||||
private final byte[] key;
|
||||
|
||||
public RootKey(byte[] key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public byte[] getKeyBytes() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public Pair<RootKey, ChainKey> createChain(ECPublicKey theirEphemeral, ECKeyPair ourEphemeral)
|
||||
throws InvalidKeyException
|
||||
{
|
||||
HKDF kdf = new HKDF();
|
||||
byte[] sharedSecret = Curve.calculateAgreement(theirEphemeral, ourEphemeral.getPrivateKey());
|
||||
DerivedSecrets keys = kdf.deriveSecrets(sharedSecret, key, "WhisperRatchet".getBytes());
|
||||
RootKey newRootKey = new RootKey(keys.getMacKey().getEncoded());
|
||||
ChainKey newChainKey = new ChainKey(keys.getCipherKey().getEncoded(), 0);
|
||||
|
||||
return new Pair<RootKey, ChainKey>(newRootKey, newChainKey);
|
||||
}
|
||||
}
|
@@ -30,8 +30,9 @@ import java.nio.channels.FileChannel;
|
||||
|
||||
public abstract class Record {
|
||||
|
||||
protected static final String SESSIONS_DIRECTORY = "sessions";
|
||||
public static final String PREKEY_DIRECTORY = "prekeys";
|
||||
protected static final String SESSIONS_DIRECTORY = "sessions";
|
||||
protected static final String SESSIONS_DIRECTORY_V2 = "sessions-v2";
|
||||
public static final String PREKEY_DIRECTORY = "prekeys";
|
||||
|
||||
protected final String address;
|
||||
protected final String directory;
|
||||
|
@@ -0,0 +1,93 @@
|
||||
package org.whispersystems.textsecure.storage;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.IdentityKey;
|
||||
import org.whispersystems.textsecure.crypto.MasterSecret;
|
||||
|
||||
/**
|
||||
* Helper class for generating key pairs and calculating ECDH agreements.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class Session {
|
||||
|
||||
public static void clearV1SessionFor(Context context, CanonicalRecipientAddress recipient) {
|
||||
//XXX Obviously we should probably do something more thorough here eventually.
|
||||
LocalKeyRecord.delete(context, recipient);
|
||||
RemoteKeyRecord.delete(context, recipient);
|
||||
SessionRecordV1.delete(context, recipient);
|
||||
}
|
||||
|
||||
public static void abortSessionFor(Context context, CanonicalRecipientAddress recipient) {
|
||||
Log.w("Session", "Aborting session, deleting keys...");
|
||||
clearV1SessionFor(context, recipient);
|
||||
SessionRecordV2.delete(context, recipient);
|
||||
}
|
||||
|
||||
public static boolean hasSession(Context context, MasterSecret masterSecret,
|
||||
CanonicalRecipientAddress recipient)
|
||||
{
|
||||
Log.w("Session", "Checking session...");
|
||||
return hasV1Session(context, recipient) || hasV2Session(context, masterSecret, recipient);
|
||||
}
|
||||
|
||||
public static boolean hasRemoteIdentityKey(Context context,
|
||||
MasterSecret masterSecret,
|
||||
CanonicalRecipientAddress recipient)
|
||||
{
|
||||
return (hasV2Session(context, masterSecret, recipient) ||
|
||||
(hasV1Session(context, recipient) &&
|
||||
new SessionRecordV1(context, masterSecret, recipient).getIdentityKey() != null));
|
||||
}
|
||||
|
||||
private static boolean hasV2Session(Context context, MasterSecret masterSecret,
|
||||
CanonicalRecipientAddress recipient)
|
||||
{
|
||||
return SessionRecordV2.hasSession(context, masterSecret, recipient);
|
||||
}
|
||||
|
||||
private static boolean hasV1Session(Context context, CanonicalRecipientAddress recipient) {
|
||||
return SessionRecordV1.hasSession(context, recipient) &&
|
||||
RemoteKeyRecord.hasRecord(context, recipient) &&
|
||||
LocalKeyRecord.hasRecord(context, recipient);
|
||||
}
|
||||
|
||||
public static IdentityKey getRemoteIdentityKey(Context context, MasterSecret masterSecret,
|
||||
CanonicalRecipientAddress recipient)
|
||||
{
|
||||
if (SessionRecordV2.hasSession(context, masterSecret, recipient)) {
|
||||
return new SessionRecordV2(context, masterSecret, recipient).getRemoteIdentityKey();
|
||||
} else if (SessionRecordV1.hasSession(context, recipient)) {
|
||||
return new SessionRecordV1(context, masterSecret, recipient).getIdentityKey();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static IdentityKey getRemoteIdentityKey(Context context, MasterSecret masterSecret,
|
||||
long recipientId)
|
||||
{
|
||||
if (SessionRecordV2.hasSession(context, masterSecret, recipientId)) {
|
||||
return new SessionRecordV2(context, masterSecret, recipientId).getRemoteIdentityKey();
|
||||
} else if (SessionRecordV1.hasSession(context, recipientId)) {
|
||||
return new SessionRecordV1(context, masterSecret, recipientId).getIdentityKey();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static int getSessionVersion(Context context, MasterSecret masterSecret,
|
||||
CanonicalRecipientAddress recipient)
|
||||
{
|
||||
if (SessionRecordV2.hasSession(context, masterSecret, recipient)) {
|
||||
return new SessionRecordV2(context, masterSecret, recipient).getSessionVersion();
|
||||
} else if (SessionRecordV1.hasSession(context, recipient)) {
|
||||
return new SessionRecordV1(context, masterSecret, recipient).getSessionVersion();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
@@ -19,7 +19,7 @@ package org.whispersystems.textsecure.storage;
|
||||
import org.whispersystems.textsecure.crypto.InvalidMessageException;
|
||||
import org.whispersystems.textsecure.crypto.MasterCipher;
|
||||
import org.whispersystems.textsecure.crypto.MasterSecret;
|
||||
import org.whispersystems.textsecure.crypto.SessionCipher;
|
||||
import org.whispersystems.textsecure.crypto.kdf.NKDF;
|
||||
import org.whispersystems.textsecure.util.Conversions;
|
||||
import org.whispersystems.textsecure.util.Util;
|
||||
|
||||
@@ -77,13 +77,13 @@ public class SessionKey {
|
||||
this.localKeyId = Conversions.byteArrayToMedium(decrypted, 0);
|
||||
this.remoteKeyId = Conversions.byteArrayToMedium(decrypted, 3);
|
||||
|
||||
byte[] keyBytes = new byte[SessionCipher.CIPHER_KEY_LENGTH];
|
||||
byte[] keyBytes = new byte[NKDF.LEGACY_CIPHER_KEY_LENGTH];
|
||||
System.arraycopy(decrypted, 6, keyBytes, 0, keyBytes.length);
|
||||
|
||||
byte[] macBytes = new byte[SessionCipher.MAC_KEY_LENGTH];
|
||||
byte[] macBytes = new byte[NKDF.LEGACY_MAC_KEY_LENGTH];
|
||||
System.arraycopy(decrypted, 6 + keyBytes.length, macBytes, 0, macBytes.length);
|
||||
|
||||
if (decrypted.length < 6 + SessionCipher.CIPHER_KEY_LENGTH + SessionCipher.MAC_KEY_LENGTH + 1) {
|
||||
if (decrypted.length < 6 + NKDF.LEGACY_CIPHER_KEY_LENGTH + NKDF.LEGACY_MAC_KEY_LENGTH + 1) {
|
||||
throw new InvalidMessageException("No mode included");
|
||||
}
|
||||
|
||||
|
@@ -1,19 +1,3 @@
|
||||
/**
|
||||
* 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.whispersystems.textsecure.storage;
|
||||
|
||||
import android.content.Context;
|
||||
@@ -36,30 +20,28 @@ import java.nio.channels.FileChannel;
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class SessionRecord extends Record {
|
||||
public class SessionRecordV1 extends Record {
|
||||
|
||||
private static final int CURRENT_VERSION_MARKER = 0X55555557;
|
||||
private static final int[] VALID_VERSION_MARKERS = {CURRENT_VERSION_MARKER, 0X55555556, 0X55555555};
|
||||
private static final int CURRENT_VERSION_MARKER = 0X55555556;
|
||||
private static final int[] VALID_VERSION_MARKERS = {CURRENT_VERSION_MARKER, 0X55555555};
|
||||
private static final Object FILE_LOCK = new Object();
|
||||
|
||||
private int counter;
|
||||
private byte[] localFingerprint;
|
||||
private byte[] remoteFingerprint;
|
||||
private int negotiatedSessionVersion;
|
||||
private int currentSessionVersion;
|
||||
|
||||
private IdentityKey identityKey;
|
||||
private SessionKey sessionKeyRecord;
|
||||
private boolean verifiedSessionKey;
|
||||
private boolean prekeyBundleRequired;
|
||||
|
||||
private final MasterSecret masterSecret;
|
||||
|
||||
public SessionRecord(Context context, MasterSecret masterSecret, CanonicalRecipientAddress recipient) {
|
||||
public SessionRecordV1(Context context, MasterSecret masterSecret, CanonicalRecipientAddress recipient) {
|
||||
this(context, masterSecret, getRecipientId(context, recipient));
|
||||
}
|
||||
|
||||
public SessionRecord(Context context, MasterSecret masterSecret, long recipientId) {
|
||||
public SessionRecordV1(Context context, MasterSecret masterSecret, long recipientId) {
|
||||
super(context, SESSIONS_DIRECTORY, recipientId+"");
|
||||
this.masterSecret = masterSecret;
|
||||
this.currentSessionVersion = 31337;
|
||||
@@ -71,8 +53,12 @@ public class SessionRecord extends Record {
|
||||
}
|
||||
|
||||
public static boolean hasSession(Context context, CanonicalRecipientAddress recipient) {
|
||||
Log.w("LocalKeyRecord", "Checking: " + getRecipientId(context, recipient));
|
||||
return hasRecord(context, SESSIONS_DIRECTORY, getRecipientId(context, recipient)+"");
|
||||
return hasSession(context, getRecipientId(context, recipient));
|
||||
}
|
||||
|
||||
public static boolean hasSession(Context context, long recipientId) {
|
||||
Log.w("SessionRecordV1", "Checking: " + recipientId);
|
||||
return hasRecord(context, SESSIONS_DIRECTORY, recipientId+"");
|
||||
}
|
||||
|
||||
private static long getRecipientId(Context context, CanonicalRecipientAddress recipient) {
|
||||
@@ -96,14 +82,6 @@ public class SessionRecord extends Record {
|
||||
return (currentSessionVersion == 31337 ? 0 : currentSessionVersion);
|
||||
}
|
||||
|
||||
public int getNegotiatedSessionVersion() {
|
||||
return negotiatedSessionVersion;
|
||||
}
|
||||
|
||||
public void setNegotiatedSessionVersion(int sessionVersion) {
|
||||
this.negotiatedSessionVersion = sessionVersion;
|
||||
}
|
||||
|
||||
public void setSessionVersion(int sessionVersion) {
|
||||
this.currentSessionVersion = sessionVersion;
|
||||
}
|
||||
@@ -128,18 +106,6 @@ public class SessionRecord extends Record {
|
||||
return this.identityKey;
|
||||
}
|
||||
|
||||
public boolean isPrekeyBundleRequired() {
|
||||
return prekeyBundleRequired;
|
||||
}
|
||||
|
||||
public void setPrekeyBundleRequired(boolean prekeyBundleRequired) {
|
||||
this.prekeyBundleRequired = prekeyBundleRequired;
|
||||
}
|
||||
|
||||
// public void setVerifiedSessionKey(boolean verifiedSessionKey) {
|
||||
// this.verifiedSessionKey = verifiedSessionKey;
|
||||
// }
|
||||
|
||||
public boolean isVerifiedSession() {
|
||||
return this.verifiedSessionKey;
|
||||
}
|
||||
@@ -182,8 +148,6 @@ public class SessionRecord extends Record {
|
||||
writeInteger(currentSessionVersion, out);
|
||||
writeIdentityKey(out);
|
||||
writeInteger(verifiedSessionKey ? 1 : 0, out);
|
||||
writeInteger(prekeyBundleRequired ? 1 : 0, out);
|
||||
writeInteger(negotiatedSessionVersion, out);
|
||||
|
||||
if (sessionKeyRecord != null)
|
||||
writeBlob(sessionKeyRecord.serialize(), out);
|
||||
@@ -230,13 +194,6 @@ public class SessionRecord extends Record {
|
||||
this.verifiedSessionKey = (readInteger(in) == 1);
|
||||
}
|
||||
|
||||
if (versionMarker >= 0X55555557) {
|
||||
this.prekeyBundleRequired = (readInteger(in) == 1);
|
||||
this.negotiatedSessionVersion = readInteger(in);
|
||||
} else {
|
||||
this.negotiatedSessionVersion = currentSessionVersion;
|
||||
}
|
||||
|
||||
if (in.available() != 0) {
|
||||
try {
|
||||
this.sessionKeyRecord = new SessionKey(readBlob(in), masterSecret);
|
||||
@@ -265,7 +222,7 @@ public class SessionRecord extends Record {
|
||||
(this.sessionKeyRecord.getRemoteKeyId() == remoteKeyId) &&
|
||||
(this.sessionKeyRecord.getMode() == mode))
|
||||
{
|
||||
return this.sessionKeyRecord;
|
||||
return this.sessionKeyRecord;
|
||||
}
|
||||
|
||||
return null;
|
@@ -0,0 +1,505 @@
|
||||
/**
|
||||
* Copyright (C) 2013 Open 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.whispersystems.textsecure.storage;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.IdentityKey;
|
||||
import org.whispersystems.textsecure.crypto.IdentityKeyPair;
|
||||
import org.whispersystems.textsecure.crypto.InvalidKeyException;
|
||||
import org.whispersystems.textsecure.crypto.InvalidMessageException;
|
||||
import org.whispersystems.textsecure.crypto.MasterCipher;
|
||||
import org.whispersystems.textsecure.crypto.MasterSecret;
|
||||
import org.whispersystems.textsecure.crypto.ecc.Curve;
|
||||
import org.whispersystems.textsecure.crypto.ecc.ECKeyPair;
|
||||
import org.whispersystems.textsecure.crypto.ecc.ECPrivateKey;
|
||||
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
|
||||
import org.whispersystems.textsecure.crypto.ratchet.ChainKey;
|
||||
import org.whispersystems.textsecure.crypto.ratchet.MessageKeys;
|
||||
import org.whispersystems.textsecure.crypto.ratchet.RootKey;
|
||||
import org.whispersystems.textsecure.storage.StorageProtos.SessionStructure.Chain;
|
||||
import org.whispersystems.textsecure.storage.StorageProtos.SessionStructure.PendingKeyExchange;
|
||||
import org.whispersystems.textsecure.storage.StorageProtos.SessionStructure.PendingPreKey;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
/**
|
||||
* A disk record representing a current session.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class SessionRecordV2 extends Record {
|
||||
|
||||
private static final Object FILE_LOCK = new Object();
|
||||
private static final int CURRENT_VERSION = 1;
|
||||
|
||||
private final MasterSecret masterSecret;
|
||||
private StorageProtos.SessionStructure sessionStructure =
|
||||
StorageProtos.SessionStructure.newBuilder().build();
|
||||
|
||||
public SessionRecordV2(Context context, MasterSecret masterSecret, CanonicalRecipientAddress recipient) {
|
||||
this(context, masterSecret, getRecipientId(context, recipient));
|
||||
}
|
||||
|
||||
public SessionRecordV2(Context context, MasterSecret masterSecret, long recipientId) {
|
||||
super(context, SESSIONS_DIRECTORY_V2, recipientId+"");
|
||||
this.masterSecret = masterSecret;
|
||||
loadData();
|
||||
}
|
||||
|
||||
public static void delete(Context context, CanonicalRecipientAddress recipient) {
|
||||
delete(context, SESSIONS_DIRECTORY_V2, getRecipientId(context, recipient) + "");
|
||||
}
|
||||
|
||||
public static boolean hasSession(Context context, MasterSecret masterSecret,
|
||||
CanonicalRecipientAddress recipient)
|
||||
{
|
||||
return hasSession(context, masterSecret, getRecipientId(context, recipient));
|
||||
}
|
||||
|
||||
public static boolean hasSession(Context context, MasterSecret masterSecret, long recipientId) {
|
||||
return hasRecord(context, SESSIONS_DIRECTORY_V2, recipientId+"") &&
|
||||
new SessionRecordV2(context, masterSecret, recipientId).hasSenderChain();
|
||||
}
|
||||
|
||||
private static long getRecipientId(Context context, CanonicalRecipientAddress recipient) {
|
||||
return recipient.getCanonicalAddress(context);
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
this.sessionStructure = StorageProtos.SessionStructure.newBuilder().build();
|
||||
}
|
||||
|
||||
public void setSessionVersion(int version) {
|
||||
this.sessionStructure = this.sessionStructure.toBuilder()
|
||||
.setSessionVersion(version)
|
||||
.build();
|
||||
}
|
||||
|
||||
public int getSessionVersion() {
|
||||
return this.sessionStructure.getSessionVersion();
|
||||
}
|
||||
|
||||
public void setRemoteIdentityKey(IdentityKey identityKey) {
|
||||
this.sessionStructure = this.sessionStructure.toBuilder()
|
||||
.setRemoteIdentityPublic(ByteString.copyFrom(identityKey.serialize()))
|
||||
.build();
|
||||
}
|
||||
|
||||
public void setLocalIdentityKey(IdentityKey identityKey) {
|
||||
this.sessionStructure = this.sessionStructure.toBuilder()
|
||||
.setLocalIdentityPublic(ByteString.copyFrom(identityKey.serialize()))
|
||||
.build();
|
||||
}
|
||||
|
||||
public IdentityKey getRemoteIdentityKey() {
|
||||
try {
|
||||
if (!this.sessionStructure.hasRemoteIdentityPublic()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new IdentityKey(this.sessionStructure.getRemoteIdentityPublic().toByteArray(), 0);
|
||||
} catch (InvalidKeyException e) {
|
||||
Log.w("SessionRecordV2", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public IdentityKey getLocalIdentityKey() {
|
||||
try {
|
||||
return new IdentityKey(this.sessionStructure.getLocalIdentityPublic().toByteArray(), 0);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public int getPreviousCounter() {
|
||||
return sessionStructure.getPreviousCounter();
|
||||
}
|
||||
|
||||
public void setPreviousCounter(int previousCounter) {
|
||||
this.sessionStructure = this.sessionStructure.toBuilder()
|
||||
.setPreviousCounter(previousCounter)
|
||||
.build();
|
||||
}
|
||||
|
||||
public RootKey getRootKey() {
|
||||
return new RootKey(this.sessionStructure.getRootKey().toByteArray());
|
||||
}
|
||||
|
||||
public void setRootKey(RootKey rootKey) {
|
||||
this.sessionStructure = this.sessionStructure.toBuilder()
|
||||
.setRootKey(ByteString.copyFrom(rootKey.getKeyBytes()))
|
||||
.build();
|
||||
}
|
||||
|
||||
public ECPublicKey getSenderEphemeral() {
|
||||
try {
|
||||
return Curve.decodePoint(sessionStructure.getSenderChain().getSenderEphemeral().toByteArray(), 0);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public ECKeyPair getSenderEphemeralPair() {
|
||||
ECPublicKey publicKey = getSenderEphemeral();
|
||||
ECPrivateKey privateKey = Curve.decodePrivatePoint(publicKey.getType(),
|
||||
sessionStructure.getSenderChain()
|
||||
.getSenderEphemeralPrivate()
|
||||
.toByteArray());
|
||||
|
||||
return new ECKeyPair(publicKey, privateKey);
|
||||
}
|
||||
|
||||
public boolean hasReceiverChain(ECPublicKey senderEphemeral) {
|
||||
return getReceiverChain(senderEphemeral) != null;
|
||||
}
|
||||
|
||||
public boolean hasSenderChain() {
|
||||
return sessionStructure.hasSenderChain();
|
||||
}
|
||||
|
||||
private Pair<Chain,Integer> getReceiverChain(ECPublicKey senderEphemeral) {
|
||||
List<Chain> receiverChains = sessionStructure.getReceiverChainsList();
|
||||
int index = 0;
|
||||
|
||||
for (Chain receiverChain : receiverChains) {
|
||||
try {
|
||||
ECPublicKey chainSenderEphemeral = Curve.decodePoint(receiverChain.getSenderEphemeral().toByteArray(), 0);
|
||||
|
||||
if (chainSenderEphemeral.equals(senderEphemeral)) {
|
||||
return new Pair<Chain,Integer>(receiverChain,index);
|
||||
}
|
||||
} catch (InvalidKeyException e) {
|
||||
Log.w("SessionRecordV2", e);
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public ChainKey getReceiverChainKey(ECPublicKey senderEphemeral) {
|
||||
Pair<Chain,Integer> receiverChainAndIndex = getReceiverChain(senderEphemeral);
|
||||
Chain receiverChain = receiverChainAndIndex.first;
|
||||
|
||||
if (receiverChain == null) {
|
||||
return null;
|
||||
} else {
|
||||
return new ChainKey(receiverChain.getChainKey().getKey().toByteArray(),
|
||||
receiverChain.getChainKey().getIndex());
|
||||
}
|
||||
}
|
||||
|
||||
public void addReceiverChain(ECPublicKey senderEphemeral, ChainKey chainKey) {
|
||||
Chain.ChainKey chainKeyStructure = Chain.ChainKey.newBuilder()
|
||||
.setKey(ByteString.copyFrom(chainKey.getKey()))
|
||||
.setIndex(chainKey.getIndex())
|
||||
.build();
|
||||
|
||||
Chain chain = Chain.newBuilder()
|
||||
.setChainKey(chainKeyStructure)
|
||||
.setSenderEphemeral(ByteString.copyFrom(senderEphemeral.serialize()))
|
||||
.build();
|
||||
|
||||
// XXX knock old chain out.
|
||||
this.sessionStructure = this.sessionStructure.toBuilder().addReceiverChains(chain).build();
|
||||
}
|
||||
|
||||
public void setSenderChain(ECKeyPair senderEphemeralPair, ChainKey chainKey) {
|
||||
Chain.ChainKey chainKeyStructure = Chain.ChainKey.newBuilder()
|
||||
.setKey(ByteString.copyFrom(chainKey.getKey()))
|
||||
.setIndex(chainKey.getIndex())
|
||||
.build();
|
||||
|
||||
Chain senderChain = Chain.newBuilder()
|
||||
.setSenderEphemeral(ByteString.copyFrom(senderEphemeralPair.getPublicKey().serialize()))
|
||||
.setSenderEphemeralPrivate(ByteString.copyFrom(senderEphemeralPair.getPrivateKey().serialize()))
|
||||
.setChainKey(chainKeyStructure)
|
||||
.build();
|
||||
|
||||
this.sessionStructure = this.sessionStructure.toBuilder().setSenderChain(senderChain).build();
|
||||
}
|
||||
|
||||
public ChainKey getSenderChainKey() {
|
||||
Chain.ChainKey chainKeyStructure = sessionStructure.getSenderChain().getChainKey();
|
||||
return new ChainKey(chainKeyStructure.getKey().toByteArray(), chainKeyStructure.getIndex());
|
||||
}
|
||||
|
||||
|
||||
public void setSenderChainKey(ChainKey nextChainKey) {
|
||||
Chain.ChainKey chainKey = Chain.ChainKey.newBuilder()
|
||||
.setKey(ByteString.copyFrom(nextChainKey.getKey()))
|
||||
.setIndex(nextChainKey.getIndex())
|
||||
.build();
|
||||
|
||||
Chain chain = sessionStructure.getSenderChain().toBuilder()
|
||||
.setChainKey(chainKey).build();
|
||||
|
||||
this.sessionStructure = this.sessionStructure.toBuilder().setSenderChain(chain).build();
|
||||
}
|
||||
|
||||
public boolean hasMessageKeys(ECPublicKey senderEphemeral, int counter) {
|
||||
Pair<Chain,Integer> chainAndIndex = getReceiverChain(senderEphemeral);
|
||||
Chain chain = chainAndIndex.first;
|
||||
|
||||
if (chain == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
List<Chain.MessageKey> messageKeyList = chain.getMessageKeysList();
|
||||
|
||||
for (Chain.MessageKey messageKey : messageKeyList) {
|
||||
if (messageKey.getIndex() == counter) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public MessageKeys removeMessageKeys(ECPublicKey senderEphemeral, int counter) {
|
||||
Pair<Chain,Integer> chainAndIndex = getReceiverChain(senderEphemeral);
|
||||
Chain chain = chainAndIndex.first;
|
||||
|
||||
if (chain == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<Chain.MessageKey> messageKeyList = chain.getMessageKeysList();
|
||||
Iterator<Chain.MessageKey> messageKeyIterator = messageKeyList.iterator();
|
||||
MessageKeys result = null;
|
||||
|
||||
while (messageKeyIterator.hasNext()) {
|
||||
Chain.MessageKey messageKey = messageKeyIterator.next();
|
||||
|
||||
if (messageKey.getIndex() == counter) {
|
||||
result = new MessageKeys(new SecretKeySpec(messageKey.getCipherKey().toByteArray(), "AES"),
|
||||
new SecretKeySpec(messageKey.getMacKey().toByteArray(), "HmacSHA256"),
|
||||
messageKey.getIndex());
|
||||
|
||||
messageKeyIterator.remove();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Chain updatedChain = chain.toBuilder().clearMessageKeys()
|
||||
.addAllMessageKeys(messageKeyList)
|
||||
.build();
|
||||
|
||||
this.sessionStructure = this.sessionStructure.toBuilder()
|
||||
.setReceiverChains(chainAndIndex.second, updatedChain)
|
||||
.build();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public void setMessageKeys(ECPublicKey senderEphemeral, MessageKeys messageKeys) {
|
||||
Pair<Chain,Integer> chainAndIndex = getReceiverChain(senderEphemeral);
|
||||
Chain chain = chainAndIndex.first;
|
||||
Chain.MessageKey messageKeyStructure = Chain.MessageKey.newBuilder()
|
||||
.setCipherKey(ByteString.copyFrom(messageKeys.getCipherKey().getEncoded()))
|
||||
.setMacKey(ByteString.copyFrom(messageKeys.getMacKey().getEncoded()))
|
||||
.setIndex(messageKeys.getCounter())
|
||||
.build();
|
||||
|
||||
Chain updatedChain = chain.toBuilder()
|
||||
.addMessageKeys(messageKeyStructure)
|
||||
.build();
|
||||
|
||||
this.sessionStructure = this.sessionStructure.toBuilder()
|
||||
.setReceiverChains(chainAndIndex.second, updatedChain)
|
||||
.build();
|
||||
}
|
||||
|
||||
public void setReceiverChainKey(ECPublicKey senderEphemeral, ChainKey chainKey) {
|
||||
Pair<Chain,Integer> chainAndIndex = getReceiverChain(senderEphemeral);
|
||||
Chain chain = chainAndIndex.first;
|
||||
|
||||
Chain.ChainKey chainKeyStructure = Chain.ChainKey.newBuilder()
|
||||
.setKey(ByteString.copyFrom(chainKey.getKey()))
|
||||
.setIndex(chainKey.getIndex())
|
||||
.build();
|
||||
|
||||
Chain updatedChain = chain.toBuilder().setChainKey(chainKeyStructure).build();
|
||||
|
||||
this.sessionStructure = this.sessionStructure.toBuilder()
|
||||
.setReceiverChains(chainAndIndex.second, updatedChain)
|
||||
.build();
|
||||
}
|
||||
|
||||
public void setPendingKeyExchange(int sequence,
|
||||
ECKeyPair ourBaseKey,
|
||||
ECKeyPair ourEphemeralKey,
|
||||
IdentityKeyPair ourIdentityKey)
|
||||
{
|
||||
PendingKeyExchange structure =
|
||||
PendingKeyExchange.newBuilder()
|
||||
.setSequence(sequence)
|
||||
.setLocalBaseKey(ByteString.copyFrom(ourBaseKey.getPublicKey().serialize()))
|
||||
.setLocalBaseKeyPrivate(ByteString.copyFrom(ourBaseKey.getPrivateKey().serialize()))
|
||||
.setLocalEphemeralKey(ByteString.copyFrom(ourEphemeralKey.getPublicKey().serialize()))
|
||||
.setLocalEphemeralKeyPrivate(ByteString.copyFrom(ourEphemeralKey.getPrivateKey().serialize()))
|
||||
.setLocalIdentityKey(ByteString.copyFrom(ourIdentityKey.getPublicKey().serialize()))
|
||||
.setLocalIdentityKeyPrivate(ByteString.copyFrom(ourIdentityKey.getPrivateKey().serialize()))
|
||||
.build();
|
||||
|
||||
this.sessionStructure = this.sessionStructure.toBuilder()
|
||||
.setPendingKeyExchange(structure)
|
||||
.build();
|
||||
}
|
||||
|
||||
public int getPendingKeyExchangeSequence() {
|
||||
return sessionStructure.getPendingKeyExchange().getSequence();
|
||||
}
|
||||
|
||||
public ECKeyPair getPendingKeyExchangeBaseKey() throws InvalidKeyException {
|
||||
ECPublicKey publicKey = Curve.decodePoint(sessionStructure.getPendingKeyExchange()
|
||||
.getLocalBaseKey().toByteArray(), 0);
|
||||
|
||||
ECPrivateKey privateKey = Curve.decodePrivatePoint(publicKey.getType(),
|
||||
sessionStructure.getPendingKeyExchange()
|
||||
.getLocalBaseKeyPrivate()
|
||||
.toByteArray());
|
||||
|
||||
return new ECKeyPair(publicKey, privateKey);
|
||||
}
|
||||
|
||||
public ECKeyPair getPendingKeyExchangeEphemeralKey() throws InvalidKeyException {
|
||||
ECPublicKey publicKey = Curve.decodePoint(sessionStructure.getPendingKeyExchange()
|
||||
.getLocalEphemeralKey().toByteArray(), 0);
|
||||
|
||||
ECPrivateKey privateKey = Curve.decodePrivatePoint(publicKey.getType(),
|
||||
sessionStructure.getPendingKeyExchange()
|
||||
.getLocalEphemeralKeyPrivate()
|
||||
.toByteArray());
|
||||
|
||||
return new ECKeyPair(publicKey, privateKey);
|
||||
}
|
||||
|
||||
public IdentityKeyPair getPendingKeyExchangeIdentityKey() throws InvalidKeyException {
|
||||
IdentityKey publicKey = new IdentityKey(sessionStructure.getPendingKeyExchange()
|
||||
.getLocalIdentityKey().toByteArray(), 0);
|
||||
|
||||
ECPrivateKey privateKey = Curve.decodePrivatePoint(publicKey.getPublicKey().getType(),
|
||||
sessionStructure.getPendingKeyExchange()
|
||||
.getLocalIdentityKeyPrivate()
|
||||
.toByteArray());
|
||||
|
||||
return new IdentityKeyPair(publicKey, privateKey);
|
||||
}
|
||||
|
||||
public boolean hasPendingKeyExchange() {
|
||||
return sessionStructure.hasPendingKeyExchange();
|
||||
}
|
||||
|
||||
public void setPendingPreKey(int preKeyId, ECPublicKey baseKey) {
|
||||
PendingPreKey pending = PendingPreKey.newBuilder()
|
||||
.setPreKeyId(preKeyId)
|
||||
.setBaseKey(ByteString.copyFrom(baseKey.serialize()))
|
||||
.build();
|
||||
|
||||
this.sessionStructure = this.sessionStructure.toBuilder()
|
||||
.setPendingPreKey(pending)
|
||||
.build();
|
||||
}
|
||||
|
||||
public boolean hasPendingPreKey() {
|
||||
return this.sessionStructure.hasPendingPreKey();
|
||||
}
|
||||
|
||||
public Pair<Integer, ECPublicKey> getPendingPreKey() {
|
||||
try {
|
||||
return new Pair<Integer, ECPublicKey>(sessionStructure.getPendingPreKey().getPreKeyId(),
|
||||
Curve.decodePoint(sessionStructure.getPendingPreKey()
|
||||
.getBaseKey()
|
||||
.toByteArray(), 0));
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void clearPendingPreKey() {
|
||||
this.sessionStructure = this.sessionStructure.toBuilder()
|
||||
.clearPendingPreKey()
|
||||
.build();
|
||||
}
|
||||
|
||||
public void save() {
|
||||
synchronized (FILE_LOCK) {
|
||||
try {
|
||||
RandomAccessFile file = openRandomAccessFile();
|
||||
FileChannel out = file.getChannel();
|
||||
out.position(0);
|
||||
|
||||
MasterCipher cipher = new MasterCipher(masterSecret);
|
||||
writeInteger(CURRENT_VERSION, out);
|
||||
writeBlob(cipher.encryptBytes(sessionStructure.toByteArray()), out);
|
||||
|
||||
out.truncate(out.position());
|
||||
file.close();
|
||||
} catch (IOException ioe) {
|
||||
throw new IllegalArgumentException(ioe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void loadData() {
|
||||
synchronized (FILE_LOCK) {
|
||||
try {
|
||||
FileInputStream in = this.openInputStream();
|
||||
int versionMarker = readInteger(in);
|
||||
|
||||
if (versionMarker > CURRENT_VERSION) {
|
||||
throw new AssertionError("Unknown version: " + versionMarker);
|
||||
}
|
||||
|
||||
MasterCipher cipher = new MasterCipher(masterSecret);
|
||||
byte[] encryptedBlob = readBlob(in);
|
||||
|
||||
|
||||
this.sessionStructure = StorageProtos.SessionStructure
|
||||
.parseFrom(cipher.decryptBytes(encryptedBlob));
|
||||
|
||||
in.close();
|
||||
} catch (FileNotFoundException e) {
|
||||
Log.w("SessionRecordV2", "No session information found.");
|
||||
// XXX
|
||||
} catch (IOException ioe) {
|
||||
Log.w("SessionRecordV2", ioe);
|
||||
// XXX
|
||||
} catch (InvalidMessageException e) {
|
||||
Log.w("SessionRecordV2", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
4187
library/src/org/whispersystems/textsecure/storage/StorageProtos.java
Normal file
4187
library/src/org/whispersystems/textsecure/storage/StorageProtos.java
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user