mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-23 18:15:22 +00:00
Remove V1 code.
This commit is contained in:
parent
ca8c950553
commit
1d07ca3e6f
@ -23,7 +23,6 @@ import android.os.Parcelable;
|
|||||||
import org.whispersystems.textsecure.crypto.ecc.Curve;
|
import org.whispersystems.textsecure.crypto.ecc.Curve;
|
||||||
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
|
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
|
||||||
import org.whispersystems.textsecure.util.Hex;
|
import org.whispersystems.textsecure.util.Hex;
|
||||||
import org.whispersystems.textsecure.util.Util;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A class for representing an identity key.
|
* A class for representing an identity key.
|
||||||
@ -80,15 +79,8 @@ public class IdentityKey implements Parcelable, SerializableKey {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public byte[] serialize() {
|
public byte[] serialize() {
|
||||||
if (publicKey.getType() == Curve.NIST_TYPE) {
|
|
||||||
byte[] versionBytes = {0x01};
|
|
||||||
byte[] encodedKey = publicKey.serialize();
|
|
||||||
|
|
||||||
return Util.combine(versionBytes, encodedKey);
|
|
||||||
} else {
|
|
||||||
return publicKey.serialize();
|
return publicKey.serialize();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public String getFingerprint() {
|
public String getFingerprint() {
|
||||||
return Hex.toString(publicKey.serialize());
|
return Hex.toString(publicKey.serialize());
|
||||||
|
@ -1,81 +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.util.Log;
|
|
||||||
|
|
||||||
import org.whispersystems.textsecure.crypto.ecc.ECKeyPair;
|
|
||||||
import org.whispersystems.textsecure.crypto.ecc.ECPrivateKey;
|
|
||||||
import org.whispersystems.textsecure.util.Hex;
|
|
||||||
import org.whispersystems.textsecure.util.Util;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a session's active KeyPair.
|
|
||||||
*
|
|
||||||
* @author Moxie Marlinspike
|
|
||||||
*/
|
|
||||||
|
|
||||||
public class KeyPair {
|
|
||||||
|
|
||||||
private PublicKey publicKey;
|
|
||||||
private ECPrivateKey privateKey;
|
|
||||||
|
|
||||||
private final MasterCipher masterCipher;
|
|
||||||
|
|
||||||
public KeyPair(int keyPairId, ECKeyPair keyPair, MasterSecret masterSecret) {
|
|
||||||
this.masterCipher = new MasterCipher(masterSecret);
|
|
||||||
this.publicKey = new PublicKey(keyPairId, keyPair.getPublicKey());
|
|
||||||
this.privateKey = keyPair.getPrivateKey();
|
|
||||||
}
|
|
||||||
|
|
||||||
public KeyPair(byte[] bytes, MasterCipher masterCipher) throws InvalidKeyException {
|
|
||||||
this.masterCipher = masterCipher;
|
|
||||||
deserialize(bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getId() {
|
|
||||||
return publicKey.getId();
|
|
||||||
}
|
|
||||||
|
|
||||||
public PublicKey getPublicKey() {
|
|
||||||
return publicKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ECPrivateKey getPrivateKey() {
|
|
||||||
return privateKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] toBytes() {
|
|
||||||
return serialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void deserialize(byte[] bytes) throws InvalidKeyException {
|
|
||||||
this.publicKey = new PublicKey(bytes);
|
|
||||||
byte[] privateKeyBytes = new byte[bytes.length - PublicKey.KEY_SIZE];
|
|
||||||
System.arraycopy(bytes, PublicKey.KEY_SIZE, privateKeyBytes, 0, privateKeyBytes.length);
|
|
||||||
this.privateKey = masterCipher.decryptKey(this.publicKey.getType(), privateKeyBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] serialize() {
|
|
||||||
byte[] publicKeyBytes = publicKey.serialize();
|
|
||||||
Log.w("KeyPair", "Serialized public key bytes: " + Hex.toString(publicKeyBytes));
|
|
||||||
byte[] privateKeyBytes = masterCipher.encryptKey(privateKey);
|
|
||||||
return Util.combine(publicKeyBytes, privateKeyBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,7 @@
|
|||||||
|
package org.whispersystems.textsecure.crypto;
|
||||||
|
|
||||||
|
public class LegacyMessageException extends Exception {
|
||||||
|
public LegacyMessageException(String s) {
|
||||||
|
super(s);
|
||||||
|
}
|
||||||
|
}
|
@ -83,11 +83,11 @@ public class MasterCipher {
|
|||||||
return new String(decodeAndDecryptBytes(body));
|
return new String(decodeAndDecryptBytes(body));
|
||||||
}
|
}
|
||||||
|
|
||||||
public ECPrivateKey decryptKey(int type, byte[] key)
|
public ECPrivateKey decryptKey(byte[] key)
|
||||||
throws org.whispersystems.textsecure.crypto.InvalidKeyException
|
throws org.whispersystems.textsecure.crypto.InvalidKeyException
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
return Curve.decodePrivatePoint(type, decryptBytes(key));
|
return Curve.decodePrivatePoint(decryptBytes(key));
|
||||||
} catch (InvalidMessageException ime) {
|
} catch (InvalidMessageException ime) {
|
||||||
throw new org.whispersystems.textsecure.crypto.InvalidKeyException(ime);
|
throw new org.whispersystems.textsecure.crypto.InvalidKeyException(ime);
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ public abstract class SessionCipher {
|
|||||||
protected static final Object SESSION_LOCK = new Object();
|
protected static final Object SESSION_LOCK = new Object();
|
||||||
|
|
||||||
public abstract CiphertextMessage encrypt(byte[] paddedMessage);
|
public abstract CiphertextMessage encrypt(byte[] paddedMessage);
|
||||||
public abstract byte[] decrypt(byte[] decodedMessage) throws InvalidMessageException, DuplicateMessageException;
|
public abstract byte[] decrypt(byte[] decodedMessage) throws InvalidMessageException, DuplicateMessageException, LegacyMessageException;
|
||||||
public abstract int getRemoteRegistrationId();
|
public abstract int getRemoteRegistrationId();
|
||||||
|
|
||||||
public static SessionCipher createFor(Context context,
|
public static SessionCipher createFor(Context context,
|
||||||
@ -38,8 +38,6 @@ public abstract class SessionCipher {
|
|||||||
{
|
{
|
||||||
if (SessionRecordV2.hasSession(context, masterSecret, recipient)) {
|
if (SessionRecordV2.hasSession(context, masterSecret, recipient)) {
|
||||||
return new SessionCipherV2(context, masterSecret, recipient);
|
return new SessionCipherV2(context, masterSecret, recipient);
|
||||||
} else if (SessionRecordV1.hasSession(context, recipient.getRecipientId())) {
|
|
||||||
return new SessionCipherV1(context, masterSecret, recipient.getRecipient());
|
|
||||||
} else {
|
} else {
|
||||||
throw new AssertionError("Attempt to initialize cipher for non-existing session.");
|
throw new AssertionError("Attempt to initialize cipher for non-existing session.");
|
||||||
}
|
}
|
||||||
|
@ -1,332 +0,0 @@
|
|||||||
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.CanonicalRecipient;
|
|
||||||
import org.whispersystems.textsecure.storage.RecipientDevice;
|
|
||||||
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 CanonicalRecipient recipient;
|
|
||||||
|
|
||||||
public SessionCipherV1(Context context,
|
|
||||||
MasterSecret masterSecret,
|
|
||||||
CanonicalRecipient 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getRemoteRegistrationId() {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
CanonicalRecipient 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -78,7 +78,7 @@ public class SessionCipherV2 extends SessionCipher {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public byte[] decrypt(byte[] decodedMessage)
|
public byte[] decrypt(byte[] decodedMessage)
|
||||||
throws InvalidMessageException, DuplicateMessageException
|
throws InvalidMessageException, DuplicateMessageException, LegacyMessageException
|
||||||
{
|
{
|
||||||
synchronized (SESSION_LOCK) {
|
synchronized (SESSION_LOCK) {
|
||||||
SessionRecordV2 sessionRecord = getSessionRecord();
|
SessionRecordV2 sessionRecord = getSessionRecord();
|
||||||
@ -111,7 +111,7 @@ public class SessionCipherV2 extends SessionCipher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public byte[] decrypt(SessionState sessionState, byte[] decodedMessage)
|
public byte[] decrypt(SessionState sessionState, byte[] decodedMessage)
|
||||||
throws InvalidMessageException, DuplicateMessageException
|
throws InvalidMessageException, DuplicateMessageException, LegacyMessageException
|
||||||
{
|
{
|
||||||
if (!sessionState.hasSenderChain()) {
|
if (!sessionState.hasSenderChain()) {
|
||||||
throw new InvalidMessageException("Uninitialized session!");
|
throw new InvalidMessageException("Uninitialized session!");
|
||||||
@ -152,7 +152,7 @@ public class SessionCipherV2 extends SessionCipher {
|
|||||||
RootKey rootKey = sessionState.getRootKey();
|
RootKey rootKey = sessionState.getRootKey();
|
||||||
ECKeyPair ourEphemeral = sessionState.getSenderEphemeralPair();
|
ECKeyPair ourEphemeral = sessionState.getSenderEphemeralPair();
|
||||||
Pair<RootKey, ChainKey> receiverChain = rootKey.createChain(theirEphemeral, ourEphemeral);
|
Pair<RootKey, ChainKey> receiverChain = rootKey.createChain(theirEphemeral, ourEphemeral);
|
||||||
ECKeyPair ourNewEphemeral = Curve.generateKeyPairForType(Curve.DJB_TYPE, true);
|
ECKeyPair ourNewEphemeral = Curve.generateKeyPair(true);
|
||||||
Pair<RootKey, ChainKey> senderChain = receiverChain.first.createChain(theirEphemeral, ourNewEphemeral);
|
Pair<RootKey, ChainKey> senderChain = receiverChain.first.createChain(theirEphemeral, ourNewEphemeral);
|
||||||
|
|
||||||
sessionState.setRootKey(senderChain.first);
|
sessionState.setRootKey(senderChain.first);
|
||||||
|
@ -21,26 +21,10 @@ import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
|
|||||||
|
|
||||||
public class Curve {
|
public class Curve {
|
||||||
|
|
||||||
public static final int NIST_TYPE = 0x02;
|
|
||||||
private static final int NIST_TYPE2 = 0x03;
|
|
||||||
public static final int DJB_TYPE = 0x05;
|
public static final int DJB_TYPE = 0x05;
|
||||||
|
|
||||||
public static ECKeyPair generateKeyPairForType(int keyType, boolean ephemeral) {
|
public static ECKeyPair generateKeyPair(boolean ephemeral) {
|
||||||
if (keyType == DJB_TYPE) {
|
|
||||||
return Curve25519.generateKeyPair(ephemeral);
|
return Curve25519.generateKeyPair(ephemeral);
|
||||||
} else if (keyType == NIST_TYPE || keyType == NIST_TYPE2) {
|
|
||||||
return CurveP256.generateKeyPair();
|
|
||||||
} else {
|
|
||||||
throw new AssertionError("Bad key type: " + keyType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ECKeyPair generateKeyPairForSession(int messageVersion, boolean ephemeral) {
|
|
||||||
if (messageVersion <= CiphertextMessage.LEGACY_VERSION) {
|
|
||||||
return generateKeyPairForType(NIST_TYPE, ephemeral);
|
|
||||||
} else {
|
|
||||||
return generateKeyPairForType(DJB_TYPE, ephemeral);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ECPublicKey decodePoint(byte[] bytes, int offset)
|
public static ECPublicKey decodePoint(byte[] bytes, int offset)
|
||||||
@ -50,21 +34,13 @@ public class Curve {
|
|||||||
|
|
||||||
if (type == DJB_TYPE) {
|
if (type == DJB_TYPE) {
|
||||||
return Curve25519.decodePoint(bytes, offset);
|
return Curve25519.decodePoint(bytes, offset);
|
||||||
} else if (type == NIST_TYPE || type == NIST_TYPE2) {
|
|
||||||
return CurveP256.decodePoint(bytes, offset);
|
|
||||||
} else {
|
} else {
|
||||||
throw new InvalidKeyException("Unknown key type: " + type);
|
throw new InvalidKeyException("Unknown key type: " + type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ECPrivateKey decodePrivatePoint(int type, byte[] bytes) {
|
public static ECPrivateKey decodePrivatePoint(byte[] bytes) {
|
||||||
if (type == DJB_TYPE) {
|
|
||||||
return new DjbECPrivateKey(bytes);
|
return new DjbECPrivateKey(bytes);
|
||||||
} else if (type == NIST_TYPE || type == NIST_TYPE2) {
|
|
||||||
return CurveP256.decodePrivatePoint(bytes);
|
|
||||||
} else {
|
|
||||||
throw new AssertionError("Bad key type: " + type);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static byte[] calculateAgreement(ECPublicKey publicKey, ECPrivateKey privateKey)
|
public static byte[] calculateAgreement(ECPublicKey publicKey, ECPrivateKey privateKey)
|
||||||
@ -76,8 +52,6 @@ public class Curve {
|
|||||||
|
|
||||||
if (publicKey.getType() == DJB_TYPE) {
|
if (publicKey.getType() == DJB_TYPE) {
|
||||||
return Curve25519.calculateAgreement(publicKey, privateKey);
|
return Curve25519.calculateAgreement(publicKey, privateKey);
|
||||||
} else if (publicKey.getType() == NIST_TYPE || publicKey.getType() == NIST_TYPE2) {
|
|
||||||
return CurveP256.calculateAgreement(publicKey, privateKey);
|
|
||||||
} else {
|
} else {
|
||||||
throw new InvalidKeyException("Unknown type: " + publicKey.getType());
|
throw new InvalidKeyException("Unknown type: " + publicKey.getType());
|
||||||
}
|
}
|
||||||
|
@ -1,122 +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.ecc;
|
|
||||||
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import org.spongycastle.crypto.AsymmetricCipherKeyPair;
|
|
||||||
import org.spongycastle.crypto.agreement.ECDHBasicAgreement;
|
|
||||||
import org.spongycastle.crypto.generators.ECKeyPairGenerator;
|
|
||||||
import org.spongycastle.crypto.params.ECDomainParameters;
|
|
||||||
import org.spongycastle.crypto.params.ECKeyGenerationParameters;
|
|
||||||
import org.spongycastle.crypto.params.ECPrivateKeyParameters;
|
|
||||||
import org.spongycastle.crypto.params.ECPublicKeyParameters;
|
|
||||||
import org.spongycastle.math.ec.ECCurve;
|
|
||||||
import org.spongycastle.math.ec.ECFieldElement;
|
|
||||||
import org.spongycastle.math.ec.ECPoint;
|
|
||||||
import org.whispersystems.textsecure.crypto.InvalidKeyException;
|
|
||||||
|
|
||||||
import java.math.BigInteger;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
|
|
||||||
public class CurveP256 {
|
|
||||||
|
|
||||||
private static final BigInteger q = new BigInteger("FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF", 16);
|
|
||||||
private static final BigInteger a = new BigInteger("FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC", 16);
|
|
||||||
private static final BigInteger b = new BigInteger("5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B", 16);
|
|
||||||
private static final BigInteger n = new BigInteger("FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551", 16);
|
|
||||||
|
|
||||||
private static final ECFieldElement x = new ECFieldElement.Fp(q, new BigInteger("6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296", 16));
|
|
||||||
private static final ECFieldElement y = new ECFieldElement.Fp(q, new BigInteger("4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5", 16));
|
|
||||||
|
|
||||||
private static final ECCurve curve = new ECCurve.Fp(q, a, b);
|
|
||||||
private static final ECPoint g = new ECPoint.Fp(curve, x, y, true);
|
|
||||||
|
|
||||||
private static final ECDomainParameters domainParameters = new ECDomainParameters(curve, g, n);
|
|
||||||
|
|
||||||
public static final int P256_POINT_SIZE = 33;
|
|
||||||
|
|
||||||
static byte[] encodePoint(ECPoint point) {
|
|
||||||
synchronized (curve) {
|
|
||||||
return point.getEncoded();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static ECPublicKey decodePoint(byte[] encoded, int offset)
|
|
||||||
throws InvalidKeyException
|
|
||||||
{
|
|
||||||
byte[] pointBytes = new byte[P256_POINT_SIZE];
|
|
||||||
System.arraycopy(encoded, offset, pointBytes, 0, pointBytes.length);
|
|
||||||
|
|
||||||
synchronized (curve) {
|
|
||||||
ECPoint Q;
|
|
||||||
|
|
||||||
try {
|
|
||||||
Q = curve.decodePoint(pointBytes);
|
|
||||||
} catch (RuntimeException re) {
|
|
||||||
throw new InvalidKeyException(re);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new NistECPublicKey(new ECPublicKeyParameters(Q, domainParameters));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static ECPrivateKey decodePrivatePoint(byte[] encoded) {
|
|
||||||
BigInteger d = new BigInteger(encoded);
|
|
||||||
return new NistECPrivateKey(new ECPrivateKeyParameters(d, domainParameters));
|
|
||||||
}
|
|
||||||
|
|
||||||
static byte[] calculateAgreement(ECPublicKey publicKey, ECPrivateKey privateKey) {
|
|
||||||
ECDHBasicAgreement agreement = new ECDHBasicAgreement();
|
|
||||||
agreement.init(((NistECPrivateKey)privateKey).getParameters());
|
|
||||||
|
|
||||||
synchronized (curve) {
|
|
||||||
return agreement.calculateAgreement(((NistECPublicKey)publicKey).getParameters()).toByteArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ECKeyPair generateKeyPair() {
|
|
||||||
try {
|
|
||||||
synchronized (curve) {
|
|
||||||
ECKeyGenerationParameters keyParamters = new ECKeyGenerationParameters(domainParameters, SecureRandom.getInstance("SHA1PRNG"));
|
|
||||||
ECKeyPairGenerator generator = new ECKeyPairGenerator();
|
|
||||||
generator.init(keyParamters);
|
|
||||||
|
|
||||||
AsymmetricCipherKeyPair keyPair = generator.generateKeyPair();
|
|
||||||
keyPair = cloneKeyPairWithPointCompression(keyPair);
|
|
||||||
|
|
||||||
return new ECKeyPair(new NistECPublicKey((ECPublicKeyParameters)keyPair.getPublic()),
|
|
||||||
new NistECPrivateKey((ECPrivateKeyParameters)keyPair.getPrivate()));
|
|
||||||
}
|
|
||||||
} catch (NoSuchAlgorithmException nsae) {
|
|
||||||
Log.w("CurveP256", nsae);
|
|
||||||
throw new AssertionError(nsae);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is dumb, but the ECPublicKeys that the generator makes by default don't have point compression
|
|
||||||
// turned on, and there's no setter. Great.
|
|
||||||
private static AsymmetricCipherKeyPair cloneKeyPairWithPointCompression(AsymmetricCipherKeyPair keyPair) {
|
|
||||||
ECPublicKeyParameters publicKey = (ECPublicKeyParameters)keyPair.getPublic();
|
|
||||||
ECPoint q = publicKey.getQ();
|
|
||||||
|
|
||||||
return new AsymmetricCipherKeyPair(new ECPublicKeyParameters(new ECPoint.Fp(q.getCurve(), q.getX(), q.getY(), true),
|
|
||||||
publicKey.getParameters()), keyPair.getPrivate());
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,43 +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.ecc;
|
|
||||||
|
|
||||||
import org.spongycastle.crypto.params.ECPrivateKeyParameters;
|
|
||||||
|
|
||||||
public class NistECPrivateKey implements ECPrivateKey {
|
|
||||||
|
|
||||||
private final ECPrivateKeyParameters privateKey;
|
|
||||||
|
|
||||||
public NistECPrivateKey(ECPrivateKeyParameters privateKey) {
|
|
||||||
this.privateKey = privateKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public byte[] serialize() {
|
|
||||||
return privateKey.getD().toByteArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getType() {
|
|
||||||
return Curve.NIST_TYPE;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ECPrivateKeyParameters getParameters() {
|
|
||||||
return privateKey;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,63 +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.ecc;
|
|
||||||
|
|
||||||
import org.spongycastle.crypto.params.ECPublicKeyParameters;
|
|
||||||
|
|
||||||
public class NistECPublicKey implements ECPublicKey {
|
|
||||||
|
|
||||||
private final ECPublicKeyParameters publicKey;
|
|
||||||
|
|
||||||
NistECPublicKey(ECPublicKeyParameters publicKey) {
|
|
||||||
this.publicKey = publicKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public byte[] serialize() {
|
|
||||||
return CurveP256.encodePoint(publicKey.getQ());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getType() {
|
|
||||||
return Curve.NIST_TYPE;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object other) {
|
|
||||||
if (other == null) return false;
|
|
||||||
if (!(other instanceof NistECPublicKey)) return false;
|
|
||||||
|
|
||||||
NistECPublicKey that = (NistECPublicKey)other;
|
|
||||||
return publicKey.getQ().equals(that.publicKey.getQ());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int hashCode() {
|
|
||||||
return publicKey.getQ().hashCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int compareTo(ECPublicKey another) {
|
|
||||||
return publicKey.getQ().getX().toBigInteger()
|
|
||||||
.compareTo(((NistECPublicKey) another).publicKey.getQ().getX().toBigInteger());
|
|
||||||
}
|
|
||||||
|
|
||||||
public ECPublicKeyParameters getParameters() {
|
|
||||||
return publicKey;
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,15 +2,14 @@ package org.whispersystems.textsecure.crypto.protocol;
|
|||||||
|
|
||||||
public interface CiphertextMessage {
|
public interface CiphertextMessage {
|
||||||
|
|
||||||
public static final int LEGACY_VERSION = 1;
|
public static final int UNSUPPORTED_VERSION = 1;
|
||||||
public static final int CURRENT_VERSION = 2;
|
public static final int CURRENT_VERSION = 2;
|
||||||
|
|
||||||
public static final int LEGACY_WHISPER_TYPE = 1;
|
public static final int WHISPER_TYPE = 2;
|
||||||
public static final int CURRENT_WHISPER_TYPE = 2;
|
public static final int PREKEY_TYPE = 3;
|
||||||
public static final int PREKEY_WHISPER_TYPE = 3;
|
|
||||||
|
|
||||||
// This should be the worst case (worse than V2). So not always accurate, but good enough for padding.
|
// 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;
|
public static final int ENCRYPTED_MESSAGE_OVERHEAD = 53;
|
||||||
|
|
||||||
public byte[] serialize();
|
public byte[] serialize();
|
||||||
public int getType();
|
public int getType();
|
||||||
|
@ -7,6 +7,7 @@ import org.whispersystems.textsecure.crypto.IdentityKey;
|
|||||||
import org.whispersystems.textsecure.crypto.InvalidKeyException;
|
import org.whispersystems.textsecure.crypto.InvalidKeyException;
|
||||||
import org.whispersystems.textsecure.crypto.InvalidMessageException;
|
import org.whispersystems.textsecure.crypto.InvalidMessageException;
|
||||||
import org.whispersystems.textsecure.crypto.InvalidVersionException;
|
import org.whispersystems.textsecure.crypto.InvalidVersionException;
|
||||||
|
import org.whispersystems.textsecure.crypto.LegacyMessageException;
|
||||||
import org.whispersystems.textsecure.crypto.ecc.Curve;
|
import org.whispersystems.textsecure.crypto.ecc.Curve;
|
||||||
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
|
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
|
||||||
import org.whispersystems.textsecure.util.Conversions;
|
import org.whispersystems.textsecure.util.Conversions;
|
||||||
@ -26,7 +27,7 @@ public class PreKeyWhisperMessage implements CiphertextMessage {
|
|||||||
throws InvalidMessageException, InvalidVersionException
|
throws InvalidMessageException, InvalidVersionException
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
this.version = Conversions.lowBitsToInt(serialized[0]);
|
this.version = Conversions.highBitsToInt(serialized[0]);
|
||||||
|
|
||||||
if (this.version > CiphertextMessage.CURRENT_VERSION) {
|
if (this.version > CiphertextMessage.CURRENT_VERSION) {
|
||||||
throw new InvalidVersionException("Unknown version: " + this.version);
|
throw new InvalidVersionException("Unknown version: " + this.version);
|
||||||
@ -54,6 +55,8 @@ public class PreKeyWhisperMessage implements CiphertextMessage {
|
|||||||
throw new InvalidMessageException(e);
|
throw new InvalidMessageException(e);
|
||||||
} catch (InvalidKeyException e) {
|
} catch (InvalidKeyException e) {
|
||||||
throw new InvalidMessageException(e);
|
throw new InvalidMessageException(e);
|
||||||
|
} catch (LegacyMessageException e) {
|
||||||
|
throw new InvalidMessageException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,7 +109,7 @@ public class PreKeyWhisperMessage implements CiphertextMessage {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getType() {
|
public int getType() {
|
||||||
return CiphertextMessage.PREKEY_WHISPER_TYPE;
|
return CiphertextMessage.PREKEY_TYPE;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,187 +0,0 @@
|
|||||||
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.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -7,6 +7,7 @@ import com.google.protobuf.InvalidProtocolBufferException;
|
|||||||
|
|
||||||
import org.whispersystems.textsecure.crypto.InvalidKeyException;
|
import org.whispersystems.textsecure.crypto.InvalidKeyException;
|
||||||
import org.whispersystems.textsecure.crypto.InvalidMessageException;
|
import org.whispersystems.textsecure.crypto.InvalidMessageException;
|
||||||
|
import org.whispersystems.textsecure.crypto.LegacyMessageException;
|
||||||
import org.whispersystems.textsecure.crypto.ecc.Curve;
|
import org.whispersystems.textsecure.crypto.ecc.Curve;
|
||||||
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
|
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
|
||||||
import org.whispersystems.textsecure.crypto.protocol.WhisperProtos.WhisperMessage;
|
import org.whispersystems.textsecure.crypto.protocol.WhisperProtos.WhisperMessage;
|
||||||
@ -31,13 +32,17 @@ public class WhisperMessageV2 implements CiphertextMessage {
|
|||||||
private final byte[] ciphertext;
|
private final byte[] ciphertext;
|
||||||
private final byte[] serialized;
|
private final byte[] serialized;
|
||||||
|
|
||||||
public WhisperMessageV2(byte[] serialized) throws InvalidMessageException {
|
public WhisperMessageV2(byte[] serialized) throws InvalidMessageException, LegacyMessageException {
|
||||||
try {
|
try {
|
||||||
byte[][] messageParts = Util.split(serialized, 1, serialized.length - 1 - MAC_LENGTH, MAC_LENGTH);
|
byte[][] messageParts = Util.split(serialized, 1, serialized.length - 1 - MAC_LENGTH, MAC_LENGTH);
|
||||||
byte version = messageParts[0][0];
|
byte version = messageParts[0][0];
|
||||||
byte[] message = messageParts[1];
|
byte[] message = messageParts[1];
|
||||||
byte[] mac = messageParts[2];
|
byte[] mac = messageParts[2];
|
||||||
|
|
||||||
|
if (Conversions.highBitsToInt(version) <= CiphertextMessage.UNSUPPORTED_VERSION) {
|
||||||
|
throw new LegacyMessageException("Legacy message: " + Conversions.highBitsToInt(version));
|
||||||
|
}
|
||||||
|
|
||||||
if (Conversions.highBitsToInt(version) != CURRENT_VERSION) {
|
if (Conversions.highBitsToInt(version) != CURRENT_VERSION) {
|
||||||
throw new InvalidMessageException("Unknown version: " + Conversions.highBitsToInt(version));
|
throw new InvalidMessageException("Unknown version: " + Conversions.highBitsToInt(version));
|
||||||
}
|
}
|
||||||
@ -129,7 +134,7 @@ public class WhisperMessageV2 implements CiphertextMessage {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getType() {
|
public int getType() {
|
||||||
return CiphertextMessage.CURRENT_WHISPER_TYPE;
|
return CiphertextMessage.WHISPER_TYPE;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,7 @@ public class RatchetingSession {
|
|||||||
sessionState.setRemoteIdentityKey(theirIdentityKey);
|
sessionState.setRemoteIdentityKey(theirIdentityKey);
|
||||||
sessionState.setLocalIdentityKey(ourIdentityKey.getPublicKey());
|
sessionState.setLocalIdentityKey(ourIdentityKey.getPublicKey());
|
||||||
|
|
||||||
ECKeyPair sendingKey = Curve.generateKeyPairForType(ourIdentityKey.getPublicKey().getPublicKey().getType(), true);
|
ECKeyPair sendingKey = Curve.generateKeyPair(true);
|
||||||
Pair<RootKey, ChainKey> receivingChain = calculate3DHE(true, ourBaseKey, theirBaseKey, ourIdentityKey, theirIdentityKey);
|
Pair<RootKey, ChainKey> receivingChain = calculate3DHE(true, ourBaseKey, theirBaseKey, ourIdentityKey, theirIdentityKey);
|
||||||
Pair<RootKey, ChainKey> sendingChain = receivingChain.first.createChain(theirEphemeralKey, sendingKey);
|
Pair<RootKey, ChainKey> sendingChain = receivingChain.first.createChain(theirEphemeralKey, sendingKey);
|
||||||
|
|
||||||
|
@ -18,135 +18,14 @@
|
|||||||
package org.whispersystems.textsecure.storage;
|
package org.whispersystems.textsecure.storage;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import org.whispersystems.textsecure.crypto.InvalidKeyException;
|
public class LocalKeyRecord {
|
||||||
import org.whispersystems.textsecure.crypto.KeyPair;
|
|
||||||
import org.whispersystems.textsecure.crypto.MasterCipher;
|
|
||||||
import org.whispersystems.textsecure.crypto.MasterSecret;
|
|
||||||
import org.whispersystems.textsecure.crypto.ecc.Curve;
|
|
||||||
import org.whispersystems.textsecure.util.Medium;
|
|
||||||
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.RandomAccessFile;
|
|
||||||
import java.nio.channels.FileChannel;
|
|
||||||
|
|
||||||
public class LocalKeyRecord extends Record {
|
|
||||||
|
|
||||||
private static final Object FILE_LOCK = new Object();
|
|
||||||
|
|
||||||
private KeyPair localCurrentKeyPair;
|
|
||||||
private KeyPair localNextKeyPair;
|
|
||||||
|
|
||||||
private final MasterCipher masterCipher;
|
|
||||||
private final MasterSecret masterSecret;
|
|
||||||
|
|
||||||
public LocalKeyRecord(Context context, MasterSecret masterSecret, CanonicalRecipient recipient) {
|
|
||||||
super(context, SESSIONS_DIRECTORY, getFileNameForRecipient(recipient));
|
|
||||||
this.masterSecret = masterSecret;
|
|
||||||
this.masterCipher = new MasterCipher(masterSecret);
|
|
||||||
loadData();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean hasRecord(Context context, CanonicalRecipient recipient) {
|
|
||||||
Log.w("LocalKeyRecord", "Checking: " + getFileNameForRecipient(recipient));
|
|
||||||
return Record.hasRecord(context, SESSIONS_DIRECTORY, getFileNameForRecipient(recipient));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void delete(Context context, CanonicalRecipient recipient) {
|
public static void delete(Context context, CanonicalRecipient recipient) {
|
||||||
Record.delete(context, SESSIONS_DIRECTORY, getFileNameForRecipient(recipient));
|
Record.delete(context, Record.SESSIONS_DIRECTORY, getFileNameForRecipient(recipient));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String getFileNameForRecipient(CanonicalRecipient recipient) {
|
private static String getFileNameForRecipient(CanonicalRecipient recipient) {
|
||||||
return recipient.getRecipientId() + "-local";
|
return recipient.getRecipientId() + "-local";
|
||||||
}
|
}
|
||||||
|
|
||||||
public void advanceKeyIfNecessary(int keyId) {
|
|
||||||
Log.w("LocalKeyRecord", "Remote client acknowledges receiving key id: " + keyId);
|
|
||||||
if (keyId == localNextKeyPair.getId()) {
|
|
||||||
int keyType = this.localNextKeyPair.getPublicKey().getType();
|
|
||||||
|
|
||||||
this.localCurrentKeyPair = this.localNextKeyPair;
|
|
||||||
this.localNextKeyPair = new KeyPair((this.localNextKeyPair.getId()+1) % Medium.MAX_VALUE,
|
|
||||||
Curve.generateKeyPairForType(keyType, true),
|
|
||||||
masterSecret);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCurrentKeyPair(KeyPair localCurrentKeyPair) {
|
|
||||||
this.localCurrentKeyPair = localCurrentKeyPair;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setNextKeyPair(KeyPair localNextKeyPair) {
|
|
||||||
this.localNextKeyPair = localNextKeyPair;
|
|
||||||
}
|
|
||||||
|
|
||||||
public KeyPair getCurrentKeyPair() {
|
|
||||||
return this.localCurrentKeyPair;
|
|
||||||
}
|
|
||||||
|
|
||||||
public KeyPair getNextKeyPair() {
|
|
||||||
return this.localNextKeyPair;
|
|
||||||
}
|
|
||||||
|
|
||||||
public KeyPair getKeyPairForId(int id) throws InvalidKeyIdException {
|
|
||||||
if (this.localCurrentKeyPair.getId() == id) return this.localCurrentKeyPair;
|
|
||||||
else if (this.localNextKeyPair.getId() == id) return this.localNextKeyPair;
|
|
||||||
else throw new InvalidKeyIdException("No local key for ID: " + id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void save() {
|
|
||||||
synchronized (FILE_LOCK) {
|
|
||||||
try {
|
|
||||||
RandomAccessFile file = openRandomAccessFile();
|
|
||||||
FileChannel out = file.getChannel();
|
|
||||||
out.position(0);
|
|
||||||
|
|
||||||
writeKeyPair(localCurrentKeyPair, out);
|
|
||||||
writeKeyPair(localNextKeyPair, out);
|
|
||||||
|
|
||||||
out.force(true);
|
|
||||||
out.truncate(out.position());
|
|
||||||
out.close();
|
|
||||||
file.close();
|
|
||||||
} catch (IOException ioe) {
|
|
||||||
Log.w("keyrecord", ioe);
|
|
||||||
// XXX
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadData() {
|
|
||||||
Log.w("LocalKeyRecord", "Loading local key record...");
|
|
||||||
synchronized (FILE_LOCK) {
|
|
||||||
try {
|
|
||||||
FileInputStream in = this.openInputStream();
|
|
||||||
localCurrentKeyPair = readKeyPair(in, masterCipher);
|
|
||||||
localNextKeyPair = readKeyPair(in, masterCipher);
|
|
||||||
in.close();
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
Log.w("LocalKeyRecord", "No local keypair set found.");
|
|
||||||
} catch (IOException ioe) {
|
|
||||||
Log.w("keyrecord", ioe);
|
|
||||||
// XXX
|
|
||||||
} catch (InvalidKeyException ike) {
|
|
||||||
Log.w("LocalKeyRecord", ike);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void writeKeyPair(KeyPair keyPair, FileChannel out) throws IOException {
|
|
||||||
byte[] keyPairBytes = keyPair.toBytes();
|
|
||||||
writeBlob(keyPairBytes, out);
|
|
||||||
}
|
|
||||||
|
|
||||||
private KeyPair readKeyPair(FileInputStream in, MasterCipher masterCipher)
|
|
||||||
throws IOException, InvalidKeyException
|
|
||||||
{
|
|
||||||
byte[] keyPairBytes = readBlob(in);
|
|
||||||
return new KeyPair(keyPairBytes, masterCipher);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -60,7 +60,7 @@ public class PreKeyRecord extends Record {
|
|||||||
public ECKeyPair getKeyPair() {
|
public ECKeyPair getKeyPair() {
|
||||||
try {
|
try {
|
||||||
ECPublicKey publicKey = Curve.decodePoint(this.structure.getPublicKey().toByteArray(), 0);
|
ECPublicKey publicKey = Curve.decodePoint(this.structure.getPublicKey().toByteArray(), 0);
|
||||||
ECPrivateKey privateKey = Curve.decodePrivatePoint(publicKey.getType(), this.structure.getPrivateKey().toByteArray());
|
ECPrivateKey privateKey = Curve.decodePrivatePoint(this.structure.getPrivateKey().toByteArray());
|
||||||
|
|
||||||
return new ECKeyPair(publicKey, privateKey);
|
return new ECKeyPair(publicKey, privateKey);
|
||||||
} catch (InvalidKeyException e) {
|
} catch (InvalidKeyException e) {
|
||||||
|
@ -37,126 +37,13 @@ import java.nio.channels.FileChannel;
|
|||||||
* @author Moxie Marlinspike
|
* @author Moxie Marlinspike
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public class RemoteKeyRecord extends Record {
|
public class RemoteKeyRecord {
|
||||||
private static final Object FILE_LOCK = new Object();
|
|
||||||
|
|
||||||
private PublicKey remoteKeyCurrent;
|
|
||||||
private PublicKey remoteKeyLast;
|
|
||||||
|
|
||||||
public RemoteKeyRecord(Context context, CanonicalRecipient recipient) {
|
|
||||||
super(context, SESSIONS_DIRECTORY, getFileNameForRecipient(recipient));
|
|
||||||
loadData();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void delete(Context context, CanonicalRecipient recipient) {
|
public static void delete(Context context, CanonicalRecipient recipient) {
|
||||||
delete(context, SESSIONS_DIRECTORY, getFileNameForRecipient(recipient));
|
Record.delete(context, Record.SESSIONS_DIRECTORY, getFileNameForRecipient(recipient));
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean hasRecord(Context context, CanonicalRecipient recipient) {
|
|
||||||
Log.w("LocalKeyRecord", "Checking: " + getFileNameForRecipient(recipient));
|
|
||||||
return hasRecord(context, SESSIONS_DIRECTORY, getFileNameForRecipient(recipient));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String getFileNameForRecipient(CanonicalRecipient recipient) {
|
private static String getFileNameForRecipient(CanonicalRecipient recipient) {
|
||||||
return recipient.getRecipientId() + "-remote";
|
return recipient.getRecipientId() + "-remote";
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateCurrentRemoteKey(PublicKey remoteKey) {
|
|
||||||
Log.w("RemoteKeyRecord", "Updating current remote key: " + remoteKey.getId());
|
|
||||||
if (isWrappingGreaterThan(remoteKey.getId(), remoteKeyCurrent.getId())) {
|
|
||||||
this.remoteKeyLast = this.remoteKeyCurrent;
|
|
||||||
this.remoteKeyCurrent = remoteKey;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setCurrentRemoteKey(PublicKey remoteKeyCurrent) {
|
|
||||||
this.remoteKeyCurrent = remoteKeyCurrent;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setLastRemoteKey(PublicKey remoteKeyLast) {
|
|
||||||
this.remoteKeyLast = remoteKeyLast;
|
|
||||||
}
|
|
||||||
|
|
||||||
public PublicKey getCurrentRemoteKey() {
|
|
||||||
return this.remoteKeyCurrent;
|
|
||||||
}
|
|
||||||
|
|
||||||
public PublicKey getLastRemoteKey() {
|
|
||||||
return this.remoteKeyLast;
|
|
||||||
}
|
|
||||||
|
|
||||||
public PublicKey getKeyForId(int id) throws InvalidKeyIdException {
|
|
||||||
if (this.remoteKeyCurrent.getId() == id) return this.remoteKeyCurrent;
|
|
||||||
else if (this.remoteKeyLast.getId() == id) return this.remoteKeyLast;
|
|
||||||
else throw new InvalidKeyIdException("No remote key for ID: " + id);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void save() {
|
|
||||||
Log.w("RemoteKeyRecord", "Saving remote key record for recipient: " + this.address);
|
|
||||||
synchronized (FILE_LOCK) {
|
|
||||||
try {
|
|
||||||
RandomAccessFile file = openRandomAccessFile();
|
|
||||||
FileChannel out = file.getChannel();
|
|
||||||
Log.w("RemoteKeyRecord", "Opened file of size: " + out.size());
|
|
||||||
out.position(0);
|
|
||||||
|
|
||||||
writeKey(remoteKeyCurrent, out);
|
|
||||||
writeKey(remoteKeyLast, out);
|
|
||||||
|
|
||||||
out.truncate(out.position());
|
|
||||||
out.close();
|
|
||||||
file.close();
|
|
||||||
} catch (IOException ioe) {
|
|
||||||
Log.w("keyrecord", ioe);
|
|
||||||
// XXX
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isWrappingGreaterThan(int receivedValue, int currentValue) {
|
|
||||||
if (receivedValue > currentValue) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (receivedValue == currentValue) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
int gap = (receivedValue - currentValue) + Medium.MAX_VALUE;
|
|
||||||
|
|
||||||
return (gap >= 0) && (gap < 5);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadData() {
|
|
||||||
Log.w("RemoteKeyRecord", "Loading remote key record for recipient: " + this.address);
|
|
||||||
synchronized (FILE_LOCK) {
|
|
||||||
try {
|
|
||||||
FileInputStream in = this.openInputStream();
|
|
||||||
remoteKeyCurrent = readKey(in);
|
|
||||||
remoteKeyLast = readKey(in);
|
|
||||||
in.close();
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
Log.w("RemoteKeyRecord", "No remote keys found.");
|
|
||||||
} catch (IOException ioe) {
|
|
||||||
Log.w("keyrecord", ioe);
|
|
||||||
// XXX
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void writeKey(PublicKey key, FileChannel out) throws IOException {
|
|
||||||
byte[] keyBytes = key.serialize();
|
|
||||||
Log.w("RemoteKeyRecord", "Serializing remote key bytes: " + Hex.toString(keyBytes));
|
|
||||||
writeBlob(keyBytes, out);
|
|
||||||
}
|
|
||||||
|
|
||||||
private PublicKey readKey(FileInputStream in) throws IOException {
|
|
||||||
try {
|
|
||||||
byte[] keyBytes = readBlob(in);
|
|
||||||
return new PublicKey(keyBytes);
|
|
||||||
} catch (InvalidKeyException ike) {
|
|
||||||
throw new AssertionError(ike);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ import android.util.Log;
|
|||||||
|
|
||||||
import org.whispersystems.textsecure.crypto.IdentityKey;
|
import org.whispersystems.textsecure.crypto.IdentityKey;
|
||||||
import org.whispersystems.textsecure.crypto.MasterSecret;
|
import org.whispersystems.textsecure.crypto.MasterSecret;
|
||||||
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper class for generating key pairs and calculating ECDH agreements.
|
* Helper class for generating key pairs and calculating ECDH agreements.
|
||||||
@ -32,7 +31,8 @@ public class Session {
|
|||||||
CanonicalRecipient recipient)
|
CanonicalRecipient recipient)
|
||||||
{
|
{
|
||||||
Log.w("Session", "Checking session...");
|
Log.w("Session", "Checking session...");
|
||||||
return hasV1Session(context, recipient) || hasV2Session(context, masterSecret, recipient);
|
return SessionRecordV2.hasSession(context, masterSecret, recipient.getRecipientId(),
|
||||||
|
RecipientDevice.DEFAULT_DEVICE_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean hasEncryptCapableSession(Context context,
|
public static boolean hasEncryptCapableSession(Context context,
|
||||||
@ -50,30 +50,8 @@ public class Session {
|
|||||||
CanonicalRecipient recipient,
|
CanonicalRecipient recipient,
|
||||||
RecipientDevice device)
|
RecipientDevice device)
|
||||||
{
|
{
|
||||||
return
|
return hasSession(context, masterSecret, recipient) &&
|
||||||
hasV1Session(context, recipient) ||
|
!SessionRecordV2.needsRefresh(context, masterSecret, device);
|
||||||
(hasV2Session(context, masterSecret, recipient) &&
|
|
||||||
!SessionRecordV2.needsRefresh(context, masterSecret, device));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean hasRemoteIdentityKey(Context context,
|
|
||||||
MasterSecret masterSecret,
|
|
||||||
CanonicalRecipient recipient)
|
|
||||||
{
|
|
||||||
return (hasV2Session(context, masterSecret, recipient) || (hasV1Session(context, recipient) &&
|
|
||||||
new SessionRecordV1(context, masterSecret, recipient).getIdentityKey() != null));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean hasV2Session(Context context, MasterSecret masterSecret,
|
|
||||||
CanonicalRecipient recipient)
|
|
||||||
{
|
|
||||||
return SessionRecordV2.hasSession(context, masterSecret, recipient.getRecipientId(),
|
|
||||||
RecipientDevice.DEFAULT_DEVICE_ID);
|
|
||||||
}
|
|
||||||
private static boolean hasV1Session(Context context, CanonicalRecipient recipient) {
|
|
||||||
return SessionRecordV1.hasSession(context, recipient) &&
|
|
||||||
RemoteKeyRecord.hasRecord(context, recipient) &&
|
|
||||||
LocalKeyRecord.hasRecord(context, recipient);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IdentityKey getRemoteIdentityKey(Context context, MasterSecret masterSecret,
|
public static IdentityKey getRemoteIdentityKey(Context context, MasterSecret masterSecret,
|
||||||
@ -92,25 +70,8 @@ public class Session {
|
|||||||
return new SessionRecordV2(context, masterSecret, recipientId,
|
return new SessionRecordV2(context, masterSecret, recipientId,
|
||||||
RecipientDevice.DEFAULT_DEVICE_ID).getSessionState()
|
RecipientDevice.DEFAULT_DEVICE_ID).getSessionState()
|
||||||
.getRemoteIdentityKey();
|
.getRemoteIdentityKey();
|
||||||
} else if (SessionRecordV1.hasSession(context, recipientId)) {
|
|
||||||
return new SessionRecordV1(context, masterSecret, recipientId).getIdentityKey();
|
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int getSessionVersion(Context context, MasterSecret masterSecret,
|
|
||||||
CanonicalRecipient recipient)
|
|
||||||
{
|
|
||||||
if (SessionRecordV2.hasSession(context, masterSecret,
|
|
||||||
recipient.getRecipientId(),
|
|
||||||
RecipientDevice.DEFAULT_DEVICE_ID))
|
|
||||||
{
|
|
||||||
return CiphertextMessage.CURRENT_VERSION;
|
|
||||||
} else if (SessionRecordV1.hasSession(context, recipient)) {
|
|
||||||
return new SessionRecordV1(context, masterSecret, recipient).getSessionVersion();
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,6 @@
|
|||||||
package org.whispersystems.textsecure.storage;
|
package org.whispersystems.textsecure.storage;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import org.whispersystems.textsecure.crypto.IdentityKey;
|
|
||||||
import org.whispersystems.textsecure.crypto.InvalidKeyException;
|
|
||||||
import org.whispersystems.textsecure.crypto.InvalidMessageException;
|
|
||||||
import org.whispersystems.textsecure.crypto.MasterSecret;
|
|
||||||
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.RandomAccessFile;
|
|
||||||
import java.nio.channels.FileChannel;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A disk record representing a current session.
|
* A disk record representing a current session.
|
||||||
@ -20,208 +8,8 @@ import java.nio.channels.FileChannel;
|
|||||||
* @author Moxie Marlinspike
|
* @author Moxie Marlinspike
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public class SessionRecordV1 extends Record {
|
public class SessionRecordV1 {
|
||||||
|
|
||||||
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 currentSessionVersion;
|
|
||||||
|
|
||||||
private IdentityKey identityKey;
|
|
||||||
private SessionKey sessionKeyRecord;
|
|
||||||
private boolean verifiedSessionKey;
|
|
||||||
|
|
||||||
private final MasterSecret masterSecret;
|
|
||||||
|
|
||||||
public SessionRecordV1(Context context, MasterSecret masterSecret, CanonicalRecipient recipient) {
|
|
||||||
this(context, masterSecret, recipient.getRecipientId());
|
|
||||||
}
|
|
||||||
|
|
||||||
public SessionRecordV1(Context context, MasterSecret masterSecret, long recipientId) {
|
|
||||||
super(context, SESSIONS_DIRECTORY, recipientId+"");
|
|
||||||
this.masterSecret = masterSecret;
|
|
||||||
this.currentSessionVersion = 31337;
|
|
||||||
loadData();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void delete(Context context, CanonicalRecipient recipient) {
|
public static void delete(Context context, CanonicalRecipient recipient) {
|
||||||
delete(context, SESSIONS_DIRECTORY, recipient.getRecipientId() + "");
|
Record.delete(context, Record.SESSIONS_DIRECTORY, recipient.getRecipientId() + "");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean hasSession(Context context, CanonicalRecipient recipient) {
|
|
||||||
return hasSession(context, recipient.getRecipientId());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean hasSession(Context context, long recipientId) {
|
|
||||||
Log.w("SessionRecordV1", "Checking: " + recipientId);
|
|
||||||
return hasRecord(context, SESSIONS_DIRECTORY, recipientId+"");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSessionKey(SessionKey sessionKeyRecord) {
|
|
||||||
this.sessionKeyRecord = sessionKeyRecord;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSessionId(byte[] localFingerprint, byte[] remoteFingerprint) {
|
|
||||||
this.localFingerprint = localFingerprint;
|
|
||||||
this.remoteFingerprint = remoteFingerprint;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setIdentityKey(IdentityKey identityKey) {
|
|
||||||
this.identityKey = identityKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getSessionVersion() {
|
|
||||||
return (currentSessionVersion == 31337 ? 0 : currentSessionVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSessionVersion(int sessionVersion) {
|
|
||||||
this.currentSessionVersion = sessionVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getCounter() {
|
|
||||||
return this.counter;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void incrementCounter() {
|
|
||||||
this.counter++;
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] getLocalFingerprint() {
|
|
||||||
return this.localFingerprint;
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] getRemoteFingerprint() {
|
|
||||||
return this.remoteFingerprint;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IdentityKey getIdentityKey() {
|
|
||||||
return this.identityKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isVerifiedSession() {
|
|
||||||
return this.verifiedSessionKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void writeIdentityKey(FileChannel out) throws IOException {
|
|
||||||
if (identityKey == null) writeBlob(new byte[0], out);
|
|
||||||
else writeBlob(identityKey.serialize(), out);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isValidVersionMarker(int versionMarker) {
|
|
||||||
for (int VALID_VERSION_MARKER : VALID_VERSION_MARKERS)
|
|
||||||
if (versionMarker == VALID_VERSION_MARKER)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void readIdentityKey(FileInputStream in) throws IOException {
|
|
||||||
try {
|
|
||||||
byte[] blob = readBlob(in);
|
|
||||||
|
|
||||||
if (blob.length == 0) this.identityKey = null;
|
|
||||||
else this.identityKey = new IdentityKey(blob, 0);
|
|
||||||
} catch (InvalidKeyException ike) {
|
|
||||||
throw new AssertionError(ike);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void save() {
|
|
||||||
synchronized (FILE_LOCK) {
|
|
||||||
try {
|
|
||||||
RandomAccessFile file = openRandomAccessFile();
|
|
||||||
FileChannel out = file.getChannel();
|
|
||||||
out.position(0);
|
|
||||||
|
|
||||||
writeInteger(CURRENT_VERSION_MARKER, out);
|
|
||||||
writeInteger(counter, out);
|
|
||||||
writeBlob(localFingerprint, out);
|
|
||||||
writeBlob(remoteFingerprint, out);
|
|
||||||
writeInteger(currentSessionVersion, out);
|
|
||||||
writeIdentityKey(out);
|
|
||||||
writeInteger(verifiedSessionKey ? 1 : 0, out);
|
|
||||||
|
|
||||||
if (sessionKeyRecord != null)
|
|
||||||
writeBlob(sessionKeyRecord.serialize(), 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);
|
|
||||||
|
|
||||||
// Sigh, always put a version number on everything.
|
|
||||||
if (!isValidVersionMarker(versionMarker)) {
|
|
||||||
this.counter = versionMarker;
|
|
||||||
this.localFingerprint = readBlob(in);
|
|
||||||
this.remoteFingerprint = readBlob(in);
|
|
||||||
this.currentSessionVersion = 31337;
|
|
||||||
|
|
||||||
if (in.available() != 0) {
|
|
||||||
try {
|
|
||||||
this.sessionKeyRecord = new SessionKey(readBlob(in), masterSecret);
|
|
||||||
} catch (InvalidMessageException e) {
|
|
||||||
Log.w("SessionRecord", e);
|
|
||||||
this.sessionKeyRecord = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
in.close();
|
|
||||||
} else {
|
|
||||||
this.counter = readInteger(in);
|
|
||||||
this.localFingerprint = readBlob (in);
|
|
||||||
this.remoteFingerprint = readBlob (in);
|
|
||||||
this.currentSessionVersion = readInteger(in);
|
|
||||||
|
|
||||||
if (versionMarker >= 0X55555556) {
|
|
||||||
readIdentityKey(in);
|
|
||||||
this.verifiedSessionKey = (readInteger(in) == 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (in.available() != 0) {
|
|
||||||
try {
|
|
||||||
this.sessionKeyRecord = new SessionKey(readBlob(in), masterSecret);
|
|
||||||
} catch (InvalidMessageException e) {
|
|
||||||
Log.w("SessionRecord", e);
|
|
||||||
this.sessionKeyRecord = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
in.close();
|
|
||||||
}
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
Log.w("SessionRecord", "No session information found.");
|
|
||||||
// XXX
|
|
||||||
} catch (IOException ioe) {
|
|
||||||
Log.w("keyrecord", ioe);
|
|
||||||
// XXX
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public SessionKey getSessionKey(int mode, int localKeyId, int remoteKeyId) {
|
|
||||||
if (this.sessionKeyRecord == null) return null;
|
|
||||||
|
|
||||||
if ((this.sessionKeyRecord.getLocalKeyId() == localKeyId) &&
|
|
||||||
(this.sessionKeyRecord.getRemoteKeyId() == remoteKeyId) &&
|
|
||||||
(this.sessionKeyRecord.getMode() == mode))
|
|
||||||
{
|
|
||||||
return this.sessionKeyRecord;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -122,8 +122,7 @@ public class SessionState {
|
|||||||
|
|
||||||
public ECKeyPair getSenderEphemeralPair() {
|
public ECKeyPair getSenderEphemeralPair() {
|
||||||
ECPublicKey publicKey = getSenderEphemeral();
|
ECPublicKey publicKey = getSenderEphemeral();
|
||||||
ECPrivateKey privateKey = Curve.decodePrivatePoint(publicKey.getType(),
|
ECPrivateKey privateKey = Curve.decodePrivatePoint(sessionStructure.getSenderChain()
|
||||||
sessionStructure.getSenderChain()
|
|
||||||
.getSenderEphemeralPrivate()
|
.getSenderEphemeralPrivate()
|
||||||
.toByteArray());
|
.toByteArray());
|
||||||
|
|
||||||
@ -342,8 +341,7 @@ public class SessionState {
|
|||||||
ECPublicKey publicKey = Curve.decodePoint(sessionStructure.getPendingKeyExchange()
|
ECPublicKey publicKey = Curve.decodePoint(sessionStructure.getPendingKeyExchange()
|
||||||
.getLocalBaseKey().toByteArray(), 0);
|
.getLocalBaseKey().toByteArray(), 0);
|
||||||
|
|
||||||
ECPrivateKey privateKey = Curve.decodePrivatePoint(publicKey.getType(),
|
ECPrivateKey privateKey = Curve.decodePrivatePoint(sessionStructure.getPendingKeyExchange()
|
||||||
sessionStructure.getPendingKeyExchange()
|
|
||||||
.getLocalBaseKeyPrivate()
|
.getLocalBaseKeyPrivate()
|
||||||
.toByteArray());
|
.toByteArray());
|
||||||
|
|
||||||
@ -354,8 +352,7 @@ public class SessionState {
|
|||||||
ECPublicKey publicKey = Curve.decodePoint(sessionStructure.getPendingKeyExchange()
|
ECPublicKey publicKey = Curve.decodePoint(sessionStructure.getPendingKeyExchange()
|
||||||
.getLocalEphemeralKey().toByteArray(), 0);
|
.getLocalEphemeralKey().toByteArray(), 0);
|
||||||
|
|
||||||
ECPrivateKey privateKey = Curve.decodePrivatePoint(publicKey.getType(),
|
ECPrivateKey privateKey = Curve.decodePrivatePoint(sessionStructure.getPendingKeyExchange()
|
||||||
sessionStructure.getPendingKeyExchange()
|
|
||||||
.getLocalEphemeralKeyPrivate()
|
.getLocalEphemeralKeyPrivate()
|
||||||
.toByteArray());
|
.toByteArray());
|
||||||
|
|
||||||
@ -366,8 +363,7 @@ public class SessionState {
|
|||||||
IdentityKey publicKey = new IdentityKey(sessionStructure.getPendingKeyExchange()
|
IdentityKey publicKey = new IdentityKey(sessionStructure.getPendingKeyExchange()
|
||||||
.getLocalIdentityKey().toByteArray(), 0);
|
.getLocalIdentityKey().toByteArray(), 0);
|
||||||
|
|
||||||
ECPrivateKey privateKey = Curve.decodePrivatePoint(publicKey.getPublicKey().getType(),
|
ECPrivateKey privateKey = Curve.decodePrivatePoint(sessionStructure.getPendingKeyExchange()
|
||||||
sessionStructure.getPendingKeyExchange()
|
|
||||||
.getLocalIdentityKeyPrivate()
|
.getLocalIdentityKeyPrivate()
|
||||||
.toByteArray());
|
.toByteArray());
|
||||||
|
|
||||||
|
@ -26,14 +26,13 @@ import android.view.View;
|
|||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.crypto.KeyExchangeInitiator;
|
import org.thoughtcrime.securesms.crypto.KeyExchangeInitiator;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
|
||||||
import org.whispersystems.textsecure.crypto.MasterSecret;
|
|
||||||
import org.whispersystems.textsecure.storage.LocalKeyRecord;
|
|
||||||
import org.whispersystems.textsecure.storage.RecipientDevice;
|
|
||||||
import org.whispersystems.textsecure.storage.RemoteKeyRecord;
|
|
||||||
import org.thoughtcrime.securesms.protocol.Tag;
|
import org.thoughtcrime.securesms.protocol.Tag;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.util.MemoryCleaner;
|
import org.thoughtcrime.securesms.util.MemoryCleaner;
|
||||||
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
|
import org.whispersystems.textsecure.crypto.MasterSecret;
|
||||||
|
import org.whispersystems.textsecure.storage.RecipientDevice;
|
||||||
|
import org.whispersystems.textsecure.storage.Session;
|
||||||
import org.whispersystems.textsecure.storage.SessionRecordV2;
|
import org.whispersystems.textsecure.storage.SessionRecordV2;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -114,10 +113,10 @@ public class AutoInitiateActivity extends Activity {
|
|||||||
return !sp.getBoolean("pref_thread_auto_init_exempt_" + threadId, false);
|
return !sp.getBoolean("pref_thread_auto_init_exempt_" + threadId, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isExchangeQualified(Context context, MasterSecret masterSecret, Recipient recipient) {
|
private static boolean isExchangeQualified(Context context,
|
||||||
return
|
MasterSecret masterSecret,
|
||||||
(new RemoteKeyRecord(context,recipient).getCurrentRemoteKey() == null) &&
|
Recipient recipient)
|
||||||
(new LocalKeyRecord(context, masterSecret, recipient).getCurrentKeyPair() == null) &&
|
{
|
||||||
!SessionRecordV2.hasSession(context, masterSecret, recipient.getRecipientId(), RecipientDevice.DEFAULT_DEVICE_ID);
|
return !Session.hasSession(context, masterSecret, recipient);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -163,7 +163,6 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi
|
|||||||
private long threadId;
|
private long threadId;
|
||||||
private int distributionType;
|
private int distributionType;
|
||||||
private boolean isEncryptedConversation;
|
private boolean isEncryptedConversation;
|
||||||
private boolean isAuthenticatedConversation;
|
|
||||||
private boolean isMmsEnabled = true;
|
private boolean isMmsEnabled = true;
|
||||||
private boolean isCharactersLeftViewEnabled;
|
private boolean isCharactersLeftViewEnabled;
|
||||||
|
|
||||||
@ -261,12 +260,7 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi
|
|||||||
boolean pushRegistered = TextSecurePreferences.isPushRegistered(this);
|
boolean pushRegistered = TextSecurePreferences.isPushRegistered(this);
|
||||||
|
|
||||||
if (isSingleConversation() && isEncryptedConversation) {
|
if (isSingleConversation() && isEncryptedConversation) {
|
||||||
if (isAuthenticatedConversation) {
|
|
||||||
inflater.inflate(R.menu.conversation_secure_identity, menu);
|
inflater.inflate(R.menu.conversation_secure_identity, menu);
|
||||||
} else {
|
|
||||||
inflater.inflate(R.menu.conversation_secure_no_identity, menu);
|
|
||||||
}
|
|
||||||
|
|
||||||
inflater.inflate(R.menu.conversation_secure_sms, menu.findItem(R.id.menu_security).getSubMenu());
|
inflater.inflate(R.menu.conversation_secure_sms, menu.findItem(R.id.menu_security).getSubMenu());
|
||||||
} else if (isSingleConversation() && !pushRegistered) {
|
} else if (isSingleConversation() && !pushRegistered) {
|
||||||
inflater.inflate(R.menu.conversation_insecure, menu);
|
inflater.inflate(R.menu.conversation_insecure, menu);
|
||||||
@ -671,14 +665,12 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi
|
|||||||
(isSingleConversation() && Session.hasSession(this, masterSecret, primaryRecipient)))
|
(isSingleConversation() && Session.hasSession(this, masterSecret, primaryRecipient)))
|
||||||
{
|
{
|
||||||
this.isEncryptedConversation = true;
|
this.isEncryptedConversation = true;
|
||||||
this.isAuthenticatedConversation = Session.hasRemoteIdentityKey(this, masterSecret, primaryRecipient);
|
|
||||||
this.characterCalculator = new EncryptedCharacterCalculator();
|
this.characterCalculator = new EncryptedCharacterCalculator();
|
||||||
|
|
||||||
if (isPushDestination) sendButton.setImageDrawable(drawables.getDrawable(0));
|
if (isPushDestination) sendButton.setImageDrawable(drawables.getDrawable(0));
|
||||||
else sendButton.setImageDrawable(drawables.getDrawable(1));
|
else sendButton.setImageDrawable(drawables.getDrawable(1));
|
||||||
} else {
|
} else {
|
||||||
this.isEncryptedConversation = false;
|
this.isEncryptedConversation = false;
|
||||||
this.isAuthenticatedConversation = false;
|
|
||||||
this.characterCalculator = new CharacterCalculator();
|
this.characterCalculator = new CharacterCalculator();
|
||||||
sendButton.setImageDrawable(drawables.getDrawable(2));
|
sendButton.setImageDrawable(drawables.getDrawable(2));
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,7 @@ import org.thoughtcrime.securesms.crypto.DecryptingQueue;
|
|||||||
import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor;
|
import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor;
|
||||||
import org.thoughtcrime.securesms.crypto.KeyExchangeProcessorV2;
|
import org.thoughtcrime.securesms.crypto.KeyExchangeProcessorV2;
|
||||||
import org.thoughtcrime.securesms.crypto.protocol.KeyExchangeMessage;
|
import org.thoughtcrime.securesms.crypto.protocol.KeyExchangeMessage;
|
||||||
|
import org.whispersystems.textsecure.crypto.LegacyMessageException;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.service.SendReceiveService;
|
import org.thoughtcrime.securesms.service.SendReceiveService;
|
||||||
@ -96,6 +97,8 @@ public class ReceiveKeyActivity extends Activity {
|
|||||||
Log.w("ReceiveKeyActivity", ive);
|
Log.w("ReceiveKeyActivity", ive);
|
||||||
} catch (InvalidMessageException e) {
|
} catch (InvalidMessageException e) {
|
||||||
Log.w("ReceiveKeyActivity", e);
|
Log.w("ReceiveKeyActivity", e);
|
||||||
|
} catch (LegacyMessageException e) {
|
||||||
|
Log.w("ReceiveKeyActivity", e);
|
||||||
}
|
}
|
||||||
initializeListeners();
|
initializeListeners();
|
||||||
}
|
}
|
||||||
@ -162,7 +165,8 @@ public class ReceiveKeyActivity extends Activity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void initializeKey()
|
private void initializeKey()
|
||||||
throws InvalidKeyException, InvalidVersionException, InvalidMessageException
|
throws InvalidKeyException, InvalidVersionException,
|
||||||
|
InvalidMessageException, LegacyMessageException
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
String messageBody = getIntent().getStringExtra("body");
|
String messageBody = getIntent().getStringExtra("body");
|
||||||
|
@ -28,8 +28,6 @@ import org.thoughtcrime.securesms.util.DynamicTheme;
|
|||||||
import org.thoughtcrime.securesms.util.MemoryCleaner;
|
import org.thoughtcrime.securesms.util.MemoryCleaner;
|
||||||
import org.whispersystems.textsecure.crypto.IdentityKey;
|
import org.whispersystems.textsecure.crypto.IdentityKey;
|
||||||
import org.whispersystems.textsecure.crypto.MasterSecret;
|
import org.whispersystems.textsecure.crypto.MasterSecret;
|
||||||
import org.whispersystems.textsecure.crypto.ecc.Curve;
|
|
||||||
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
|
|
||||||
import org.whispersystems.textsecure.storage.Session;
|
import org.whispersystems.textsecure.storage.Session;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -45,8 +43,6 @@ public class VerifyIdentityActivity extends KeyScanningActivity {
|
|||||||
private TextView localIdentityFingerprint;
|
private TextView localIdentityFingerprint;
|
||||||
private TextView remoteIdentityFingerprint;
|
private TextView remoteIdentityFingerprint;
|
||||||
|
|
||||||
private int keyType;
|
|
||||||
|
|
||||||
private final DynamicTheme dynamicTheme = new DynamicTheme ();
|
private final DynamicTheme dynamicTheme = new DynamicTheme ();
|
||||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||||
|
|
||||||
@ -78,12 +74,12 @@ public class VerifyIdentityActivity extends KeyScanningActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void initializeLocalIdentityKey() {
|
private void initializeLocalIdentityKey() {
|
||||||
if (!IdentityKeyUtil.hasIdentityKey(this, keyType)) {
|
if (!IdentityKeyUtil.hasIdentityKey(this)) {
|
||||||
localIdentityFingerprint.setText(R.string.VerifyIdentityActivity_you_do_not_have_an_identity_key);
|
localIdentityFingerprint.setText(R.string.VerifyIdentityActivity_you_do_not_have_an_identity_key);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
localIdentityFingerprint.setText(IdentityKeyUtil.getIdentityKey(this, keyType).getFingerprint());
|
localIdentityFingerprint.setText(IdentityKeyUtil.getIdentityKey(this).getFingerprint());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeRemoteIdentityKey() {
|
private void initializeRemoteIdentityKey() {
|
||||||
@ -110,19 +106,11 @@ public class VerifyIdentityActivity extends KeyScanningActivity {
|
|||||||
this.remoteIdentityFingerprint = (TextView)findViewById(R.id.friend_reads);
|
this.remoteIdentityFingerprint = (TextView)findViewById(R.id.friend_reads);
|
||||||
this.recipient = this.getIntent().getParcelableExtra("recipient");
|
this.recipient = this.getIntent().getParcelableExtra("recipient");
|
||||||
this.masterSecret = this.getIntent().getParcelableExtra("master_secret");
|
this.masterSecret = this.getIntent().getParcelableExtra("master_secret");
|
||||||
|
|
||||||
int sessionVersion = Session.getSessionVersion(this, masterSecret, recipient);
|
|
||||||
|
|
||||||
if (sessionVersion <= CiphertextMessage.LEGACY_VERSION) {
|
|
||||||
this.keyType = Curve.NIST_TYPE;
|
|
||||||
} else {
|
|
||||||
this.keyType = Curve.DJB_TYPE;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void initiateDisplay() {
|
protected void initiateDisplay() {
|
||||||
if (!IdentityKeyUtil.hasIdentityKey(this, keyType)) {
|
if (!IdentityKeyUtil.hasIdentityKey(this)) {
|
||||||
Toast.makeText(this,
|
Toast.makeText(this,
|
||||||
R.string.VerifyIdentityActivity_you_don_t_have_an_identity_key_exclamation,
|
R.string.VerifyIdentityActivity_you_don_t_have_an_identity_key_exclamation,
|
||||||
Toast.LENGTH_LONG).show();
|
Toast.LENGTH_LONG).show();
|
||||||
@ -161,7 +149,7 @@ public class VerifyIdentityActivity extends KeyScanningActivity {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected IdentityKey getIdentityKeyToDisplay() {
|
protected IdentityKey getIdentityKeyToDisplay() {
|
||||||
return IdentityKeyUtil.getIdentityKey(this, keyType);
|
return IdentityKeyUtil.getIdentityKey(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -27,11 +27,10 @@ import android.widget.Toast;
|
|||||||
import com.actionbarsherlock.view.Menu;
|
import com.actionbarsherlock.view.Menu;
|
||||||
import com.actionbarsherlock.view.MenuInflater;
|
import com.actionbarsherlock.view.MenuInflater;
|
||||||
import com.actionbarsherlock.view.MenuItem;
|
import com.actionbarsherlock.view.MenuItem;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||||
import org.thoughtcrime.securesms.util.Dialogs;
|
import org.thoughtcrime.securesms.util.Dialogs;
|
||||||
import org.whispersystems.textsecure.crypto.MasterSecret;
|
import org.whispersystems.textsecure.crypto.MasterSecret;
|
||||||
import org.whispersystems.textsecure.crypto.ecc.Curve;
|
|
||||||
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activity that displays the local identity key and offers the option to regenerate it.
|
* Activity that displays the local identity key and offers the option to regenerate it.
|
||||||
@ -45,7 +44,7 @@ public class ViewLocalIdentityActivity extends ViewIdentityActivity {
|
|||||||
public void onCreate(Bundle bundle) {
|
public void onCreate(Bundle bundle) {
|
||||||
this.masterSecret = getIntent().getParcelableExtra("master_secret");
|
this.masterSecret = getIntent().getParcelableExtra("master_secret");
|
||||||
|
|
||||||
getIntent().putExtra("identity_key", IdentityKeyUtil.getIdentityKey(this, Curve.DJB_TYPE));
|
getIntent().putExtra("identity_key", IdentityKeyUtil.getIdentityKey(this));
|
||||||
getIntent().putExtra("title", getString(R.string.ViewIdentityActivity_my_identity_fingerprint));
|
getIntent().putExtra("title", getString(R.string.ViewIdentityActivity_my_identity_fingerprint));
|
||||||
super.onCreate(bundle);
|
super.onCreate(bundle);
|
||||||
}
|
}
|
||||||
@ -116,8 +115,7 @@ public class ViewLocalIdentityActivity extends ViewIdentityActivity {
|
|||||||
Toast.LENGTH_LONG).show();
|
Toast.LENGTH_LONG).show();
|
||||||
|
|
||||||
getIntent().putExtra("identity_key",
|
getIntent().putExtra("identity_key",
|
||||||
IdentityKeyUtil.getIdentityKey(ViewLocalIdentityActivity.this,
|
IdentityKeyUtil.getIdentityKey(ViewLocalIdentityActivity.this));
|
||||||
Curve.DJB_TYPE));
|
|
||||||
initialize();
|
initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,7 +70,7 @@ public class AsymmetricMasterCipher {
|
|||||||
byte[][] parts = Util.split(combined, PublicKey.KEY_SIZE, combined.length - PublicKey.KEY_SIZE);
|
byte[][] parts = Util.split(combined, PublicKey.KEY_SIZE, combined.length - PublicKey.KEY_SIZE);
|
||||||
PublicKey theirPublicKey = new PublicKey(parts[0], 0);
|
PublicKey theirPublicKey = new PublicKey(parts[0], 0);
|
||||||
|
|
||||||
ECPrivateKey ourPrivateKey = asymmetricMasterSecret.getPrivateKey(theirPublicKey.getType());
|
ECPrivateKey ourPrivateKey = asymmetricMasterSecret.getPrivateKey();
|
||||||
byte[] secret = Curve.calculateAgreement(theirPublicKey.getKey(), ourPrivateKey);
|
byte[] secret = Curve.calculateAgreement(theirPublicKey.getKey(), ourPrivateKey);
|
||||||
MasterCipher masterCipher = getMasterCipherForSecret(secret);
|
MasterCipher masterCipher = getMasterCipherForSecret(secret);
|
||||||
byte[] decryptedBody = masterCipher.decryptBytes(parts[1]);
|
byte[] decryptedBody = masterCipher.decryptBytes(parts[1]);
|
||||||
@ -85,15 +85,8 @@ public class AsymmetricMasterCipher {
|
|||||||
|
|
||||||
public String encryptBody(String body) {
|
public String encryptBody(String body) {
|
||||||
try {
|
try {
|
||||||
ECPublicKey theirPublic;
|
ECPublicKey theirPublic = asymmetricMasterSecret.getDjbPublicKey();
|
||||||
|
ECKeyPair ourKeyPair = Curve.generateKeyPair(true);
|
||||||
if (asymmetricMasterSecret.getDjbPublicKey() != null) {
|
|
||||||
theirPublic = asymmetricMasterSecret.getDjbPublicKey();
|
|
||||||
} else {
|
|
||||||
theirPublic = asymmetricMasterSecret.getNistPublicKey();
|
|
||||||
}
|
|
||||||
|
|
||||||
ECKeyPair ourKeyPair = Curve.generateKeyPairForType(theirPublic.getType(), true);
|
|
||||||
byte[] secret = Curve.calculateAgreement(theirPublic, ourKeyPair.getPrivateKey());
|
byte[] secret = Curve.calculateAgreement(theirPublic, ourKeyPair.getPrivateKey());
|
||||||
MasterCipher masterCipher = getMasterCipherForSecret(secret);
|
MasterCipher masterCipher = getMasterCipherForSecret(secret);
|
||||||
byte[] encryptedBodyBytes = masterCipher.encryptBytes(body.getBytes());
|
byte[] encryptedBodyBytes = masterCipher.encryptBytes(body.getBytes());
|
||||||
|
@ -43,32 +43,20 @@ public class AsymmetricMasterSecret {
|
|||||||
private final ECPublicKey djbPublicKey;
|
private final ECPublicKey djbPublicKey;
|
||||||
private final ECPrivateKey djbPrivateKey;
|
private final ECPrivateKey djbPrivateKey;
|
||||||
|
|
||||||
private final ECPublicKey nistPublicKey;
|
|
||||||
private final ECPrivateKey nistPrivateKey;
|
|
||||||
|
|
||||||
public AsymmetricMasterSecret(ECPublicKey djbPublicKey, ECPrivateKey djbPrivateKey,
|
public AsymmetricMasterSecret(ECPublicKey djbPublicKey, ECPrivateKey djbPrivateKey)
|
||||||
ECPublicKey nistPublicKey, ECPrivateKey nistPrivateKey)
|
|
||||||
{
|
{
|
||||||
this.djbPublicKey = djbPublicKey;
|
this.djbPublicKey = djbPublicKey;
|
||||||
this.djbPrivateKey = djbPrivateKey;
|
this.djbPrivateKey = djbPrivateKey;
|
||||||
this.nistPublicKey = nistPublicKey;
|
|
||||||
this.nistPrivateKey = nistPrivateKey;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ECPublicKey getDjbPublicKey() {
|
public ECPublicKey getDjbPublicKey() {
|
||||||
return djbPublicKey;
|
return djbPublicKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ECPublicKey getNistPublicKey() {
|
|
||||||
return nistPublicKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ECPrivateKey getPrivateKey(int type) {
|
public ECPrivateKey getPrivateKey() {
|
||||||
if (type == Curve.DJB_TYPE) {
|
|
||||||
return djbPrivateKey;
|
return djbPrivateKey;
|
||||||
} else {
|
|
||||||
return nistPrivateKey;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ import android.database.Cursor;
|
|||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.crypto.protocol.KeyExchangeMessage;
|
import org.thoughtcrime.securesms.crypto.protocol.KeyExchangeMessage;
|
||||||
|
import org.whispersystems.textsecure.crypto.LegacyMessageException;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
|
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
|
||||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||||
@ -217,6 +218,9 @@ public class DecryptingQueue {
|
|||||||
} catch (DuplicateMessageException e) {
|
} catch (DuplicateMessageException e) {
|
||||||
Log.w("DecryptingQueue", e);
|
Log.w("DecryptingQueue", e);
|
||||||
sendResult(PushReceiver.RESULT_DECRYPT_DUPLICATE);
|
sendResult(PushReceiver.RESULT_DECRYPT_DUPLICATE);
|
||||||
|
} catch (LegacyMessageException e) {
|
||||||
|
Log.w("DecryptionQueue", e);
|
||||||
|
sendResult(PushReceiver.RESULT_DECRYPT_FAILED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -319,6 +323,9 @@ public class DecryptingQueue {
|
|||||||
} catch (DuplicateMessageException dme) {
|
} catch (DuplicateMessageException dme) {
|
||||||
Log.w("DecryptingQueue", dme);
|
Log.w("DecryptingQueue", dme);
|
||||||
database.markAsDecryptDuplicate(messageId, threadId);
|
database.markAsDecryptDuplicate(messageId, threadId);
|
||||||
|
} catch (LegacyMessageException lme) {
|
||||||
|
Log.w("DecryptingQueue", lme);
|
||||||
|
database.markAsLegacyVersion(messageId, threadId);
|
||||||
} catch (MmsException mme) {
|
} catch (MmsException mme) {
|
||||||
Log.w("DecryptingQueue", mme);
|
Log.w("DecryptingQueue", mme);
|
||||||
database.markAsDecryptFailed(messageId, threadId);
|
database.markAsDecryptFailed(messageId, threadId);
|
||||||
@ -390,6 +397,10 @@ public class DecryptingQueue {
|
|||||||
Log.w("DecryptionQueue", e);
|
Log.w("DecryptionQueue", e);
|
||||||
database.markAsDecryptFailed(messageId);
|
database.markAsDecryptFailed(messageId);
|
||||||
return;
|
return;
|
||||||
|
} catch (LegacyMessageException lme) {
|
||||||
|
Log.w("DecryptionQueue", lme);
|
||||||
|
database.markAsLegacyVersion(messageId);
|
||||||
|
return;
|
||||||
} catch (RecipientFormattingException e) {
|
} catch (RecipientFormattingException e) {
|
||||||
Log.w("DecryptionQueue", e);
|
Log.w("DecryptionQueue", e);
|
||||||
database.markAsDecryptFailed(messageId);
|
database.markAsDecryptFailed(messageId);
|
||||||
@ -457,6 +468,9 @@ public class DecryptingQueue {
|
|||||||
} catch (RecipientFormattingException e) {
|
} catch (RecipientFormattingException e) {
|
||||||
Log.w("DecryptingQueue", e);
|
Log.w("DecryptingQueue", e);
|
||||||
DatabaseFactory.getEncryptingSmsDatabase(context).markAsCorruptKeyExchange(messageId);
|
DatabaseFactory.getEncryptingSmsDatabase(context).markAsCorruptKeyExchange(messageId);
|
||||||
|
} catch (LegacyMessageException e) {
|
||||||
|
Log.w("DecryptingQueue", e);
|
||||||
|
DatabaseFactory.getEncryptingSmsDatabase(context).markAsLegacyVersion(messageId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,31 +22,17 @@ import android.content.SharedPreferences;
|
|||||||
import android.content.SharedPreferences.Editor;
|
import android.content.SharedPreferences.Editor;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import org.spongycastle.asn1.ASN1Encoding;
|
|
||||||
import org.spongycastle.asn1.ASN1Primitive;
|
|
||||||
import org.spongycastle.asn1.ASN1Sequence;
|
|
||||||
import org.spongycastle.asn1.DERInteger;
|
|
||||||
import org.spongycastle.asn1.DERSequence;
|
|
||||||
import org.spongycastle.crypto.signers.ECDSASigner;
|
|
||||||
import org.whispersystems.textsecure.crypto.IdentityKey;
|
import org.whispersystems.textsecure.crypto.IdentityKey;
|
||||||
import org.whispersystems.textsecure.crypto.IdentityKeyPair;
|
import org.whispersystems.textsecure.crypto.IdentityKeyPair;
|
||||||
import org.whispersystems.textsecure.crypto.InvalidKeyException;
|
import org.whispersystems.textsecure.crypto.InvalidKeyException;
|
||||||
import org.whispersystems.textsecure.crypto.MasterCipher;
|
import org.whispersystems.textsecure.crypto.MasterCipher;
|
||||||
import org.whispersystems.textsecure.crypto.MasterSecret;
|
import org.whispersystems.textsecure.crypto.MasterSecret;
|
||||||
import org.whispersystems.textsecure.crypto.PublicKey;
|
|
||||||
import org.whispersystems.textsecure.crypto.ecc.Curve;
|
import org.whispersystems.textsecure.crypto.ecc.Curve;
|
||||||
import org.whispersystems.textsecure.crypto.ecc.ECKeyPair;
|
import org.whispersystems.textsecure.crypto.ecc.ECKeyPair;
|
||||||
import org.whispersystems.textsecure.crypto.ecc.ECPrivateKey;
|
import org.whispersystems.textsecure.crypto.ecc.ECPrivateKey;
|
||||||
import org.whispersystems.textsecure.crypto.ecc.NistECPrivateKey;
|
|
||||||
import org.whispersystems.textsecure.crypto.ecc.NistECPublicKey;
|
|
||||||
import org.whispersystems.textsecure.util.Base64;
|
import org.whispersystems.textsecure.util.Base64;
|
||||||
import org.whispersystems.textsecure.util.Conversions;
|
|
||||||
import org.whispersystems.textsecure.util.Util;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.math.BigInteger;
|
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility class for working with identity keys.
|
* Utility class for working with identity keys.
|
||||||
@ -56,39 +42,22 @@ import java.security.NoSuchAlgorithmException;
|
|||||||
|
|
||||||
public class IdentityKeyUtil {
|
public class IdentityKeyUtil {
|
||||||
|
|
||||||
private static final String IDENTITY_PUBLIC_KEY_NIST_PREF = "pref_identity_public";
|
|
||||||
private static final String IDENTITY_PRIVATE_KEY_NIST_PREF = "pref_identity_private";
|
|
||||||
|
|
||||||
private static final String IDENTITY_PUBLIC_KEY_DJB_PREF = "pref_identity_public_curve25519";
|
private static final String IDENTITY_PUBLIC_KEY_DJB_PREF = "pref_identity_public_curve25519";
|
||||||
private static final String IDENTITY_PRIVATE_KEY_DJB_PREF = "pref_identity_private_curve25519";
|
private static final String IDENTITY_PRIVATE_KEY_DJB_PREF = "pref_identity_private_curve25519";
|
||||||
|
|
||||||
public static boolean hasIdentityKey(Context context, int type) {
|
public static boolean hasIdentityKey(Context context) {
|
||||||
SharedPreferences preferences = context.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0);
|
SharedPreferences preferences = context.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0);
|
||||||
|
|
||||||
if (type == Curve.DJB_TYPE) {
|
|
||||||
return
|
return
|
||||||
preferences.contains(IDENTITY_PUBLIC_KEY_DJB_PREF) &&
|
preferences.contains(IDENTITY_PUBLIC_KEY_DJB_PREF) &&
|
||||||
preferences.contains(IDENTITY_PRIVATE_KEY_DJB_PREF);
|
preferences.contains(IDENTITY_PRIVATE_KEY_DJB_PREF);
|
||||||
} else if (type == Curve.NIST_TYPE) {
|
|
||||||
return
|
|
||||||
preferences.contains(IDENTITY_PUBLIC_KEY_NIST_PREF) &&
|
|
||||||
preferences.contains(IDENTITY_PRIVATE_KEY_NIST_PREF);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
public static IdentityKey getIdentityKey(Context context) {
|
||||||
}
|
if (!hasIdentityKey(context)) return null;
|
||||||
|
|
||||||
public static IdentityKey getIdentityKey(Context context, int type) {
|
|
||||||
if (!hasIdentityKey(context, type)) return null;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String key;
|
byte[] publicKeyBytes = Base64.decode(retrieve(context, IDENTITY_PUBLIC_KEY_DJB_PREF));
|
||||||
|
|
||||||
if (type == Curve.DJB_TYPE) key = IDENTITY_PUBLIC_KEY_DJB_PREF;
|
|
||||||
else if (type == Curve.NIST_TYPE) key = IDENTITY_PUBLIC_KEY_NIST_PREF;
|
|
||||||
else return null;
|
|
||||||
|
|
||||||
byte[] publicKeyBytes = Base64.decode(retrieve(context, key));
|
|
||||||
return new IdentityKey(publicKeyBytes, 0);
|
return new IdentityKey(publicKeyBytes, 0);
|
||||||
} catch (IOException ioe) {
|
} catch (IOException ioe) {
|
||||||
Log.w("IdentityKeyUtil", ioe);
|
Log.w("IdentityKeyUtil", ioe);
|
||||||
@ -100,22 +69,15 @@ public class IdentityKeyUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static IdentityKeyPair getIdentityKeyPair(Context context,
|
public static IdentityKeyPair getIdentityKeyPair(Context context,
|
||||||
MasterSecret masterSecret,
|
MasterSecret masterSecret)
|
||||||
int type)
|
|
||||||
{
|
{
|
||||||
if (!hasIdentityKey(context, type))
|
if (!hasIdentityKey(context))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String key;
|
|
||||||
|
|
||||||
if (type == Curve.DJB_TYPE) key = IDENTITY_PRIVATE_KEY_DJB_PREF;
|
|
||||||
else if (type == Curve.NIST_TYPE) key = IDENTITY_PRIVATE_KEY_NIST_PREF;
|
|
||||||
else return null;
|
|
||||||
|
|
||||||
MasterCipher masterCipher = new MasterCipher(masterSecret);
|
MasterCipher masterCipher = new MasterCipher(masterSecret);
|
||||||
IdentityKey publicKey = getIdentityKey(context, type);
|
IdentityKey publicKey = getIdentityKey(context);
|
||||||
ECPrivateKey privateKey = masterCipher.decryptKey(type, Base64.decode(retrieve(context, key)));
|
ECPrivateKey privateKey = masterCipher.decryptKey(Base64.decode(retrieve(context, IDENTITY_PRIVATE_KEY_DJB_PREF)));
|
||||||
|
|
||||||
return new IdentityKeyPair(publicKey, privateKey);
|
return new IdentityKeyPair(publicKey, privateKey);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@ -125,30 +87,14 @@ public class IdentityKeyUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getFingerprint(Context context, int type) {
|
|
||||||
if (!hasIdentityKey(context, type)) return null;
|
|
||||||
|
|
||||||
IdentityKey identityKey = getIdentityKey(context, type);
|
|
||||||
|
|
||||||
if (identityKey == null) return null;
|
|
||||||
else return identityKey.getFingerprint();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void generateIdentityKeys(Context context, MasterSecret masterSecret) {
|
public static void generateIdentityKeys(Context context, MasterSecret masterSecret) {
|
||||||
ECKeyPair nistKeyPair = Curve.generateKeyPairForType(Curve.NIST_TYPE, false);
|
ECKeyPair djbKeyPair = Curve.generateKeyPair(false);
|
||||||
ECKeyPair djbKeyPair = Curve.generateKeyPairForType(Curve.DJB_TYPE, false);
|
|
||||||
|
|
||||||
MasterCipher masterCipher = new MasterCipher(masterSecret);
|
MasterCipher masterCipher = new MasterCipher(masterSecret);
|
||||||
IdentityKey nistIdentityKey = new IdentityKey(nistKeyPair.getPublicKey());
|
|
||||||
IdentityKey djbIdentityKey = new IdentityKey(djbKeyPair.getPublicKey());
|
IdentityKey djbIdentityKey = new IdentityKey(djbKeyPair.getPublicKey());
|
||||||
|
|
||||||
byte[] nistPrivateKey = masterCipher.encryptKey(nistKeyPair.getPrivateKey());
|
|
||||||
byte[] djbPrivateKey = masterCipher.encryptKey(djbKeyPair.getPrivateKey());
|
byte[] djbPrivateKey = masterCipher.encryptKey(djbKeyPair.getPrivateKey());
|
||||||
|
|
||||||
save(context, IDENTITY_PUBLIC_KEY_NIST_PREF, Base64.encodeBytes(nistIdentityKey.serialize()));
|
|
||||||
save(context, IDENTITY_PUBLIC_KEY_DJB_PREF, Base64.encodeBytes(djbIdentityKey.serialize()));
|
save(context, IDENTITY_PUBLIC_KEY_DJB_PREF, Base64.encodeBytes(djbIdentityKey.serialize()));
|
||||||
|
|
||||||
save(context, IDENTITY_PRIVATE_KEY_NIST_PREF, Base64.encodeBytes(nistPrivateKey));
|
|
||||||
save(context, IDENTITY_PRIVATE_KEY_DJB_PREF, Base64.encodeBytes(djbPrivateKey));
|
save(context, IDENTITY_PRIVATE_KEY_DJB_PREF, Base64.encodeBytes(djbPrivateKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,7 +106,7 @@ public class IdentityKeyUtil {
|
|||||||
|
|
||||||
public static void generateCurve25519IdentityKeys(Context context, MasterSecret masterSecret) {
|
public static void generateCurve25519IdentityKeys(Context context, MasterSecret masterSecret) {
|
||||||
MasterCipher masterCipher = new MasterCipher(masterSecret);
|
MasterCipher masterCipher = new MasterCipher(masterSecret);
|
||||||
ECKeyPair djbKeyPair = Curve.generateKeyPairForType(Curve.DJB_TYPE, false);
|
ECKeyPair djbKeyPair = Curve.generateKeyPair(false);
|
||||||
IdentityKey djbIdentityKey = new IdentityKey(djbKeyPair.getPublicKey());
|
IdentityKey djbIdentityKey = new IdentityKey(djbKeyPair.getPublicKey());
|
||||||
byte[] djbPrivateKey = masterCipher.encryptKey(djbKeyPair.getPrivateKey());
|
byte[] djbPrivateKey = masterCipher.encryptKey(djbKeyPair.getPrivateKey());
|
||||||
|
|
||||||
@ -168,86 +114,6 @@ public class IdentityKeyUtil {
|
|||||||
save(context, IDENTITY_PRIVATE_KEY_DJB_PREF, Base64.encodeBytes(djbPrivateKey));
|
save(context, IDENTITY_PRIVATE_KEY_DJB_PREF, Base64.encodeBytes(djbPrivateKey));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IdentityKey verifySignedKeyExchange(byte[] keyExchangeBytes)
|
|
||||||
throws InvalidKeyException
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
byte[] messageBytes = new byte[1 + PublicKey.KEY_SIZE];
|
|
||||||
System.arraycopy(keyExchangeBytes, 0, messageBytes, 0, messageBytes.length);
|
|
||||||
|
|
||||||
byte[] publicKeyBytes = new byte[IdentityKey.NIST_SIZE];
|
|
||||||
System.arraycopy(keyExchangeBytes, messageBytes.length, publicKeyBytes, 0, publicKeyBytes.length);
|
|
||||||
|
|
||||||
int signatureLength = Conversions.byteArrayToShort(keyExchangeBytes, messageBytes.length + publicKeyBytes.length);
|
|
||||||
byte[] signatureBytes = new byte[signatureLength];
|
|
||||||
System.arraycopy(keyExchangeBytes, messageBytes.length + publicKeyBytes.length + 2, signatureBytes, 0, signatureBytes.length);
|
|
||||||
|
|
||||||
byte[] messageHash = getMessageHash(messageBytes, publicKeyBytes);
|
|
||||||
IdentityKey identityKey = new IdentityKey(publicKeyBytes, 0);
|
|
||||||
ECDSASigner verifier = new ECDSASigner();
|
|
||||||
|
|
||||||
if (identityKey.getPublicKey().getType() != Curve.NIST_TYPE) {
|
|
||||||
throw new InvalidKeyException("Signing only support on P256 keys!");
|
|
||||||
}
|
|
||||||
|
|
||||||
verifier.init(false, ((NistECPublicKey)identityKey.getPublicKey()).getParameters());
|
|
||||||
|
|
||||||
ASN1Sequence sequence = (ASN1Sequence) ASN1Primitive.fromByteArray(signatureBytes);
|
|
||||||
BigInteger[] signatureIntegers = new BigInteger[]{
|
|
||||||
((DERInteger)sequence.getObjectAt(0)).getValue(),
|
|
||||||
((DERInteger)sequence.getObjectAt(1)).getValue()
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!verifier.verifySignature(messageHash, signatureIntegers[0], signatureIntegers[1]))
|
|
||||||
throw new InvalidKeyException("Invalid signature!");
|
|
||||||
else
|
|
||||||
return identityKey;
|
|
||||||
|
|
||||||
} catch (IOException ioe) {
|
|
||||||
throw new InvalidKeyException(ioe);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public static byte[] getSignedKeyExchange(Context context, MasterSecret masterSecret,
|
|
||||||
byte[] keyExchangeBytes)
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
MasterCipher masterCipher = new MasterCipher(masterSecret);
|
|
||||||
byte[] publicKeyBytes = getIdentityKey(context, Curve.NIST_TYPE).serialize();
|
|
||||||
byte[] messageHash = getMessageHash(keyExchangeBytes, publicKeyBytes);
|
|
||||||
byte[] privateKeyBytes = Base64.decode(retrieve(context, IDENTITY_PRIVATE_KEY_NIST_PREF));
|
|
||||||
ECPrivateKey privateKey = masterCipher.decryptKey(Curve.NIST_TYPE, privateKeyBytes);
|
|
||||||
ECDSASigner signer = new ECDSASigner();
|
|
||||||
|
|
||||||
signer.init(true, ((NistECPrivateKey)privateKey).getParameters());
|
|
||||||
|
|
||||||
BigInteger[] messageSignatureInts = signer.generateSignature(messageHash);
|
|
||||||
DERInteger[] derMessageSignatureInts = new DERInteger[]{ new DERInteger(messageSignatureInts[0]), new DERInteger(messageSignatureInts[1]) };
|
|
||||||
byte[] messageSignatureBytes = new DERSequence(derMessageSignatureInts).getEncoded(ASN1Encoding.DER);
|
|
||||||
byte[] messageSignature = new byte[2 + messageSignatureBytes.length];
|
|
||||||
|
|
||||||
Conversions.shortToByteArray(messageSignature, 0, messageSignatureBytes.length);
|
|
||||||
System.arraycopy(messageSignatureBytes, 0, messageSignature, 2, messageSignatureBytes.length);
|
|
||||||
|
|
||||||
return Util.combine(keyExchangeBytes, publicKeyBytes, messageSignature);
|
|
||||||
} catch (IOException ioe) {
|
|
||||||
throw new AssertionError(ioe);
|
|
||||||
} catch (InvalidKeyException e) {
|
|
||||||
throw new AssertionError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] getMessageHash(byte[] messageBytes, byte[] publicKeyBytes) {
|
|
||||||
try {
|
|
||||||
MessageDigest md = MessageDigest.getInstance("SHA1");
|
|
||||||
md.update(messageBytes);
|
|
||||||
return md.digest(publicKeyBytes);
|
|
||||||
} catch (NoSuchAlgorithmException nsae) {
|
|
||||||
throw new AssertionError(nsae);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String retrieve(Context context, String key) {
|
public static String retrieve(Context context, String key) {
|
||||||
SharedPreferences preferences = context.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0);
|
SharedPreferences preferences = context.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0);
|
||||||
return preferences.getString(key, null);
|
return preferences.getString(key, null);
|
||||||
|
@ -31,7 +31,6 @@ import org.whispersystems.textsecure.crypto.IdentityKeyPair;
|
|||||||
import org.whispersystems.textsecure.crypto.MasterSecret;
|
import org.whispersystems.textsecure.crypto.MasterSecret;
|
||||||
import org.whispersystems.textsecure.crypto.ecc.Curve;
|
import org.whispersystems.textsecure.crypto.ecc.Curve;
|
||||||
import org.whispersystems.textsecure.crypto.ecc.ECKeyPair;
|
import org.whispersystems.textsecure.crypto.ecc.ECKeyPair;
|
||||||
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
|
|
||||||
import org.whispersystems.textsecure.storage.RecipientDevice;
|
import org.whispersystems.textsecure.storage.RecipientDevice;
|
||||||
import org.whispersystems.textsecure.storage.SessionRecordV2;
|
import org.whispersystems.textsecure.storage.SessionRecordV2;
|
||||||
|
|
||||||
@ -62,9 +61,9 @@ public class KeyExchangeInitiator {
|
|||||||
private static void initiateKeyExchange(Context context, MasterSecret masterSecret, Recipient recipient) {
|
private static void initiateKeyExchange(Context context, MasterSecret masterSecret, Recipient recipient) {
|
||||||
int sequence = getRandomSequence();
|
int sequence = getRandomSequence();
|
||||||
int flags = KeyExchangeMessageV2.INITIATE_FLAG;
|
int flags = KeyExchangeMessageV2.INITIATE_FLAG;
|
||||||
ECKeyPair baseKey = Curve.generateKeyPairForSession(CiphertextMessage.CURRENT_VERSION, true);
|
ECKeyPair baseKey = Curve.generateKeyPair(true);
|
||||||
ECKeyPair ephemeralKey = Curve.generateKeyPairForSession(CiphertextMessage.CURRENT_VERSION, true);
|
ECKeyPair ephemeralKey = Curve.generateKeyPair(true);
|
||||||
IdentityKeyPair identityKey = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret, Curve.DJB_TYPE);
|
IdentityKeyPair identityKey = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret);
|
||||||
|
|
||||||
KeyExchangeMessageV2 message = new KeyExchangeMessageV2(sequence, flags,
|
KeyExchangeMessageV2 message = new KeyExchangeMessageV2(sequence, flags,
|
||||||
baseKey.getPublicKey(),
|
baseKey.getPublicKey(),
|
||||||
|
@ -41,12 +41,8 @@ public abstract class KeyExchangeProcessor {
|
|||||||
RecipientDevice recipientDevice,
|
RecipientDevice recipientDevice,
|
||||||
KeyExchangeMessage message)
|
KeyExchangeMessage message)
|
||||||
{
|
{
|
||||||
if (message.isLegacy()) {
|
|
||||||
return new KeyExchangeProcessorV1(context, masterSecret, recipientDevice.getRecipient());
|
|
||||||
} else {
|
|
||||||
return new KeyExchangeProcessorV2(context, masterSecret, recipientDevice);
|
return new KeyExchangeProcessorV2(context, masterSecret, recipientDevice);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public static void broadcastSecurityUpdateEvent(Context context, long threadId) {
|
public static void broadcastSecurityUpdateEvent(Context context, long threadId) {
|
||||||
Intent intent = new Intent(SECURITY_UPDATE_EVENT);
|
Intent intent = new Intent(SECURITY_UPDATE_EVENT);
|
||||||
|
@ -1,153 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.crypto;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.crypto.protocol.KeyExchangeMessage;
|
|
||||||
import org.thoughtcrime.securesms.crypto.protocol.KeyExchangeMessageV1;
|
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
|
||||||
import org.thoughtcrime.securesms.recipients.RecipientFactory;
|
|
||||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
|
||||||
import org.thoughtcrime.securesms.sms.OutgoingKeyExchangeMessage;
|
|
||||||
import org.whispersystems.textsecure.crypto.IdentityKey;
|
|
||||||
import org.whispersystems.textsecure.crypto.KeyPair;
|
|
||||||
import org.whispersystems.textsecure.crypto.MasterSecret;
|
|
||||||
import org.whispersystems.textsecure.crypto.ecc.Curve;
|
|
||||||
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
|
|
||||||
import org.whispersystems.textsecure.storage.CanonicalRecipient;
|
|
||||||
import org.whispersystems.textsecure.storage.LocalKeyRecord;
|
|
||||||
import org.whispersystems.textsecure.storage.RemoteKeyRecord;
|
|
||||||
import org.whispersystems.textsecure.storage.SessionRecordV1;
|
|
||||||
import org.whispersystems.textsecure.util.Conversions;
|
|
||||||
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class processes key exchange interactions.
|
|
||||||
*
|
|
||||||
* @author Moxie Marlinspike
|
|
||||||
*/
|
|
||||||
|
|
||||||
public class KeyExchangeProcessorV1 extends KeyExchangeProcessor {
|
|
||||||
|
|
||||||
private Context context;
|
|
||||||
private CanonicalRecipient recipient;
|
|
||||||
private MasterSecret masterSecret;
|
|
||||||
private LocalKeyRecord localKeyRecord;
|
|
||||||
private RemoteKeyRecord remoteKeyRecord;
|
|
||||||
private SessionRecordV1 sessionRecord;
|
|
||||||
|
|
||||||
public KeyExchangeProcessorV1(Context context, MasterSecret masterSecret, CanonicalRecipient recipient) {
|
|
||||||
this.context = context;
|
|
||||||
this.recipient = recipient;
|
|
||||||
this.masterSecret = masterSecret;
|
|
||||||
|
|
||||||
this.remoteKeyRecord = new RemoteKeyRecord(context, recipient);
|
|
||||||
this.localKeyRecord = new LocalKeyRecord(context, masterSecret, recipient);
|
|
||||||
this.sessionRecord = new SessionRecordV1(context, masterSecret, recipient);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isTrusted(KeyExchangeMessage message) {
|
|
||||||
return message.hasIdentityKey() && isTrusted(message.getIdentityKey());
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isTrusted(IdentityKey identityKey) {
|
|
||||||
return DatabaseFactory.getIdentityDatabase(context).isValidIdentity(masterSecret,
|
|
||||||
recipient.getRecipientId(),
|
|
||||||
identityKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean hasInitiatedSession() {
|
|
||||||
return localKeyRecord.getCurrentKeyPair() != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean needsResponseFromUs() {
|
|
||||||
return !hasInitiatedSession() || remoteKeyRecord.getCurrentRemoteKey() != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isStale(KeyExchangeMessage _message) {
|
|
||||||
KeyExchangeMessageV1 message = (KeyExchangeMessageV1)_message;
|
|
||||||
int responseKeyId = Conversions.highBitsToMedium(message.getRemoteKey().getId());
|
|
||||||
|
|
||||||
Log.w("KeyExchangeProcessor", "Key Exchange High ID Bits: " + responseKeyId);
|
|
||||||
|
|
||||||
return responseKeyId != 0 &&
|
|
||||||
(localKeyRecord.getCurrentKeyPair() != null && localKeyRecord.getCurrentKeyPair().getId() != responseKeyId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void processKeyExchangeMessage(KeyExchangeMessage _message, long threadId) {
|
|
||||||
KeyExchangeMessageV1 message = (KeyExchangeMessageV1) _message;
|
|
||||||
int initiateKeyId = Conversions.lowBitsToMedium(message.getRemoteKey().getId());
|
|
||||||
|
|
||||||
Recipient recipient = RecipientFactory.getRecipientsForIds(context,
|
|
||||||
this.recipient.getRecipientId()+"",
|
|
||||||
true).getPrimaryRecipient();
|
|
||||||
|
|
||||||
message.getRemoteKey().setId(initiateKeyId);
|
|
||||||
|
|
||||||
if (needsResponseFromUs()) {
|
|
||||||
localKeyRecord = initializeRecordFor(context, masterSecret, recipient);
|
|
||||||
|
|
||||||
KeyExchangeMessageV1 ourMessage = new KeyExchangeMessageV1(context, masterSecret,
|
|
||||||
Math.min(CiphertextMessage.LEGACY_VERSION,
|
|
||||||
message.getMaxVersion()),
|
|
||||||
localKeyRecord, initiateKeyId);
|
|
||||||
|
|
||||||
OutgoingKeyExchangeMessage textMessage = new OutgoingKeyExchangeMessage(recipient, ourMessage.serialize());
|
|
||||||
Log.w("KeyExchangeProcessorV1", "Responding with key exchange message fingerprint: " + ourMessage.getRemoteKey().getFingerprint());
|
|
||||||
Log.w("KeyExchangeProcessorV1", "Which has a local key record fingerprint: " + localKeyRecord.getCurrentKeyPair().getPublicKey().getFingerprint());
|
|
||||||
MessageSender.send(context, masterSecret, textMessage, threadId);
|
|
||||||
}
|
|
||||||
|
|
||||||
remoteKeyRecord.setCurrentRemoteKey(message.getRemoteKey());
|
|
||||||
remoteKeyRecord.setLastRemoteKey(message.getRemoteKey());
|
|
||||||
remoteKeyRecord.save();
|
|
||||||
|
|
||||||
sessionRecord.setSessionId(localKeyRecord.getCurrentKeyPair().getPublicKey().getFingerprintBytes(),
|
|
||||||
remoteKeyRecord.getCurrentRemoteKey().getFingerprintBytes());
|
|
||||||
sessionRecord.setIdentityKey(message.getIdentityKey());
|
|
||||||
sessionRecord.setSessionVersion(Math.min(1, message.getMaxVersion()));
|
|
||||||
|
|
||||||
Log.w("KeyExchangeUtil", "Setting session version: " + Math.min(1, message.getMaxVersion()));
|
|
||||||
|
|
||||||
sessionRecord.save();
|
|
||||||
|
|
||||||
if (message.hasIdentityKey()) {
|
|
||||||
DatabaseFactory.getIdentityDatabase(context)
|
|
||||||
.saveIdentity(masterSecret, recipient.getRecipientId(), message.getIdentityKey());
|
|
||||||
}
|
|
||||||
|
|
||||||
DecryptingQueue.scheduleRogueMessages(context, masterSecret, recipient);
|
|
||||||
|
|
||||||
broadcastSecurityUpdateEvent(context, threadId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public LocalKeyRecord initializeRecordFor(Context context,
|
|
||||||
MasterSecret masterSecret,
|
|
||||||
CanonicalRecipient recipient)
|
|
||||||
{
|
|
||||||
Log.w("KeyExchangeProcessorV1", "Initializing local key pairs...");
|
|
||||||
try {
|
|
||||||
SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
|
|
||||||
int initialId = secureRandom.nextInt(4094) + 1;
|
|
||||||
|
|
||||||
KeyPair currentPair = new KeyPair(initialId, Curve.generateKeyPairForSession(1, true), masterSecret);
|
|
||||||
KeyPair nextPair = new KeyPair(initialId + 1, Curve.generateKeyPairForSession(1, true), 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -99,7 +99,7 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor {
|
|||||||
PreKeyRecord preKeyRecord = new PreKeyRecord(context, masterSecret, preKeyId);
|
PreKeyRecord preKeyRecord = new PreKeyRecord(context, masterSecret, preKeyId);
|
||||||
ECKeyPair ourBaseKey = preKeyRecord.getKeyPair();
|
ECKeyPair ourBaseKey = preKeyRecord.getKeyPair();
|
||||||
ECKeyPair ourEphemeralKey = ourBaseKey;
|
ECKeyPair ourEphemeralKey = ourBaseKey;
|
||||||
IdentityKeyPair ourIdentityKey = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret, ourBaseKey.getPublicKey().getType());
|
IdentityKeyPair ourIdentityKey = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret);
|
||||||
boolean simultaneousInitiate = sessionRecord.getSessionState().hasPendingPreKey();
|
boolean simultaneousInitiate = sessionRecord.getSessionState().hasPendingPreKey();
|
||||||
|
|
||||||
if (!simultaneousInitiate) sessionRecord.clear();
|
if (!simultaneousInitiate) sessionRecord.clear();
|
||||||
@ -131,14 +131,12 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor {
|
|||||||
public void processKeyExchangeMessage(PreKeyEntity message, long threadId)
|
public void processKeyExchangeMessage(PreKeyEntity message, long threadId)
|
||||||
throws InvalidKeyException
|
throws InvalidKeyException
|
||||||
{
|
{
|
||||||
ECKeyPair ourBaseKey = Curve.generateKeyPairForSession(2, true);
|
ECKeyPair ourBaseKey = Curve.generateKeyPair(true);
|
||||||
ECKeyPair ourEphemeralKey = Curve.generateKeyPairForSession(2, true);
|
ECKeyPair ourEphemeralKey = Curve.generateKeyPair(true);
|
||||||
ECPublicKey theirBaseKey = message.getPublicKey();
|
ECPublicKey theirBaseKey = message.getPublicKey();
|
||||||
ECPublicKey theirEphemeralKey = theirBaseKey;
|
ECPublicKey theirEphemeralKey = theirBaseKey;
|
||||||
IdentityKey theirIdentityKey = message.getIdentityKey();
|
IdentityKey theirIdentityKey = message.getIdentityKey();
|
||||||
IdentityKeyPair ourIdentityKey = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret,
|
IdentityKeyPair ourIdentityKey = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret);
|
||||||
ourBaseKey.getPublicKey()
|
|
||||||
.getType());
|
|
||||||
|
|
||||||
if (sessionRecord.getSessionState().getNeedsRefresh()) sessionRecord.archiveCurrentState();
|
if (sessionRecord.getSessionState().getNeedsRefresh()) sessionRecord.archiveCurrentState();
|
||||||
else sessionRecord.clear();
|
else sessionRecord.clear();
|
||||||
@ -184,9 +182,9 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor {
|
|||||||
|
|
||||||
if (!sessionRecord.getSessionState().hasPendingKeyExchange()) {
|
if (!sessionRecord.getSessionState().hasPendingKeyExchange()) {
|
||||||
Log.w("KeyExchangeProcessorV2", "We don't have a pending initiate...");
|
Log.w("KeyExchangeProcessorV2", "We don't have a pending initiate...");
|
||||||
ourBaseKey = Curve.generateKeyPairForType(message.getBaseKey().getType(), true);
|
ourBaseKey = Curve.generateKeyPair(true);
|
||||||
ourEphemeralKey = Curve.generateKeyPairForType(message.getBaseKey().getType(), true);
|
ourEphemeralKey = Curve.generateKeyPair(true);
|
||||||
ourIdentityKey = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret, message.getBaseKey().getType());
|
ourIdentityKey = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret);
|
||||||
|
|
||||||
sessionRecord.getSessionState().setPendingKeyExchange(message.getSequence(), ourBaseKey,
|
sessionRecord.getSessionState().setPendingKeyExchange(message.getSequence(), ourBaseKey,
|
||||||
ourEphemeralKey, ourIdentityKey);
|
ourEphemeralKey, ourIdentityKey);
|
||||||
|
@ -25,7 +25,6 @@ import android.util.Log;
|
|||||||
import org.whispersystems.textsecure.crypto.InvalidKeyException;
|
import org.whispersystems.textsecure.crypto.InvalidKeyException;
|
||||||
import org.whispersystems.textsecure.crypto.MasterCipher;
|
import org.whispersystems.textsecure.crypto.MasterCipher;
|
||||||
import org.whispersystems.textsecure.crypto.MasterSecret;
|
import org.whispersystems.textsecure.crypto.MasterSecret;
|
||||||
import org.whispersystems.textsecure.crypto.PublicKey;
|
|
||||||
import org.whispersystems.textsecure.crypto.ecc.Curve;
|
import org.whispersystems.textsecure.crypto.ecc.Curve;
|
||||||
import org.whispersystems.textsecure.crypto.ecc.ECKeyPair;
|
import org.whispersystems.textsecure.crypto.ecc.ECKeyPair;
|
||||||
import org.whispersystems.textsecure.crypto.ecc.ECPrivateKey;
|
import org.whispersystems.textsecure.crypto.ecc.ECPrivateKey;
|
||||||
@ -59,8 +58,6 @@ public class MasterSecretUtil {
|
|||||||
public static final String UNENCRYPTED_PASSPHRASE = "unencrypted";
|
public static final String UNENCRYPTED_PASSPHRASE = "unencrypted";
|
||||||
public static final String PREFERENCES_NAME = "SecureSMS-Preferences";
|
public static final String PREFERENCES_NAME = "SecureSMS-Preferences";
|
||||||
|
|
||||||
private static final String ASYMMETRIC_LOCAL_PUBLIC_NIST = "asymmetric_master_secret_public";
|
|
||||||
private static final String ASYMMETRIC_LOCAL_PRIVATE_NIST = "asymmetric_master_secret_private";
|
|
||||||
private static final String ASYMMETRIC_LOCAL_PUBLIC_DJB = "asymmetric_master_secret_curve25519_public";
|
private static final String ASYMMETRIC_LOCAL_PUBLIC_DJB = "asymmetric_master_secret_curve25519_public";
|
||||||
private static final String ASYMMETRIC_LOCAL_PRIVATE_DJB = "asymmetric_master_secret_curve25519_private";
|
private static final String ASYMMETRIC_LOCAL_PRIVATE_DJB = "asymmetric_master_secret_curve25519_private";
|
||||||
|
|
||||||
@ -116,22 +113,12 @@ public class MasterSecretUtil {
|
|||||||
MasterSecret masterSecret)
|
MasterSecret masterSecret)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
byte[] nistPublicBytes = retrieve(context, ASYMMETRIC_LOCAL_PUBLIC_NIST);
|
|
||||||
byte[] djbPublicBytes = retrieve(context, ASYMMETRIC_LOCAL_PUBLIC_DJB);
|
byte[] djbPublicBytes = retrieve(context, ASYMMETRIC_LOCAL_PUBLIC_DJB);
|
||||||
|
|
||||||
byte[] nistPrivateBytes = retrieve(context, ASYMMETRIC_LOCAL_PRIVATE_NIST);
|
|
||||||
byte[] djbPrivateBytes = retrieve(context, ASYMMETRIC_LOCAL_PRIVATE_DJB);
|
byte[] djbPrivateBytes = retrieve(context, ASYMMETRIC_LOCAL_PRIVATE_DJB);
|
||||||
|
|
||||||
ECPublicKey nistPublicKey = null;
|
|
||||||
ECPublicKey djbPublicKey = null;
|
ECPublicKey djbPublicKey = null;
|
||||||
|
|
||||||
ECPrivateKey nistPrivateKey = null;
|
|
||||||
ECPrivateKey djbPrivateKey = null;
|
ECPrivateKey djbPrivateKey = null;
|
||||||
|
|
||||||
if (nistPublicBytes != null) {
|
|
||||||
nistPublicKey = new PublicKey(nistPublicBytes, 0).getKey();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (djbPublicBytes != null) {
|
if (djbPublicBytes != null) {
|
||||||
djbPublicKey = Curve.decodePoint(djbPublicBytes, 0);
|
djbPublicKey = Curve.decodePoint(djbPublicBytes, 0);
|
||||||
}
|
}
|
||||||
@ -139,16 +126,12 @@ public class MasterSecretUtil {
|
|||||||
if (masterSecret != null) {
|
if (masterSecret != null) {
|
||||||
MasterCipher masterCipher = new MasterCipher(masterSecret);
|
MasterCipher masterCipher = new MasterCipher(masterSecret);
|
||||||
|
|
||||||
if (nistPrivateBytes != null) {
|
|
||||||
nistPrivateKey = masterCipher.decryptKey(Curve.NIST_TYPE, nistPrivateBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (djbPrivateBytes != null) {
|
if (djbPrivateBytes != null) {
|
||||||
djbPrivateKey = masterCipher.decryptKey(Curve.DJB_TYPE, djbPrivateBytes);
|
djbPrivateKey = masterCipher.decryptKey(djbPrivateBytes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new AsymmetricMasterSecret(djbPublicKey, djbPrivateKey, nistPublicKey, nistPrivateKey);
|
return new AsymmetricMasterSecret(djbPublicKey, djbPrivateKey);
|
||||||
} catch (InvalidKeyException ike) {
|
} catch (InvalidKeyException ike) {
|
||||||
throw new AssertionError(ike);
|
throw new AssertionError(ike);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
@ -160,12 +143,12 @@ public class MasterSecretUtil {
|
|||||||
MasterSecret masterSecret)
|
MasterSecret masterSecret)
|
||||||
{
|
{
|
||||||
MasterCipher masterCipher = new MasterCipher(masterSecret);
|
MasterCipher masterCipher = new MasterCipher(masterSecret);
|
||||||
ECKeyPair keyPair = Curve.generateKeyPairForType(Curve.DJB_TYPE, true);
|
ECKeyPair keyPair = Curve.generateKeyPair(true);
|
||||||
|
|
||||||
save(context, ASYMMETRIC_LOCAL_PUBLIC_DJB, keyPair.getPublicKey().serialize());
|
save(context, ASYMMETRIC_LOCAL_PUBLIC_DJB, keyPair.getPublicKey().serialize());
|
||||||
save(context, ASYMMETRIC_LOCAL_PRIVATE_DJB, masterCipher.encryptKey(keyPair.getPrivateKey()));
|
save(context, ASYMMETRIC_LOCAL_PRIVATE_DJB, masterCipher.encryptKey(keyPair.getPrivateKey()));
|
||||||
|
|
||||||
return new AsymmetricMasterSecret(keyPair.getPublicKey(), keyPair.getPrivateKey(), null, null);
|
return new AsymmetricMasterSecret(keyPair.getPublicKey(), keyPair.getPrivateKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static MasterSecret generateMasterSecret(Context context, String passphrase) {
|
public static MasterSecret generateMasterSecret(Context context, String passphrase) {
|
||||||
@ -187,9 +170,7 @@ public class MasterSecretUtil {
|
|||||||
public static boolean hasAsymmericMasterSecret(Context context) {
|
public static boolean hasAsymmericMasterSecret(Context context) {
|
||||||
SharedPreferences settings = context.getSharedPreferences(PREFERENCES_NAME, 0);
|
SharedPreferences settings = context.getSharedPreferences(PREFERENCES_NAME, 0);
|
||||||
|
|
||||||
return
|
return settings.contains(ASYMMETRIC_LOCAL_PUBLIC_DJB);
|
||||||
settings.contains(ASYMMETRIC_LOCAL_PUBLIC_NIST) ||
|
|
||||||
settings.contains(ASYMMETRIC_LOCAL_PUBLIC_DJB);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isPassphraseInitialized(Context context) {
|
public static boolean isPassphraseInitialized(Context context) {
|
||||||
|
@ -1,39 +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.thoughtcrime.securesms.crypto;
|
|
||||||
|
|
||||||
import org.whispersystems.textsecure.crypto.IdentityKey;
|
|
||||||
import org.whispersystems.textsecure.crypto.PublicKey;
|
|
||||||
|
|
||||||
public class PublicKeyAndVersion {
|
|
||||||
|
|
||||||
public IdentityKey identityKey;
|
|
||||||
public final PublicKey key;
|
|
||||||
public final int version;
|
|
||||||
public final int maxVersion;
|
|
||||||
|
|
||||||
public PublicKeyAndVersion(PublicKey key, int version, int maxVersion) {
|
|
||||||
this.key = key;
|
|
||||||
this.version = version;
|
|
||||||
this.maxVersion = maxVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
public PublicKeyAndVersion(PublicKey key, IdentityKey identityKey, int version, int maxVersion) {
|
|
||||||
this(key, version, maxVersion);
|
|
||||||
this.identityKey = identityKey;
|
|
||||||
}
|
|
||||||
}
|
|
@ -21,35 +21,18 @@ import org.whispersystems.textsecure.crypto.IdentityKey;
|
|||||||
import org.whispersystems.textsecure.crypto.InvalidKeyException;
|
import org.whispersystems.textsecure.crypto.InvalidKeyException;
|
||||||
import org.whispersystems.textsecure.crypto.InvalidMessageException;
|
import org.whispersystems.textsecure.crypto.InvalidMessageException;
|
||||||
import org.whispersystems.textsecure.crypto.InvalidVersionException;
|
import org.whispersystems.textsecure.crypto.InvalidVersionException;
|
||||||
import org.whispersystems.textsecure.crypto.PublicKey;
|
import org.whispersystems.textsecure.crypto.LegacyMessageException;
|
||||||
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
|
|
||||||
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
|
|
||||||
import org.whispersystems.textsecure.util.Base64;
|
|
||||||
import org.whispersystems.textsecure.util.Conversions;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
|
|
||||||
public abstract class KeyExchangeMessage {
|
public abstract class KeyExchangeMessage {
|
||||||
public abstract boolean isLegacy();
|
|
||||||
public abstract IdentityKey getIdentityKey();
|
public abstract IdentityKey getIdentityKey();
|
||||||
public abstract boolean hasIdentityKey();
|
public abstract boolean hasIdentityKey();
|
||||||
public abstract int getMaxVersion();
|
public abstract int getMaxVersion();
|
||||||
public abstract int getVersion();
|
public abstract int getVersion();
|
||||||
|
|
||||||
public static KeyExchangeMessage createFor(String rawMessage)
|
public static KeyExchangeMessage createFor(String rawMessage)
|
||||||
throws InvalidMessageException, InvalidKeyException, InvalidVersionException
|
throws InvalidMessageException, InvalidKeyException, InvalidVersionException, LegacyMessageException
|
||||||
{
|
{
|
||||||
try {
|
|
||||||
byte[] decodedMessage = Base64.decodeWithoutPadding(rawMessage);
|
|
||||||
|
|
||||||
if (Conversions.highBitsToInt(decodedMessage[0]) <= CiphertextMessage.LEGACY_VERSION) {
|
|
||||||
return new KeyExchangeMessageV1(rawMessage);
|
|
||||||
} else {
|
|
||||||
return new KeyExchangeMessageV2(rawMessage);
|
return new KeyExchangeMessageV2(rawMessage);
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
|
||||||
throw new InvalidMessageException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,160 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.crypto.protocol;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
|
||||||
import org.whispersystems.textsecure.crypto.IdentityKey;
|
|
||||||
import org.whispersystems.textsecure.crypto.InvalidKeyException;
|
|
||||||
import org.whispersystems.textsecure.crypto.InvalidVersionException;
|
|
||||||
import org.whispersystems.textsecure.crypto.MasterSecret;
|
|
||||||
import org.whispersystems.textsecure.crypto.PublicKey;
|
|
||||||
import org.whispersystems.textsecure.crypto.ecc.Curve;
|
|
||||||
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
|
|
||||||
import org.whispersystems.textsecure.storage.LocalKeyRecord;
|
|
||||||
import org.whispersystems.textsecure.util.Base64;
|
|
||||||
import org.whispersystems.textsecure.util.Conversions;
|
|
||||||
import org.whispersystems.textsecure.util.Util;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A class for constructing and parsing key exchange messages.
|
|
||||||
*
|
|
||||||
* A key exchange message is basically represented by the following format:
|
|
||||||
*
|
|
||||||
* 1) 4 bits <protocol version number>
|
|
||||||
* 2) 4 bits <max supported protocol version number>
|
|
||||||
* 3) A serialized public key
|
|
||||||
* 4) (Optional) An identity key.
|
|
||||||
* 5) (if #4) A signature over the identity key, version bits, and serialized public key.
|
|
||||||
*
|
|
||||||
* A serialized public key is basically represented by the following format:
|
|
||||||
*
|
|
||||||
* 1) A 3 byte key ID.
|
|
||||||
* 2) An ECC key encoded with point compression.
|
|
||||||
*
|
|
||||||
* An initiating key ID is initialized with the bottom 12 bits of the ID set. A responding key
|
|
||||||
* ID does the same, but puts the initiating key ID's bottom 12 bits in the top 12 bits of its
|
|
||||||
* ID. This is used to correlate key exchange responses.
|
|
||||||
*
|
|
||||||
* @author Moxie Marlinspike
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
public class KeyExchangeMessageV1 extends KeyExchangeMessage {
|
|
||||||
|
|
||||||
private final int messageVersion;
|
|
||||||
private final int supportedVersion;
|
|
||||||
private final PublicKey publicKey;
|
|
||||||
private final String serialized;
|
|
||||||
private IdentityKey identityKey;
|
|
||||||
|
|
||||||
public KeyExchangeMessageV1(Context context, MasterSecret masterSecret,
|
|
||||||
int messageVersion, LocalKeyRecord record, int highIdBits)
|
|
||||||
{
|
|
||||||
this.publicKey = new PublicKey(record.getCurrentKeyPair().getPublicKey());
|
|
||||||
this.messageVersion = messageVersion;
|
|
||||||
this.supportedVersion = CiphertextMessage.CURRENT_VERSION;
|
|
||||||
|
|
||||||
publicKey.setId(publicKey.getId() | (highIdBits << 12));
|
|
||||||
|
|
||||||
byte[] versionBytes = {Conversions.intsToByteHighAndLow(messageVersion, supportedVersion)};
|
|
||||||
byte[] publicKeyBytes = publicKey.serialize();
|
|
||||||
|
|
||||||
byte[] serializedBytes;
|
|
||||||
|
|
||||||
if (includeIdentityNoSignature(messageVersion, context)) {
|
|
||||||
byte[] identityKey = IdentityKeyUtil.getIdentityKey(context, Curve.DJB_TYPE).serialize();
|
|
||||||
|
|
||||||
serializedBytes = Util.combine(versionBytes, publicKeyBytes, identityKey);
|
|
||||||
} else if (includeIdentitySignature(messageVersion, context)) {
|
|
||||||
byte[] prolog = Util.combine(versionBytes, publicKeyBytes);
|
|
||||||
|
|
||||||
serializedBytes = IdentityKeyUtil.getSignedKeyExchange(context, masterSecret, prolog);
|
|
||||||
} else {
|
|
||||||
serializedBytes = Util.combine(versionBytes, publicKeyBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (messageVersion < 1) this.serialized = Base64.encodeBytes(serializedBytes);
|
|
||||||
else this.serialized = Base64.encodeBytesWithoutPadding(serializedBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
public KeyExchangeMessageV1(String messageBody) throws InvalidVersionException, InvalidKeyException {
|
|
||||||
try {
|
|
||||||
byte[] keyBytes = Base64.decode(messageBody);
|
|
||||||
this.messageVersion = Conversions.highBitsToInt(keyBytes[0]);
|
|
||||||
this.supportedVersion = Conversions.lowBitsToInt(keyBytes[0]);
|
|
||||||
this.serialized = messageBody;
|
|
||||||
|
|
||||||
if (messageVersion > 1)
|
|
||||||
throw new InvalidVersionException("Legacy key exchange with version: " + messageVersion);
|
|
||||||
|
|
||||||
if (messageVersion >= 1)
|
|
||||||
keyBytes = Base64.decodeWithoutPadding(messageBody);
|
|
||||||
|
|
||||||
this.publicKey = new PublicKey(keyBytes, 1);
|
|
||||||
|
|
||||||
if (keyBytes.length <= PublicKey.KEY_SIZE + 1) {
|
|
||||||
this.identityKey = null;
|
|
||||||
} else if (messageVersion == 1) {
|
|
||||||
try {
|
|
||||||
this.identityKey = IdentityKeyUtil.verifySignedKeyExchange(keyBytes);
|
|
||||||
} catch (InvalidKeyException ike) {
|
|
||||||
Log.w("KeyUtil", ike);
|
|
||||||
this.identityKey = null;
|
|
||||||
}
|
|
||||||
} else if (messageVersion == 2) {
|
|
||||||
try {
|
|
||||||
this.identityKey = new IdentityKey(keyBytes, 1 + PublicKey.KEY_SIZE);
|
|
||||||
} catch (InvalidKeyException ike) {
|
|
||||||
Log.w("KeyUtil", ike);
|
|
||||||
this.identityKey = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (IOException ioe) {
|
|
||||||
throw new InvalidKeyException(ioe);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean includeIdentitySignature(int messageVersion, Context context) {
|
|
||||||
return IdentityKeyUtil.hasIdentityKey(context, Curve.NIST_TYPE) && (messageVersion == 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean includeIdentityNoSignature(int messageVersion, Context context) {
|
|
||||||
return IdentityKeyUtil.hasIdentityKey(context, Curve.DJB_TYPE) && (messageVersion >= 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isLegacy() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public IdentityKey getIdentityKey() {
|
|
||||||
return identityKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
public PublicKey getRemoteKey() {
|
|
||||||
return publicKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getMaxVersion() {
|
|
||||||
return supportedVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getVersion() {
|
|
||||||
return messageVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean hasIdentityKey() {
|
|
||||||
return identityKey != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String serialize() {
|
|
||||||
return serialized;
|
|
||||||
}
|
|
||||||
}
|
|
@ -7,6 +7,7 @@ import org.whispersystems.textsecure.crypto.IdentityKey;
|
|||||||
import org.whispersystems.textsecure.crypto.InvalidKeyException;
|
import org.whispersystems.textsecure.crypto.InvalidKeyException;
|
||||||
import org.whispersystems.textsecure.crypto.InvalidMessageException;
|
import org.whispersystems.textsecure.crypto.InvalidMessageException;
|
||||||
import org.whispersystems.textsecure.crypto.InvalidVersionException;
|
import org.whispersystems.textsecure.crypto.InvalidVersionException;
|
||||||
|
import org.whispersystems.textsecure.crypto.LegacyMessageException;
|
||||||
import org.whispersystems.textsecure.crypto.ecc.Curve;
|
import org.whispersystems.textsecure.crypto.ecc.Curve;
|
||||||
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
|
import org.whispersystems.textsecure.crypto.ecc.ECPublicKey;
|
||||||
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
|
import org.whispersystems.textsecure.crypto.protocol.CiphertextMessage;
|
||||||
@ -57,7 +58,7 @@ public class KeyExchangeMessageV2 extends KeyExchangeMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public KeyExchangeMessageV2(String serializedAndEncoded)
|
public KeyExchangeMessageV2(String serializedAndEncoded)
|
||||||
throws InvalidMessageException, InvalidVersionException
|
throws InvalidMessageException, InvalidVersionException, LegacyMessageException
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
byte[] serialized = Base64.decodeWithoutPadding(serializedAndEncoded);
|
byte[] serialized = Base64.decodeWithoutPadding(serializedAndEncoded);
|
||||||
@ -66,6 +67,10 @@ public class KeyExchangeMessageV2 extends KeyExchangeMessage {
|
|||||||
this.version = Conversions.highBitsToInt(parts[0][0]);
|
this.version = Conversions.highBitsToInt(parts[0][0]);
|
||||||
this.supportedVersion = Conversions.lowBitsToInt(parts[0][0]);
|
this.supportedVersion = Conversions.lowBitsToInt(parts[0][0]);
|
||||||
|
|
||||||
|
if (this.version <= CiphertextMessage.UNSUPPORTED_VERSION) {
|
||||||
|
throw new LegacyMessageException("Unsupported legacy version: " + this.version);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.version > CiphertextMessage.CURRENT_VERSION) {
|
if (this.version > CiphertextMessage.CURRENT_VERSION) {
|
||||||
throw new InvalidVersionException("Unknown version: " + this.version);
|
throw new InvalidVersionException("Unknown version: " + this.version);
|
||||||
}
|
}
|
||||||
@ -106,11 +111,6 @@ public class KeyExchangeMessageV2 extends KeyExchangeMessage {
|
|||||||
return ephemeralKey;
|
return ephemeralKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isLegacy() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IdentityKey getIdentityKey() {
|
public IdentityKey getIdentityKey() {
|
||||||
return identityKey;
|
return identityKey;
|
||||||
|
@ -89,12 +89,6 @@ public class IdentityDatabase extends Database {
|
|||||||
|
|
||||||
IdentityKey ourIdentity = new IdentityKey(Base64.decode(serializedIdentity), 0);
|
IdentityKey ourIdentity = new IdentityKey(Base64.decode(serializedIdentity), 0);
|
||||||
|
|
||||||
if (theirIdentity.getPublicKey().getType() == Curve.DJB_TYPE &&
|
|
||||||
ourIdentity.getPublicKey().getType() == Curve.NIST_TYPE)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ourIdentity.equals(theirIdentity);
|
return ourIdentity.equals(theirIdentity);
|
||||||
} else {
|
} else {
|
||||||
return true;
|
return true;
|
||||||
|
@ -362,6 +362,11 @@ public class MmsDatabase extends Database implements MmsSmsColumns {
|
|||||||
notifyConversationListeners(threadId);
|
notifyConversationListeners(threadId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void markAsLegacyVersion(long messageId, long threadId) {
|
||||||
|
updateMailboxBitmask(messageId, 0, Types.LEGACY_MESSAGE_BIT);
|
||||||
|
notifyConversationListeners(threadId);
|
||||||
|
}
|
||||||
|
|
||||||
public void setMessagesRead(long threadId) {
|
public void setMessagesRead(long threadId) {
|
||||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||||
ContentValues contentValues = new ContentValues();
|
ContentValues contentValues = new ContentValues();
|
||||||
|
@ -47,6 +47,7 @@ public interface MmsSmsColumns {
|
|||||||
protected static final long SECURE_MESSAGE_BIT = 0x800000;
|
protected static final long SECURE_MESSAGE_BIT = 0x800000;
|
||||||
protected static final long END_SESSION_BIT = 0x400000;
|
protected static final long END_SESSION_BIT = 0x400000;
|
||||||
protected static final long PUSH_MESSAGE_BIT = 0x200000;
|
protected static final long PUSH_MESSAGE_BIT = 0x200000;
|
||||||
|
protected static final long LEGACY_MESSAGE_BIT = 0x100000;
|
||||||
|
|
||||||
// Group Message Information
|
// Group Message Information
|
||||||
protected static final long GROUP_UPDATE_BIT = 0x10000;
|
protected static final long GROUP_UPDATE_BIT = 0x10000;
|
||||||
|
@ -170,6 +170,10 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
|
|||||||
updateTypeBitmask(id, 0, Types.KEY_EXCHANGE_INVALID_VERSION_BIT);
|
updateTypeBitmask(id, 0, Types.KEY_EXCHANGE_INVALID_VERSION_BIT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void markAsLegacyVersion(long id) {
|
||||||
|
updateTypeBitmask(id, 0, Types.LEGACY_MESSAGE_BIT);
|
||||||
|
}
|
||||||
|
|
||||||
public void markAsSecure(long id) {
|
public void markAsSecure(long id) {
|
||||||
updateTypeBitmask(id, 0, Types.SECURE_MESSAGE_BIT);
|
updateTypeBitmask(id, 0, Types.SECURE_MESSAGE_BIT);
|
||||||
}
|
}
|
||||||
@ -275,8 +279,9 @@ public class SmsDatabase extends Database implements MmsSmsColumns {
|
|||||||
else if (((IncomingKeyExchangeMessage)message).isProcessed()) type |= Types.KEY_EXCHANGE_PROCESSED_BIT;
|
else if (((IncomingKeyExchangeMessage)message).isProcessed()) type |= Types.KEY_EXCHANGE_PROCESSED_BIT;
|
||||||
else if (((IncomingKeyExchangeMessage)message).isCorrupted()) type |= Types.KEY_EXCHANGE_CORRUPTED_BIT;
|
else if (((IncomingKeyExchangeMessage)message).isCorrupted()) type |= Types.KEY_EXCHANGE_CORRUPTED_BIT;
|
||||||
else if (((IncomingKeyExchangeMessage)message).isInvalidVersion()) type |= Types.KEY_EXCHANGE_INVALID_VERSION_BIT;
|
else if (((IncomingKeyExchangeMessage)message).isInvalidVersion()) type |= Types.KEY_EXCHANGE_INVALID_VERSION_BIT;
|
||||||
else if (((IncomingKeyExchangeMessage)message).isPreKeyBundle()) type |= Types.KEY_EXCHANGE_BUNDLE_BIT;
|
|
||||||
else if (((IncomingKeyExchangeMessage)message).isIdentityUpdate()) type |= Types.KEY_EXCHANGE_IDENTITY_UPDATE_BIT;
|
else if (((IncomingKeyExchangeMessage)message).isIdentityUpdate()) type |= Types.KEY_EXCHANGE_IDENTITY_UPDATE_BIT;
|
||||||
|
else if (((IncomingKeyExchangeMessage)message).isLegacyVersion()) type |= Types.LEGACY_MESSAGE_BIT;
|
||||||
|
else if (((IncomingKeyExchangeMessage)message).isPreKeyBundle()) type |= Types.KEY_EXCHANGE_BUNDLE_BIT;
|
||||||
} else if (message.isSecureMessage()) {
|
} else if (message.isSecureMessage()) {
|
||||||
type |= Types.SECURE_MESSAGE_BIT;
|
type |= Types.SECURE_MESSAGE_BIT;
|
||||||
type |= Types.ENCRYPTION_REMOTE_BIT;
|
type |= Types.ENCRYPTION_REMOTE_BIT;
|
||||||
|
@ -76,7 +76,7 @@ public class PreKeyService extends Service {
|
|||||||
|
|
||||||
List<PreKeyRecord> preKeyRecords = PreKeyUtil.generatePreKeys(context, masterSecret);
|
List<PreKeyRecord> preKeyRecords = PreKeyUtil.generatePreKeys(context, masterSecret);
|
||||||
PreKeyRecord lastResortKeyRecord = PreKeyUtil.generateLastResortKey(context, masterSecret);
|
PreKeyRecord lastResortKeyRecord = PreKeyUtil.generateLastResortKey(context, masterSecret);
|
||||||
IdentityKey identityKey = IdentityKeyUtil.getIdentityKey(context, Curve.DJB_TYPE);
|
IdentityKey identityKey = IdentityKeyUtil.getIdentityKey(context);
|
||||||
|
|
||||||
Log.w(TAG, "Registering new prekeys...");
|
Log.w(TAG, "Registering new prekeys...");
|
||||||
|
|
||||||
|
@ -261,7 +261,7 @@ public class RegistrationService extends Service {
|
|||||||
throws GcmRegistrationTimeoutException, IOException
|
throws GcmRegistrationTimeoutException, IOException
|
||||||
{
|
{
|
||||||
setState(new RegistrationState(RegistrationState.STATE_GENERATING_KEYS, number));
|
setState(new RegistrationState(RegistrationState.STATE_GENERATING_KEYS, number));
|
||||||
IdentityKey identityKey = IdentityKeyUtil.getIdentityKey(this, Curve.DJB_TYPE);
|
IdentityKey identityKey = IdentityKeyUtil.getIdentityKey(this);
|
||||||
List<PreKeyRecord> records = PreKeyUtil.generatePreKeys(this, masterSecret);
|
List<PreKeyRecord> records = PreKeyUtil.generatePreKeys(this, masterSecret);
|
||||||
PreKeyRecord lastResort = PreKeyUtil.generateLastResortKey(this, masterSecret);
|
PreKeyRecord lastResort = PreKeyUtil.generateLastResortKey(this, masterSecret);
|
||||||
socket.registerPreKeys(identityKey, lastResort, records);
|
socket.registerPreKeys(identityKey, lastResort, records);
|
||||||
|
@ -26,6 +26,7 @@ import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor;
|
|||||||
import org.thoughtcrime.securesms.crypto.KeyExchangeProcessorV2;
|
import org.thoughtcrime.securesms.crypto.KeyExchangeProcessorV2;
|
||||||
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
|
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
|
||||||
import org.thoughtcrime.securesms.crypto.protocol.KeyExchangeMessage;
|
import org.thoughtcrime.securesms.crypto.protocol.KeyExchangeMessage;
|
||||||
|
import org.whispersystems.textsecure.crypto.LegacyMessageException;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
|
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
|
||||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||||
@ -188,6 +189,9 @@ public class SmsReceiver {
|
|||||||
} catch (RecipientFormattingException e) {
|
} catch (RecipientFormattingException e) {
|
||||||
Log.w("SmsReceiver", e);
|
Log.w("SmsReceiver", e);
|
||||||
message.setCorrupted(true);
|
message.setCorrupted(true);
|
||||||
|
} catch (LegacyMessageException e) {
|
||||||
|
Log.w("SmsReceiver", e);
|
||||||
|
message.setLegacyVersion(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ public class IncomingKeyExchangeMessage extends IncomingTextMessage {
|
|||||||
private boolean isProcessed;
|
private boolean isProcessed;
|
||||||
private boolean isCorrupted;
|
private boolean isCorrupted;
|
||||||
private boolean isInvalidVersion;
|
private boolean isInvalidVersion;
|
||||||
|
private boolean isLegacyVersion;
|
||||||
|
|
||||||
public IncomingKeyExchangeMessage(IncomingTextMessage base, String newBody) {
|
public IncomingKeyExchangeMessage(IncomingTextMessage base, String newBody) {
|
||||||
super(base, newBody);
|
super(base, newBody);
|
||||||
@ -15,6 +16,7 @@ public class IncomingKeyExchangeMessage extends IncomingTextMessage {
|
|||||||
this.isProcessed = ((IncomingKeyExchangeMessage)base).isProcessed;
|
this.isProcessed = ((IncomingKeyExchangeMessage)base).isProcessed;
|
||||||
this.isCorrupted = ((IncomingKeyExchangeMessage)base).isCorrupted;
|
this.isCorrupted = ((IncomingKeyExchangeMessage)base).isCorrupted;
|
||||||
this.isInvalidVersion = ((IncomingKeyExchangeMessage)base).isInvalidVersion;
|
this.isInvalidVersion = ((IncomingKeyExchangeMessage)base).isInvalidVersion;
|
||||||
|
this.isLegacyVersion = ((IncomingKeyExchangeMessage)base).isLegacyVersion;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,6 +25,10 @@ public class IncomingKeyExchangeMessage extends IncomingTextMessage {
|
|||||||
return new IncomingKeyExchangeMessage(this, messageBody);
|
return new IncomingKeyExchangeMessage(this, messageBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isIdentityUpdate() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isStale() {
|
public boolean isStale() {
|
||||||
return isStale;
|
return isStale;
|
||||||
}
|
}
|
||||||
@ -55,6 +61,14 @@ public class IncomingKeyExchangeMessage extends IncomingTextMessage {
|
|||||||
this.isInvalidVersion = isInvalidVersion;
|
this.isInvalidVersion = isInvalidVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isLegacyVersion() {
|
||||||
|
return isLegacyVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLegacyVersion(boolean isLegacyVersion) {
|
||||||
|
this.isLegacyVersion = isLegacyVersion;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isKeyExchange() {
|
public boolean isKeyExchange() {
|
||||||
return true;
|
return true;
|
||||||
|
@ -179,10 +179,6 @@ public class IncomingTextMessage implements Parcelable {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isIdentityUpdate() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isPush() {
|
public boolean isPush() {
|
||||||
return push;
|
return push;
|
||||||
}
|
}
|
||||||
|
@ -352,9 +352,9 @@ public class PushTransport extends BaseTransport {
|
|||||||
CiphertextMessage message = cipher.encrypt(plaintext);
|
CiphertextMessage message = cipher.encrypt(plaintext);
|
||||||
int remoteRegistrationId = cipher.getRemoteRegistrationId();
|
int remoteRegistrationId = cipher.getRemoteRegistrationId();
|
||||||
|
|
||||||
if (message.getType() == CiphertextMessage.PREKEY_WHISPER_TYPE) {
|
if (message.getType() == CiphertextMessage.PREKEY_TYPE) {
|
||||||
return new PushBody(IncomingPushMessageSignal.Type.PREKEY_BUNDLE_VALUE, remoteRegistrationId, message.serialize());
|
return new PushBody(IncomingPushMessageSignal.Type.PREKEY_BUNDLE_VALUE, remoteRegistrationId, message.serialize());
|
||||||
} else if (message.getType() == CiphertextMessage.CURRENT_WHISPER_TYPE) {
|
} else if (message.getType() == CiphertextMessage.WHISPER_TYPE) {
|
||||||
return new PushBody(IncomingPushMessageSignal.Type.CIPHERTEXT_VALUE, remoteRegistrationId, message.serialize());
|
return new PushBody(IncomingPushMessageSignal.Type.CIPHERTEXT_VALUE, remoteRegistrationId, message.serialize());
|
||||||
} else {
|
} else {
|
||||||
throw new AssertionError("Unknown ciphertext type: " + message.getType());
|
throw new AssertionError("Unknown ciphertext type: " + message.getType());
|
||||||
|
@ -183,7 +183,7 @@ public class SmsTransport extends BaseTransport {
|
|||||||
CiphertextMessage ciphertextMessage = sessionCipher.encrypt(paddedPlaintext);
|
CiphertextMessage ciphertextMessage = sessionCipher.encrypt(paddedPlaintext);
|
||||||
String encodedCiphertext = new String(transportDetails.getEncodedMessage(ciphertextMessage.serialize()));
|
String encodedCiphertext = new String(transportDetails.getEncodedMessage(ciphertextMessage.serialize()));
|
||||||
|
|
||||||
if (ciphertextMessage.getType() == CiphertextMessage.PREKEY_WHISPER_TYPE) {
|
if (ciphertextMessage.getType() == CiphertextMessage.PREKEY_TYPE) {
|
||||||
message = new OutgoingPrekeyBundleMessage(message, encodedCiphertext);
|
message = new OutgoingPrekeyBundleMessage(message, encodedCiphertext);
|
||||||
} else {
|
} else {
|
||||||
message = message.withBody(encodedCiphertext);
|
message = message.withBody(encodedCiphertext);
|
||||||
|
Loading…
Reference in New Issue
Block a user