mirror of
https://github.com/oxen-io/session-android.git
synced 2025-10-27 12:39:04 +00:00
Basic support for prekeybundle message delivery and receipt.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
package org.whispersystems.textsecure;
|
||||
|
||||
public class Release {
|
||||
public static final String PUSH_SERVICE_URL = "https://gcm.textsecure.whispersystems.org";
|
||||
// public static final String PUSH_SERVICE_URL = "http://192.168.1.135:8080";
|
||||
public static final boolean ENFORCE_SSL = true;
|
||||
// public static final String PUSH_SERVICE_URL = "https://gcm.textsecure.whispersystems.org";
|
||||
public static final String PUSH_SERVICE_URL = "http://192.168.1.135:8080";
|
||||
public static final boolean ENFORCE_SSL = false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* 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 org.spongycastle.crypto.params.ECPrivateKeyParameters;
|
||||
|
||||
/**
|
||||
* Holder for public and private identity key pair.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
public class IdentityKeyPair {
|
||||
|
||||
private final IdentityKey publicKey;
|
||||
private final ECPrivateKeyParameters privateKey;
|
||||
|
||||
public IdentityKeyPair(IdentityKey publicKey, ECPrivateKeyParameters privateKey) {
|
||||
this.publicKey = publicKey;
|
||||
this.privateKey = privateKey;
|
||||
}
|
||||
|
||||
public IdentityKey getPublicKey() {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
public ECPrivateKeyParameters getPrivateKey() {
|
||||
return privateKey;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
public class InvalidVersionException extends Exception {
|
||||
|
||||
public InvalidVersionException() {
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public InvalidVersionException(String detailMessage) {
|
||||
super(detailMessage);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public InvalidVersionException(Throwable throwable) {
|
||||
super(throwable);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public InvalidVersionException(String detailMessage, Throwable throwable) {
|
||||
super(detailMessage, throwable);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
}
|
||||
@@ -33,6 +33,10 @@ public class PreKeyPair {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
public AsymmetricCipherKeyPair getKeyPair() {
|
||||
return new AsymmetricCipherKeyPair(publicKey.getPublicKey(), privateKey);
|
||||
}
|
||||
|
||||
public byte[] serialize() {
|
||||
byte[] publicKeyBytes = publicKey.serialize();
|
||||
byte[] privateKeyBytes = masterCipher.encryptKey(privateKey);
|
||||
|
||||
@@ -42,6 +42,9 @@ import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This is where the session encryption magic happens. Implements a compressed version of the OTR protocol.
|
||||
@@ -58,35 +61,42 @@ public class SessionCipher {
|
||||
|
||||
public static final int ENCRYPTED_MESSAGE_OVERHEAD = EncryptedMessage.HEADER_LENGTH + MessageMac.MAC_LENGTH;
|
||||
|
||||
public SessionCipherContext getEncryptionContext(Context context, MasterSecret masterSecret,
|
||||
public SessionCipherContext getEncryptionContext(Context context,
|
||||
MasterSecret masterSecret,
|
||||
IdentityKeyPair localIdentityKey,
|
||||
CanonicalRecipientAddress recipient)
|
||||
{
|
||||
try {
|
||||
KeyRecords records = getKeyRecords(context, masterSecret, recipient);
|
||||
int localKeyId = records.getLocalKeyRecord().getCurrentKeyPair().getId();
|
||||
int remoteKeyId = records.getRemoteKeyRecord().getCurrentRemoteKey().getId();
|
||||
SessionKey sessionKey = getSessionKey(masterSecret, Cipher.ENCRYPT_MODE, records, localKeyId, remoteKeyId);
|
||||
int negotiatedVersion = records.getSessionRecord().getSessionVersion();
|
||||
SessionKey sessionKey = getSessionKey(masterSecret, Cipher.ENCRYPT_MODE, negotiatedVersion, localIdentityKey, records, localKeyId, remoteKeyId);
|
||||
PublicKey nextKey = records.getLocalKeyRecord().getNextKeyPair().getPublicKey();
|
||||
int counter = records.getSessionRecord().getCounter();
|
||||
int negotiatedVersion = records.getSessionRecord().getSessionVersion();
|
||||
|
||||
return new SessionCipherContext(records, sessionKey, localKeyId, remoteKeyId, nextKey, counter, negotiatedVersion);
|
||||
|
||||
return new SessionCipherContext(records, sessionKey, localKeyId, remoteKeyId,
|
||||
nextKey, counter, negotiatedVersion, negotiatedVersion);
|
||||
} catch (InvalidKeyIdException 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 negotiatedVersion)
|
||||
int messageVersion, int negotiatedVersion)
|
||||
throws InvalidMessageException
|
||||
{
|
||||
try {
|
||||
KeyRecords records = getKeyRecords(context, masterSecret, recipient);
|
||||
SessionKey sessionKey = getSessionKey(masterSecret, Cipher.DECRYPT_MODE, records, recipientKeyId, senderKeyId);
|
||||
return new SessionCipherContext(records, sessionKey, senderKeyId, recipientKeyId, nextKey, counter, negotiatedVersion);
|
||||
SessionKey sessionKey = getSessionKey(masterSecret, Cipher.DECRYPT_MODE, messageVersion, localIdentityKey, records, recipientKeyId, senderKeyId);
|
||||
return new SessionCipherContext(records, sessionKey, senderKeyId,
|
||||
recipientKeyId, nextKey, counter,
|
||||
messageVersion, negotiatedVersion);
|
||||
} catch (InvalidKeyIdException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
}
|
||||
@@ -193,12 +203,12 @@ public class SessionCipher {
|
||||
}
|
||||
}
|
||||
|
||||
private SecretKeySpec deriveCipherSecret(int mode, BigInteger sharedSecret,
|
||||
private SecretKeySpec deriveCipherSecret(int mode, List<BigInteger> sharedSecret,
|
||||
KeyRecords records, int localKeyId,
|
||||
int remoteKeyId)
|
||||
throws InvalidKeyIdException
|
||||
{
|
||||
byte[] sharedSecretBytes = sharedSecret.toByteArray();
|
||||
byte[] sharedSecretBytes = concatenateSharedSecrets(sharedSecret);
|
||||
byte[] derivedBytes = deriveBytes(sharedSecretBytes, 16 * 2);
|
||||
byte[] cipherSecret = new byte[16];
|
||||
|
||||
@@ -213,18 +223,40 @@ public class SessionCipher {
|
||||
|
||||
return new SecretKeySpec(cipherSecret, "AES");
|
||||
}
|
||||
|
||||
private byte[] concatenateSharedSecrets(List<BigInteger> sharedSecrets) {
|
||||
int totalByteSize = 0;
|
||||
List<byte[]> byteValues = new LinkedList<byte[]>();
|
||||
|
||||
for (BigInteger sharedSecret : sharedSecrets) {
|
||||
byte[] byteValue = sharedSecret.toByteArray();
|
||||
totalByteSize += byteValue.length;
|
||||
byteValues.add(byteValue);
|
||||
}
|
||||
|
||||
byte[] combined = new byte[totalByteSize];
|
||||
int offset = 0;
|
||||
|
||||
for (byte[] byteValue : byteValues) {
|
||||
System.arraycopy(byteValue, 0, combined, offset, byteValue.length);
|
||||
offset += byteValue.length;
|
||||
}
|
||||
|
||||
return combined;
|
||||
}
|
||||
|
||||
private boolean isLowEnd(KeyRecords records, int localKeyId, int remoteKeyId)
|
||||
throws InvalidKeyIdException
|
||||
{
|
||||
ECPublicKeyParameters localPublic = records.getLocalKeyRecord().getKeyPairForId(localKeyId).getPublicKey().getKey();
|
||||
ECPublicKeyParameters remotePublic = records.getRemoteKeyRecord().getKeyForId(remoteKeyId).getKey();
|
||||
|
||||
BigInteger local = localPublic.getQ().getX().toBigInteger();
|
||||
BigInteger remote = remotePublic.getQ().getX().toBigInteger();
|
||||
|
||||
|
||||
return local.compareTo(remote) < 0;
|
||||
}
|
||||
|
||||
|
||||
private byte[] deriveBytes(byte[] seed, int bytesNeeded) {
|
||||
MessageDigest md;
|
||||
|
||||
@@ -246,7 +278,10 @@ public class SessionCipher {
|
||||
return md.digest();
|
||||
}
|
||||
|
||||
private SessionKey getSessionKey(MasterSecret masterSecret, int mode, KeyRecords records,
|
||||
private SessionKey getSessionKey(MasterSecret masterSecret, int mode,
|
||||
int messageVersion,
|
||||
IdentityKeyPair localIdentityKey,
|
||||
KeyRecords records,
|
||||
int localKeyId, int remoteKeyId)
|
||||
throws InvalidKeyIdException
|
||||
{
|
||||
@@ -256,23 +291,41 @@ public class SessionCipher {
|
||||
if (sessionKey != null)
|
||||
return sessionKey;
|
||||
|
||||
BigInteger sharedSecret = calculateSharedSecret(records, localKeyId, remoteKeyId);
|
||||
SecretKeySpec cipherKey = deriveCipherSecret(mode, sharedSecret, records, localKeyId, remoteKeyId);
|
||||
SecretKeySpec macKey = deriveMacSecret(cipherKey);
|
||||
List<BigInteger> sharedSecret = calculateSharedSecret(messageVersion, localIdentityKey, records, localKeyId, remoteKeyId);
|
||||
SecretKeySpec cipherKey = deriveCipherSecret(mode, sharedSecret, records, localKeyId, remoteKeyId);
|
||||
SecretKeySpec macKey = deriveMacSecret(cipherKey);
|
||||
|
||||
return new SessionKey(localKeyId, remoteKeyId, cipherKey, macKey, masterSecret);
|
||||
}
|
||||
|
||||
private BigInteger calculateSharedSecret(KeyRecords records, int localKeyId, int remoteKeyId)
|
||||
private List<BigInteger> calculateSharedSecret(int messageVersion,
|
||||
IdentityKeyPair localIdentityKey,
|
||||
KeyRecords records,
|
||||
int localKeyId, int remoteKeyId)
|
||||
throws InvalidKeyIdException
|
||||
{
|
||||
ECDHBasicAgreement agreement = new ECDHBasicAgreement();
|
||||
AsymmetricCipherKeyPair localKeyPair = records.getLocalKeyRecord().getKeyPairForId(localKeyId).getKeyPair();
|
||||
ECPublicKeyParameters remoteKey = records.getRemoteKeyRecord().getKeyForId(remoteKeyId).getKey();
|
||||
|
||||
agreement.init(localKeyPair.getPrivate());
|
||||
KeyPair localKeyPair = records.getLocalKeyRecord().getKeyPairForId(localKeyId);
|
||||
ECPublicKeyParameters remoteKey = records.getRemoteKeyRecord().getKeyForId(remoteKeyId).getKey();
|
||||
IdentityKey remoteIdentityKey = records.getSessionRecord().getIdentityKey();
|
||||
|
||||
return KeyUtil.calculateAgreement(agreement, remoteKey);
|
||||
if (isInitiallyExchangedKeys(records, localKeyId, remoteKeyId) &&
|
||||
messageVersion >= EncryptedMessage.CRADLE_AGREEMENT_VERSION)
|
||||
{
|
||||
return SharedSecretCalculator.calculateSharedSecret(localKeyPair, localIdentityKey,
|
||||
remoteKey, remoteIdentityKey);
|
||||
} else {
|
||||
return SharedSecretCalculator.calculateSharedSecret(localKeyPair, remoteKey);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -285,6 +338,7 @@ public class SessionCipher {
|
||||
}
|
||||
|
||||
private static class KeyRecords {
|
||||
|
||||
private final LocalKeyRecord localKeyRecord;
|
||||
private final RemoteKeyRecord remoteKeyRecord;
|
||||
private final SessionRecord sessionRecord;
|
||||
@@ -309,6 +363,7 @@ public class SessionCipher {
|
||||
}
|
||||
|
||||
public static class SessionCipherContext {
|
||||
|
||||
private final LocalKeyRecord localKeyRecord;
|
||||
private final RemoteKeyRecord remoteKeyRecord;
|
||||
private final SessionRecord sessionRecord;
|
||||
@@ -317,6 +372,7 @@ public class SessionCipher {
|
||||
private final int recipientKeyId;
|
||||
private final PublicKey nextKey;
|
||||
private final int counter;
|
||||
private final int messageVersion;
|
||||
private final int negotiatedVersion;
|
||||
|
||||
public SessionCipherContext(KeyRecords records,
|
||||
@@ -325,6 +381,7 @@ public class SessionCipher {
|
||||
int receiverKeyId,
|
||||
PublicKey nextKey,
|
||||
int counter,
|
||||
int messageVersion,
|
||||
int negotiatedVersion)
|
||||
{
|
||||
this.localKeyRecord = records.getLocalKeyRecord();
|
||||
@@ -335,6 +392,7 @@ public class SessionCipher {
|
||||
this.recipientKeyId = receiverKeyId;
|
||||
this.nextKey = nextKey;
|
||||
this.counter = counter;
|
||||
this.messageVersion = messageVersion;
|
||||
this.negotiatedVersion = negotiatedVersion;
|
||||
}
|
||||
|
||||
@@ -373,6 +431,10 @@ public class SessionCipher {
|
||||
public int getNegotiatedVersion() {
|
||||
return negotiatedVersion;
|
||||
}
|
||||
|
||||
public int getMessageVersion() {
|
||||
return messageVersion;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package org.whispersystems.textsecure.crypto;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.spongycastle.crypto.CipherParameters;
|
||||
import org.spongycastle.crypto.agreement.ECDHBasicAgreement;
|
||||
import org.spongycastle.crypto.params.ECPublicKeyParameters;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class SharedSecretCalculator {
|
||||
|
||||
public static List<BigInteger> calculateSharedSecret(KeyPair localKeyPair,
|
||||
IdentityKeyPair localIdentityKeyPair,
|
||||
ECPublicKeyParameters remoteKey,
|
||||
IdentityKey remoteIdentityKey)
|
||||
{
|
||||
Log.w("SharedSecretCalculator", "Calculating shared secret with cradle agreement...");
|
||||
List<BigInteger> results = new LinkedList<BigInteger>();
|
||||
|
||||
if (isLowEnd(localKeyPair.getPublicKey().getKey(), remoteKey)) {
|
||||
results.add(calculateAgreement(localIdentityKeyPair.getPrivateKey(), remoteKey));
|
||||
|
||||
results.add(calculateAgreement(localKeyPair.getKeyPair().getPrivate(),
|
||||
remoteIdentityKey.getPublicKeyParameters()));
|
||||
} else {
|
||||
results.add(calculateAgreement(localKeyPair.getKeyPair().getPrivate(),
|
||||
remoteIdentityKey.getPublicKeyParameters()));
|
||||
|
||||
results.add(calculateAgreement(localIdentityKeyPair.getPrivateKey(), remoteKey));
|
||||
}
|
||||
|
||||
results.add(calculateAgreement(localKeyPair.getKeyPair().getPrivate(), remoteKey));
|
||||
return results;
|
||||
}
|
||||
|
||||
public static List<BigInteger> calculateSharedSecret(KeyPair localKeyPair,
|
||||
ECPublicKeyParameters remoteKey)
|
||||
{
|
||||
Log.w("SharedSecretCalculator", "Calculating shared secret with standard agreement...");
|
||||
List<BigInteger> results = new LinkedList<BigInteger>();
|
||||
results.add(calculateAgreement(localKeyPair.getKeyPair().getPrivate(), remoteKey));
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static BigInteger calculateAgreement(CipherParameters privateKey,
|
||||
ECPublicKeyParameters publicKey)
|
||||
{
|
||||
ECDHBasicAgreement agreement = new ECDHBasicAgreement();
|
||||
agreement.init(privateKey);
|
||||
|
||||
return KeyUtil.calculateAgreement(agreement, publicKey);
|
||||
}
|
||||
|
||||
|
||||
private static boolean isLowEnd(ECPublicKeyParameters localPublic,
|
||||
ECPublicKeyParameters remotePublic)
|
||||
{
|
||||
BigInteger local = localPublic.getQ().getX().toBigInteger();
|
||||
BigInteger remote = remotePublic.getQ().getX().toBigInteger();
|
||||
|
||||
return local.compareTo(remote) < 0;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,16 +17,20 @@
|
||||
package org.whispersystems.textsecure.crypto.protocol;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.IdentityKeyPair;
|
||||
import org.whispersystems.textsecure.crypto.InvalidKeyException;
|
||||
import org.whispersystems.textsecure.crypto.InvalidMessageException;
|
||||
import org.whispersystems.textsecure.crypto.MasterSecret;
|
||||
import org.whispersystems.textsecure.crypto.MessageMac;
|
||||
import org.whispersystems.textsecure.crypto.PublicKey;
|
||||
import org.whispersystems.textsecure.crypto.SessionCipher;
|
||||
import org.whispersystems.textsecure.crypto.SessionCipher.SessionCipherContext;
|
||||
import org.whispersystems.textsecure.crypto.TransportDetails;
|
||||
import org.whispersystems.textsecure.storage.CanonicalRecipientAddress;
|
||||
import org.whispersystems.textsecure.util.Conversions;
|
||||
import org.whispersystems.textsecure.util.Hex;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
@@ -39,29 +43,35 @@ import java.nio.ByteBuffer;
|
||||
|
||||
public class EncryptedMessage {
|
||||
|
||||
public static final int SUPPORTED_VERSION = 1;
|
||||
public static final int SUPPORTED_VERSION = 2;
|
||||
public static final int CRADLE_AGREEMENT_VERSION = 2;
|
||||
|
||||
private static final int VERSION_LENGTH = 1;
|
||||
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;
|
||||
static final int NEXT_KEY_LENGTH = PublicKey.KEY_SIZE;
|
||||
private static final int COUNTER_LENGTH = 3;
|
||||
public static final int HEADER_LENGTH = VERSION_LENGTH + SENDER_KEY_ID_LENGTH + RECEIVER_KEY_ID_LENGTH + COUNTER_LENGTH + NEXT_KEY_LENGTH;
|
||||
|
||||
private static final int VERSION_OFFSET = 0;
|
||||
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;
|
||||
static final int RECEIVER_KEY_ID_OFFSET = SENDER_KEY_ID_OFFSET + SENDER_KEY_ID_LENGTH;
|
||||
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 TEXT_OFFSET = COUNTER_OFFSET + COUNTER_LENGTH;
|
||||
|
||||
private final Context context;
|
||||
private final MasterSecret masterSecret;
|
||||
private final IdentityKeyPair localIdentityKey;
|
||||
private final TransportDetails transportDetails;
|
||||
|
||||
public EncryptedMessage(Context context, MasterSecret masterSecret, TransportDetails transportDetails) {
|
||||
public EncryptedMessage(Context context, MasterSecret masterSecret,
|
||||
IdentityKeyPair localIdentityKey,
|
||||
TransportDetails transportDetails)
|
||||
{
|
||||
this.context = context.getApplicationContext();
|
||||
this.masterSecret = masterSecret;
|
||||
this.localIdentityKey = localIdentityKey;
|
||||
this.transportDetails = transportDetails;
|
||||
}
|
||||
|
||||
@@ -69,7 +79,7 @@ public class EncryptedMessage {
|
||||
synchronized (SessionCipher.CIPHER_LOCK) {
|
||||
byte[] paddedBody = transportDetails.getPaddedMessageBody(plaintext);
|
||||
SessionCipher sessionCipher = new SessionCipher();
|
||||
SessionCipherContext sessionContext = sessionCipher.getEncryptionContext(context, masterSecret, recipient);
|
||||
SessionCipherContext sessionContext = sessionCipher.getEncryptionContext(context, masterSecret, localIdentityKey, recipient);
|
||||
byte[] ciphertextBody = sessionCipher.encrypt(sessionContext, paddedBody);
|
||||
byte[] formattedCiphertext = getFormattedCiphertext(sessionContext, ciphertextBody);
|
||||
byte[] ciphertextMessage = sessionCipher.mac(sessionContext, formattedCiphertext);
|
||||
@@ -84,25 +94,33 @@ public class EncryptedMessage {
|
||||
synchronized (SessionCipher.CIPHER_LOCK) {
|
||||
try {
|
||||
byte[] decodedMessage = transportDetails.getDecodedMessage(ciphertext);
|
||||
int messageVersion = getMessageVersion(decodedMessage);
|
||||
|
||||
if (decodedMessage.length <= HEADER_LENGTH) {
|
||||
throw new InvalidMessageException("Message is shorter than headers");
|
||||
}
|
||||
|
||||
int messageVersion = getMessageVersion(decodedMessage);
|
||||
|
||||
if (messageVersion > SUPPORTED_VERSION) {
|
||||
throw new InvalidMessageException("Unsupported version: " + messageVersion);
|
||||
}
|
||||
|
||||
int supportedVersion = getSupportedVersion(decodedMessage);
|
||||
int receiverKeyId = getReceiverKeyId(decodedMessage);
|
||||
int senderKeyId = getSenderKeyId(decodedMessage);
|
||||
int counter = getCiphertextCounter(decodedMessage);
|
||||
byte[] ciphertextBody = getMessageBody(decodedMessage);
|
||||
PublicKey nextRemoteKey = getNextRemoteKey(decodedMessage);
|
||||
int version = Math.min(supportedVersion, SUPPORTED_VERSION);
|
||||
SessionCipher sessionCipher = new SessionCipher();
|
||||
SessionCipherContext sessionContext = sessionCipher.getDecryptionContext(context, masterSecret,
|
||||
recipient, senderKeyId,
|
||||
receiverKeyId,
|
||||
nextRemoteKey,
|
||||
counter, version);
|
||||
int supportedVersion = getSupportedVersion(decodedMessage);
|
||||
int receiverKeyId = getReceiverKeyId(decodedMessage);
|
||||
int senderKeyId = getSenderKeyId(decodedMessage);
|
||||
int counter = getCiphertextCounter(decodedMessage);
|
||||
byte[] ciphertextBody = getMessageBody(decodedMessage);
|
||||
PublicKey nextRemoteKey = getNextRemoteKey(decodedMessage);
|
||||
int negotiatedVersion = Math.min(supportedVersion, SUPPORTED_VERSION);
|
||||
SessionCipher sessionCipher = new SessionCipher();
|
||||
SessionCipherContext sessionContext = sessionCipher.getDecryptionContext(context, masterSecret,
|
||||
localIdentityKey,
|
||||
recipient, senderKeyId,
|
||||
receiverKeyId,
|
||||
nextRemoteKey,
|
||||
counter,
|
||||
messageVersion,
|
||||
negotiatedVersion);
|
||||
|
||||
sessionCipher.verifyMac(sessionContext, decodedMessage);
|
||||
|
||||
@@ -155,7 +173,7 @@ public class EncryptedMessage {
|
||||
}
|
||||
|
||||
private byte[] getMessageBody(byte[] message) {
|
||||
byte[] body = new byte[message.length - HEADER_LENGTH];
|
||||
byte[] body = new byte[message.length - HEADER_LENGTH - MessageMac.MAC_LENGTH];
|
||||
System.arraycopy(message, TEXT_OFFSET, body, 0, body.length);
|
||||
|
||||
return body;
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* 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.InvalidVersionException;
|
||||
import org.whispersystems.textsecure.crypto.PublicKey;
|
||||
import org.whispersystems.textsecure.util.Base64;
|
||||
import org.whispersystems.textsecure.util.Conversions;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
private static final int VERSION_LENGTH = EncryptedMessage.VERSION_LENGTH;
|
||||
private static final int IDENTITY_KEY_LENGTH = IdentityKey.SIZE;
|
||||
public static final int HEADER_LENGTH = IDENTITY_KEY_LENGTH + EncryptedMessage.HEADER_LENGTH;
|
||||
|
||||
private static final int VERSION_OFFSET = EncryptedMessage.VERSION_OFFSET;
|
||||
private static final int IDENTITY_KEY_OFFSET = VERSION_OFFSET + EncryptedMessage.VERSION_LENGTH;
|
||||
private static final int PUBLIC_KEY_OFFSET = IDENTITY_KEY_LENGTH + EncryptedMessage.NEXT_KEY_OFFSET;
|
||||
private static final int PREKEY_ID_OFFSET = IDENTITY_KEY_LENGTH + EncryptedMessage.RECEIVER_KEY_ID_OFFSET;
|
||||
|
||||
private final byte[] messageBytes;
|
||||
|
||||
private final int supportedVersion;
|
||||
private final int messageVersion;
|
||||
private final int preKeyId;
|
||||
private final IdentityKey identityKey;
|
||||
private final PublicKey publicKey;
|
||||
private final byte[] bundledMessage;
|
||||
|
||||
public PreKeyBundleMessage(String message) throws InvalidKeyException, InvalidVersionException {
|
||||
try {
|
||||
this.messageBytes = Base64.decodeWithoutPadding(message);
|
||||
this.messageVersion = Conversions.highBitsToInt(this.messageBytes[VERSION_OFFSET]);
|
||||
|
||||
if (messageVersion > EncryptedMessage.SUPPORTED_VERSION)
|
||||
throw new InvalidVersionException("Key exchange with version: " + messageVersion +
|
||||
" but we only support: " + EncryptedMessage.SUPPORTED_VERSION);
|
||||
|
||||
this.supportedVersion = Conversions.lowBitsToInt(messageBytes[VERSION_OFFSET]);
|
||||
this.publicKey = new PublicKey(messageBytes, PUBLIC_KEY_OFFSET);
|
||||
this.identityKey = new IdentityKey(messageBytes, IDENTITY_KEY_OFFSET);
|
||||
this.preKeyId = Conversions.byteArrayToMedium(messageBytes, PREKEY_ID_OFFSET);
|
||||
this.bundledMessage = new byte[messageBytes.length - IDENTITY_KEY_LENGTH];
|
||||
|
||||
|
||||
this.bundledMessage[VERSION_OFFSET] = this.messageBytes[VERSION_OFFSET];
|
||||
System.arraycopy(messageBytes, IDENTITY_KEY_OFFSET+IDENTITY_KEY_LENGTH, bundledMessage, VERSION_OFFSET+VERSION_LENGTH, bundledMessage.length-VERSION_LENGTH);
|
||||
} catch (IOException e) {
|
||||
throw new InvalidKeyException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public PreKeyBundleMessage(IdentityKey identityKey, byte[] bundledMessage) {
|
||||
try {
|
||||
this.supportedVersion = EncryptedMessage.SUPPORTED_VERSION;
|
||||
this.messageVersion = EncryptedMessage.SUPPORTED_VERSION;
|
||||
this.identityKey = identityKey;
|
||||
this.publicKey = new PublicKey(bundledMessage, EncryptedMessage.NEXT_KEY_OFFSET);
|
||||
this.preKeyId = Conversions.byteArrayToMedium(bundledMessage, EncryptedMessage.RECEIVER_KEY_ID_OFFSET);
|
||||
this.bundledMessage = bundledMessage;
|
||||
this.messageBytes = new byte[IDENTITY_KEY_LENGTH + bundledMessage.length];
|
||||
|
||||
byte[] identityKeyBytes = identityKey.serialize();
|
||||
|
||||
messageBytes[VERSION_OFFSET] = bundledMessage[VERSION_OFFSET];
|
||||
System.arraycopy(identityKeyBytes, 0, messageBytes, IDENTITY_KEY_OFFSET, identityKeyBytes.length);
|
||||
System.arraycopy(bundledMessage, VERSION_OFFSET+VERSION_LENGTH, messageBytes, IDENTITY_KEY_OFFSET+IDENTITY_KEY_LENGTH, bundledMessage.length-VERSION_LENGTH);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public String serialize() {
|
||||
return Base64.encodeBytesWithoutPadding(this.messageBytes);
|
||||
}
|
||||
|
||||
public int getSupportedVersion() {
|
||||
return supportedVersion;
|
||||
}
|
||||
|
||||
public int getMessageVersion() {
|
||||
return messageVersion;
|
||||
}
|
||||
|
||||
public IdentityKey getIdentityKey() {
|
||||
return identityKey;
|
||||
}
|
||||
|
||||
public PublicKey getPublicKey() {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
public String getBundledMessage() {
|
||||
return Base64.encodeBytesWithoutPadding(bundledMessage);
|
||||
}
|
||||
|
||||
public int getPreKeyId() {
|
||||
return preKeyId;
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ public class IncomingPushMessage implements Parcelable {
|
||||
}
|
||||
};
|
||||
|
||||
private int type;
|
||||
private String source;
|
||||
private List<String> destinations;
|
||||
private String messageText;
|
||||
@@ -27,8 +28,9 @@ public class IncomingPushMessage implements Parcelable {
|
||||
private long timestamp;
|
||||
|
||||
public IncomingPushMessage(String source, List<String> destinations, String messageText,
|
||||
List<PushAttachmentPointer> attachments, long timestamp)
|
||||
int type, List<PushAttachmentPointer> attachments, long timestamp)
|
||||
{
|
||||
this.type = type;
|
||||
this.source = source;
|
||||
this.destinations = destinations;
|
||||
this.messageText = messageText;
|
||||
@@ -84,4 +86,8 @@ public class IncomingPushMessage implements Parcelable {
|
||||
dest.writeList(attachments);
|
||||
dest.writeLong(timestamp);
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,29 +5,36 @@ import java.util.List;
|
||||
|
||||
public class OutgoingPushMessage {
|
||||
|
||||
public static final int TYPE_MESSAGE = 1;
|
||||
public static final int TYPE_PREKEYED_MESSAGE = 2;
|
||||
|
||||
private int type;
|
||||
private List<String> destinations;
|
||||
private String messageText;
|
||||
private List<PushAttachmentPointer> attachments;
|
||||
|
||||
public OutgoingPushMessage(String destination, String messageText) {
|
||||
public OutgoingPushMessage(String destination, String messageText, int type) {
|
||||
this.destinations = new LinkedList<String>();
|
||||
this.attachments = new LinkedList<PushAttachmentPointer>();
|
||||
this.messageText = messageText;
|
||||
this.destinations.add(destination);
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public OutgoingPushMessage(List<String> destinations, String messageText) {
|
||||
public OutgoingPushMessage(List<String> destinations, String messageText, int type) {
|
||||
this.destinations = destinations;
|
||||
this.messageText = messageText;
|
||||
this.attachments = new LinkedList<PushAttachmentPointer>();
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public OutgoingPushMessage(List<String> destinations, String messageText,
|
||||
List<PushAttachmentPointer> attachments)
|
||||
List<PushAttachmentPointer> attachments, int type)
|
||||
{
|
||||
this.destinations = destinations;
|
||||
this.messageText = messageText;
|
||||
this.attachments = attachments;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public List<String> getDestinations() {
|
||||
@@ -42,4 +49,7 @@ public class OutgoingPushMessage {
|
||||
return attachments;
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,26 +78,26 @@ public class PushServiceSocket {
|
||||
makeRequest(REGISTER_GCM_PATH, "DELETE", null);
|
||||
}
|
||||
|
||||
public void sendMessage(String recipient, String messageText)
|
||||
public void sendMessage(String recipient, String messageText, int type)
|
||||
throws IOException
|
||||
{
|
||||
OutgoingPushMessage message = new OutgoingPushMessage(recipient, messageText);
|
||||
OutgoingPushMessage message = new OutgoingPushMessage(recipient, messageText, type);
|
||||
sendMessage(message);
|
||||
}
|
||||
|
||||
public void sendMessage(List<String> recipients, String messageText)
|
||||
public void sendMessage(List<String> recipients, String messageText, int type)
|
||||
throws IOException
|
||||
{
|
||||
OutgoingPushMessage message = new OutgoingPushMessage(recipients, messageText);
|
||||
OutgoingPushMessage message = new OutgoingPushMessage(recipients, messageText, type);
|
||||
sendMessage(message);
|
||||
}
|
||||
|
||||
public void sendMessage(List<String> recipients, String messageText,
|
||||
List<PushAttachmentData> attachments)
|
||||
List<PushAttachmentData> attachments, int type)
|
||||
throws IOException
|
||||
{
|
||||
List<PushAttachmentPointer> attachmentIds = sendAttachments(attachments);
|
||||
OutgoingPushMessage message = new OutgoingPushMessage(recipients, messageText, attachmentIds);
|
||||
OutgoingPushMessage message = new OutgoingPushMessage(recipients, messageText, attachmentIds, type);
|
||||
sendMessage(message);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user