mirror of
https://github.com/oxen-io/session-android.git
synced 2025-10-27 12:39:04 +00:00
Move common crypto classes into TextSecureLibrary.
1) Move all the crypto classes from securesms.crypto. 2) Move all the crypto storage from securesms.database.keys 3) Replace the old imported BC code with spongycastle.
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecure.crypto;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import org.spongycastle.crypto.params.ECPublicKeyParameters;
|
||||
import org.whispersystems.textsecure.util.Hex;
|
||||
|
||||
/**
|
||||
* A class for representing an identity key.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class IdentityKey implements Parcelable, SerializableKey {
|
||||
|
||||
public static final Parcelable.Creator<IdentityKey> CREATOR = new Parcelable.Creator<IdentityKey>() {
|
||||
public IdentityKey createFromParcel(Parcel in) {
|
||||
try {
|
||||
return new IdentityKey(in);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public IdentityKey[] newArray(int size) {
|
||||
return new IdentityKey[size];
|
||||
}
|
||||
};
|
||||
|
||||
public static final int SIZE = 1 + KeyUtil.POINT_SIZE;
|
||||
private static final int VERSION = 1;
|
||||
|
||||
private ECPublicKeyParameters publicKey;
|
||||
|
||||
public IdentityKey(ECPublicKeyParameters publicKey) {
|
||||
this.publicKey = publicKey;
|
||||
}
|
||||
|
||||
public IdentityKey(Parcel in) throws InvalidKeyException {
|
||||
int length = in.readInt();
|
||||
byte[] serialized = new byte[length];
|
||||
|
||||
in.readByteArray(serialized);
|
||||
initializeFromSerialized(serialized, 0);
|
||||
}
|
||||
|
||||
public IdentityKey(byte[] bytes, int offset) throws InvalidKeyException {
|
||||
initializeFromSerialized(bytes, offset);
|
||||
}
|
||||
|
||||
public ECPublicKeyParameters getPublicKeyParameters() {
|
||||
return this.publicKey;
|
||||
}
|
||||
|
||||
private void initializeFromSerialized(byte[] bytes, int offset) throws InvalidKeyException {
|
||||
int version = bytes[offset] & 0xff;
|
||||
|
||||
if (version > VERSION)
|
||||
throw new InvalidKeyException("Unsupported key version: " + version);
|
||||
|
||||
this.publicKey = KeyUtil.decodePoint(bytes, offset+1);
|
||||
}
|
||||
|
||||
public byte[] serialize() {
|
||||
byte[] encodedKey = KeyUtil.encodePoint(publicKey.getQ());
|
||||
byte[] combined = new byte[1 + encodedKey.length];
|
||||
|
||||
combined[0] = (byte)VERSION;
|
||||
System.arraycopy(encodedKey, 0, combined, 1, encodedKey.length);
|
||||
|
||||
return combined;
|
||||
}
|
||||
|
||||
public String getFingerprint() {
|
||||
return Hex.toString(serialize());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (!(other instanceof IdentityKey)) return false;
|
||||
return publicKey.getQ().equals(((IdentityKey)other).publicKey.getQ());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return publicKey.getQ().hashCode();
|
||||
}
|
||||
|
||||
public int describeContents() {
|
||||
// TODO Auto-generated method stub
|
||||
return 0;
|
||||
}
|
||||
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
byte[] serialized = this.serialize();
|
||||
dest.writeInt(serialized.length);
|
||||
dest.writeByteArray(serialized);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecure.crypto;
|
||||
|
||||
public class InvalidKeyException extends Exception {
|
||||
|
||||
public InvalidKeyException() {
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public InvalidKeyException(String detailMessage) {
|
||||
super(detailMessage);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public InvalidKeyException(Throwable throwable) {
|
||||
super(throwable);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public InvalidKeyException(String detailMessage, Throwable throwable) {
|
||||
super(detailMessage, throwable);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecure.crypto;
|
||||
|
||||
public class InvalidMacException extends Exception {
|
||||
|
||||
public InvalidMacException() {
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public InvalidMacException(String detailMessage) {
|
||||
super(detailMessage);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public InvalidMacException(Throwable throwable) {
|
||||
super(throwable);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public InvalidMacException(String detailMessage, Throwable throwable) {
|
||||
super(detailMessage, throwable);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecure.crypto;
|
||||
|
||||
public class InvalidMessageException extends Exception {
|
||||
|
||||
public InvalidMessageException() {
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public InvalidMessageException(String detailMessage) {
|
||||
super(detailMessage);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public InvalidMessageException(Throwable throwable) {
|
||||
super(throwable);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public InvalidMessageException(String detailMessage, Throwable throwable) {
|
||||
super(detailMessage, throwable);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecure.crypto;
|
||||
|
||||
import org.spongycastle.crypto.AsymmetricCipherKeyPair;
|
||||
import org.spongycastle.crypto.params.ECPrivateKeyParameters;
|
||||
import org.spongycastle.crypto.params.ECPublicKeyParameters;
|
||||
import org.whispersystems.textsecure.util.Hex;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* Represents a session's active KeyPair.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class KeyPair {
|
||||
|
||||
private ECPrivateKeyParameters privateKey;
|
||||
private PublicKey publicKey;
|
||||
|
||||
private final MasterCipher masterCipher;
|
||||
|
||||
public KeyPair(int keyPairId, AsymmetricCipherKeyPair keyPair, MasterSecret masterSecret) {
|
||||
this.masterCipher = new MasterCipher(masterSecret);
|
||||
this.publicKey = new PublicKey(keyPairId, (ECPublicKeyParameters)keyPair.getPublic());
|
||||
this.privateKey = (ECPrivateKeyParameters)keyPair.getPrivate();
|
||||
}
|
||||
|
||||
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 AsymmetricCipherKeyPair getKeyPair() {
|
||||
return new AsymmetricCipherKeyPair(publicKey.getKey(), 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(privateKeyBytes);
|
||||
}
|
||||
|
||||
public byte[] serialize() {
|
||||
byte[] publicKeyBytes = publicKey.serialize();
|
||||
Log.w("KeyPair", "Serialized public key bytes: " + Hex.toString(publicKeyBytes));
|
||||
byte[] privateKeyBytes = masterCipher.encryptKey(privateKey);
|
||||
byte[] combined = new byte[publicKeyBytes.length + privateKeyBytes.length];
|
||||
System.arraycopy(publicKeyBytes, 0, combined, 0, publicKeyBytes.length);
|
||||
System.arraycopy(privateKeyBytes, 0, combined, publicKeyBytes.length, privateKeyBytes.length);
|
||||
|
||||
return combined;
|
||||
}
|
||||
|
||||
}
|
||||
167
library/src/org/whispersystems/textsecure/crypto/KeyUtil.java
Normal file
167
library/src/org/whispersystems/textsecure/crypto/KeyUtil.java
Normal file
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecure.crypto;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
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.ECPublicKeyParameters;
|
||||
import org.spongycastle.math.ec.ECCurve;
|
||||
import org.spongycastle.math.ec.ECFieldElement;
|
||||
import org.spongycastle.math.ec.ECPoint;
|
||||
import org.whispersystems.textsecure.storage.CanonicalRecipientAddress;
|
||||
import org.whispersystems.textsecure.storage.LocalKeyRecord;
|
||||
import org.whispersystems.textsecure.storage.RemoteKeyRecord;
|
||||
import org.whispersystems.textsecure.storage.SessionRecord;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* Helper class for generating key pairs and calculating ECDH agreements.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class KeyUtil {
|
||||
|
||||
public 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);
|
||||
|
||||
public static final int POINT_SIZE = 33;
|
||||
|
||||
public static final ECDomainParameters domainParameters = new ECDomainParameters(curve, g, n);
|
||||
|
||||
public static byte[] encodePoint(ECPoint point) {
|
||||
synchronized (curve) {
|
||||
return point.getEncoded();
|
||||
}
|
||||
}
|
||||
|
||||
public static ECPublicKeyParameters decodePoint(byte[] encoded, int offset)
|
||||
throws InvalidKeyException
|
||||
{
|
||||
byte[] pointBytes = new byte[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 ECPublicKeyParameters(Q, KeyUtil.domainParameters);
|
||||
}
|
||||
}
|
||||
|
||||
public static BigInteger calculateAgreement(ECDHBasicAgreement agreement, ECPublicKeyParameters remoteKey) {
|
||||
synchronized (curve) {
|
||||
return agreement.calculateAgreement(remoteKey);
|
||||
}
|
||||
}
|
||||
|
||||
public static void abortSessionFor(Context context, CanonicalRecipientAddress recipient) {
|
||||
//XXX Obviously we should probably do something more thorough here eventually.
|
||||
Log.w("KeyUtil", "Aborting session, deleting keys...");
|
||||
LocalKeyRecord.delete(context, recipient);
|
||||
RemoteKeyRecord.delete(context, recipient);
|
||||
SessionRecord.delete(context, recipient);
|
||||
}
|
||||
|
||||
public static boolean isSessionFor(Context context, CanonicalRecipientAddress recipient) {
|
||||
Log.w("KeyUtil", "Checking session...");
|
||||
return
|
||||
(LocalKeyRecord.hasRecord(context, recipient)) &&
|
||||
(RemoteKeyRecord.hasRecord(context, recipient)) &&
|
||||
(SessionRecord.hasSession(context, recipient));
|
||||
}
|
||||
|
||||
public static boolean isIdentityKeyFor(Context context,
|
||||
MasterSecret masterSecret,
|
||||
CanonicalRecipientAddress recipient)
|
||||
{
|
||||
return isSessionFor(context, recipient) &&
|
||||
new SessionRecord(context, masterSecret, recipient).getIdentityKey() != null;
|
||||
}
|
||||
|
||||
public static LocalKeyRecord initializeRecordFor(CanonicalRecipientAddress recipient,
|
||||
Context context,
|
||||
MasterSecret masterSecret)
|
||||
{
|
||||
Log.w("KeyUtil", "Initializing local key pairs...");
|
||||
try {
|
||||
SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
|
||||
int initialId = secureRandom.nextInt(4094) + 1;
|
||||
|
||||
KeyPair currentPair = new KeyPair(initialId, KeyUtil.generateKeyPair(), masterSecret);
|
||||
KeyPair nextPair = new KeyPair(initialId + 1, KeyUtil.generateKeyPair(), 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);
|
||||
}
|
||||
}
|
||||
|
||||
public static AsymmetricCipherKeyPair generateKeyPair() {
|
||||
try {
|
||||
synchronized (curve) {
|
||||
ECKeyGenerationParameters keyParamters = new ECKeyGenerationParameters(domainParameters, SecureRandom.getInstance("SHA1PRNG"));
|
||||
ECKeyPairGenerator generator = new ECKeyPairGenerator();
|
||||
generator.init(keyParamters);
|
||||
|
||||
AsymmetricCipherKeyPair keyPair = generator.generateKeyPair();
|
||||
|
||||
return cloneKeyPairWithPointCompression(keyPair);
|
||||
}
|
||||
} catch (NoSuchAlgorithmException nsae) {
|
||||
Log.w("keyutil", nsae);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecure.crypto;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.spongycastle.crypto.params.ECPrivateKeyParameters;
|
||||
import org.whispersystems.textsecure.util.Base64;
|
||||
import org.whispersystems.textsecure.util.Hex;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* Class that handles encryption for local storage.
|
||||
*
|
||||
* The protocol format is roughly:
|
||||
*
|
||||
* 1) 16 byte random IV.
|
||||
* 2) AES-CBC(plaintext)
|
||||
* 3) HMAC-SHA1 of 1 and 2
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class MasterCipher {
|
||||
|
||||
private final MasterSecret masterSecret;
|
||||
private final Cipher encryptingCipher;
|
||||
private final Cipher decryptingCipher;
|
||||
private final Mac hmac;
|
||||
|
||||
public MasterCipher(MasterSecret masterSecret) {
|
||||
try {
|
||||
this.masterSecret = masterSecret;
|
||||
this.encryptingCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
this.decryptingCipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
this.hmac = Mac.getInstance("HmacSHA1");
|
||||
} catch (NoSuchPaddingException nspe) {
|
||||
throw new AssertionError(nspe);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] encryptKey(ECPrivateKeyParameters params) {
|
||||
BigInteger d = params.getD();
|
||||
byte[] dBytes = d.toByteArray();
|
||||
return encryptBytes(dBytes);
|
||||
}
|
||||
|
||||
public String encryptBody(String body) {
|
||||
return encryptAndEncodeBytes(body.getBytes());
|
||||
}
|
||||
|
||||
public String decryptBody(String body) throws InvalidMessageException {
|
||||
return new String(decodeAndDecryptBytes(body));
|
||||
}
|
||||
|
||||
public ECPrivateKeyParameters decryptKey(byte[] key) {
|
||||
try {
|
||||
BigInteger d = new BigInteger(decryptBytes(key));
|
||||
return new ECPrivateKeyParameters(d, KeyUtil.domainParameters);
|
||||
} catch (InvalidMessageException ime) {
|
||||
Log.w("bodycipher", ime);
|
||||
return null; // XXX
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] decryptBytes(byte[] decodedBody) throws InvalidMessageException {
|
||||
try {
|
||||
Mac mac = getMac(masterSecret.getMacKey());
|
||||
byte[] encryptedBody = verifyMacBody(mac, decodedBody);
|
||||
|
||||
Cipher cipher = getDecryptingCipher(masterSecret.getEncryptionKey(), encryptedBody);
|
||||
byte[] encrypted = getDecryptedBody(cipher, encryptedBody);
|
||||
|
||||
return encrypted;
|
||||
} catch (GeneralSecurityException ge) {
|
||||
throw new InvalidMessageException(ge);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] encryptBytes(byte[] body) {
|
||||
try {
|
||||
Cipher cipher = getEncryptingCipher(masterSecret.getEncryptionKey());
|
||||
Mac mac = getMac(masterSecret.getMacKey());
|
||||
|
||||
byte[] encryptedBody = getEncryptedBody(cipher, body);
|
||||
byte[] encryptedAndMacBody = getMacBody(mac, encryptedBody);
|
||||
|
||||
return encryptedAndMacBody;
|
||||
} catch (GeneralSecurityException ge) {
|
||||
Log.w("bodycipher", ge);
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public boolean verifyMacFor(String content, byte[] theirMac) {
|
||||
byte[] ourMac = getMacFor(content);
|
||||
Log.w("MasterCipher", "Our Mac: " + Hex.toString(ourMac));
|
||||
Log.w("MasterCipher", "Thr Mac: " + Hex.toString(theirMac));
|
||||
return Arrays.equals(ourMac, theirMac);
|
||||
}
|
||||
|
||||
public byte[] getMacFor(String content) {
|
||||
Log.w("MasterCipher", "Macing: " + content);
|
||||
try {
|
||||
Mac mac = getMac(masterSecret.getMacKey());
|
||||
return mac.doFinal(content.getBytes());
|
||||
} catch (GeneralSecurityException ike) {
|
||||
throw new AssertionError(ike);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] decodeAndDecryptBytes(String body) throws InvalidMessageException {
|
||||
try {
|
||||
byte[] decodedBody = Base64.decode(body);
|
||||
return decryptBytes(decodedBody);
|
||||
} catch (IOException e) {
|
||||
throw new InvalidMessageException("Bad Base64 Encoding...", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String encryptAndEncodeBytes(byte[] bytes) {
|
||||
byte[] encryptedAndMacBody = encryptBytes(bytes);
|
||||
return Base64.encodeBytes(encryptedAndMacBody);
|
||||
}
|
||||
|
||||
private byte[] verifyMacBody(Mac hmac, byte[] encryptedAndMac) throws InvalidMessageException {
|
||||
byte[] encrypted = new byte[encryptedAndMac.length - hmac.getMacLength()];
|
||||
System.arraycopy(encryptedAndMac, 0, encrypted, 0, encrypted.length);
|
||||
|
||||
byte[] remoteMac = new byte[hmac.getMacLength()];
|
||||
System.arraycopy(encryptedAndMac, encryptedAndMac.length - remoteMac.length, remoteMac, 0, remoteMac.length);
|
||||
|
||||
byte[] localMac = hmac.doFinal(encrypted);
|
||||
|
||||
if (!Arrays.equals(remoteMac, localMac))
|
||||
throw new InvalidMessageException("MAC doesen't match.");
|
||||
|
||||
return encrypted;
|
||||
}
|
||||
|
||||
private byte[] getDecryptedBody(Cipher cipher, byte[] encryptedBody) throws IllegalBlockSizeException, BadPaddingException {
|
||||
return cipher.doFinal(encryptedBody, cipher.getBlockSize(), encryptedBody.length - cipher.getBlockSize());
|
||||
}
|
||||
|
||||
private byte[] getEncryptedBody(Cipher cipher, byte[] body) throws IllegalBlockSizeException, BadPaddingException {
|
||||
byte[] encrypted = cipher.doFinal(body);
|
||||
byte[] iv = cipher.getIV();
|
||||
|
||||
byte[] ivAndBody = new byte[iv.length + encrypted.length];
|
||||
System.arraycopy(iv, 0, ivAndBody, 0, iv.length);
|
||||
System.arraycopy(encrypted, 0, ivAndBody, iv.length, encrypted.length);
|
||||
|
||||
return ivAndBody;
|
||||
}
|
||||
|
||||
private Mac getMac(SecretKeySpec key) throws NoSuchAlgorithmException, InvalidKeyException {
|
||||
// Mac hmac = Mac.getInstance("HmacSHA1");
|
||||
hmac.init(key);
|
||||
|
||||
return hmac;
|
||||
}
|
||||
|
||||
private byte[] getMacBody(Mac hmac, byte[] encryptedBody) {
|
||||
byte[] mac = hmac.doFinal(encryptedBody);
|
||||
byte[] encryptedAndMac = new byte[encryptedBody.length + mac.length];
|
||||
|
||||
System.arraycopy(encryptedBody, 0, encryptedAndMac, 0, encryptedBody.length);
|
||||
System.arraycopy(mac, 0, encryptedAndMac, encryptedBody.length, mac.length);
|
||||
|
||||
return encryptedAndMac;
|
||||
}
|
||||
|
||||
private Cipher getDecryptingCipher(SecretKeySpec key, byte[] encryptedBody) throws InvalidKeyException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchPaddingException {
|
||||
// Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
IvParameterSpec iv = new IvParameterSpec(encryptedBody, 0, decryptingCipher.getBlockSize());
|
||||
decryptingCipher.init(Cipher.DECRYPT_MODE, key, iv);
|
||||
|
||||
return decryptingCipher;
|
||||
}
|
||||
|
||||
private Cipher getEncryptingCipher(SecretKeySpec key) throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException {
|
||||
// Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
encryptingCipher.init(Cipher.ENCRYPT_MODE, key);
|
||||
|
||||
return encryptingCipher;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecure.crypto;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* When a user first initializes TextSecure, a few secrets
|
||||
* are generated. These are:
|
||||
*
|
||||
* 1) A 128bit symmetric encryption key.
|
||||
* 2) A 160bit symmetric MAC key.
|
||||
* 3) An ECC keypair.
|
||||
*
|
||||
* The first two, along with the ECC keypair's private key, are
|
||||
* then encrypted on disk using PBE.
|
||||
*
|
||||
* This class represents 1 and 2.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class MasterSecret implements Parcelable {
|
||||
|
||||
private final SecretKeySpec encryptionKey;
|
||||
private final SecretKeySpec macKey;
|
||||
|
||||
public static final Parcelable.Creator<MasterSecret> CREATOR = new Parcelable.Creator<MasterSecret>() {
|
||||
@Override
|
||||
public MasterSecret createFromParcel(Parcel in) {
|
||||
return new MasterSecret(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MasterSecret[] newArray(int size) {
|
||||
return new MasterSecret[size];
|
||||
}
|
||||
};
|
||||
|
||||
public MasterSecret(SecretKeySpec encryptionKey, SecretKeySpec macKey) {
|
||||
this.encryptionKey = encryptionKey;
|
||||
this.macKey = macKey;
|
||||
}
|
||||
|
||||
private MasterSecret(Parcel in) {
|
||||
byte[] encryptionKeyBytes = new byte[in.readInt()];
|
||||
in.readByteArray(encryptionKeyBytes);
|
||||
|
||||
byte[] macKeyBytes = new byte[in.readInt()];
|
||||
in.readByteArray(macKeyBytes);
|
||||
|
||||
this.encryptionKey = new SecretKeySpec(encryptionKeyBytes, "AES");
|
||||
this.macKey = new SecretKeySpec(macKeyBytes, "HmacSHA1");
|
||||
|
||||
// SecretKeySpec does an internal copy in its constructor.
|
||||
Arrays.fill(encryptionKeyBytes, (byte) 0x00);
|
||||
Arrays.fill(macKeyBytes, (byte)0x00);
|
||||
}
|
||||
|
||||
|
||||
public SecretKeySpec getEncryptionKey() {
|
||||
return this.encryptionKey;
|
||||
}
|
||||
|
||||
public SecretKeySpec getMacKey() {
|
||||
return this.macKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel out, int flags) {
|
||||
out.writeInt(encryptionKey.getEncoded().length);
|
||||
out.writeByteArray(encryptionKey.getEncoded());
|
||||
out.writeInt(macKey.getEncoded().length);
|
||||
out.writeByteArray(macKey.getEncoded());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public MasterSecret parcelClone() {
|
||||
Parcel thisParcel = Parcel.obtain();
|
||||
Parcel thatParcel = Parcel.obtain();
|
||||
byte[] bytes = null;
|
||||
|
||||
thisParcel.writeValue(this);
|
||||
bytes = thisParcel.marshall();
|
||||
|
||||
thatParcel.unmarshall(bytes, 0, bytes.length);
|
||||
thatParcel.setDataPosition(0);
|
||||
|
||||
MasterSecret that = (MasterSecret)thatParcel.readValue(MasterSecret.class.getClassLoader());
|
||||
|
||||
thisParcel.recycle();
|
||||
thatParcel.recycle();
|
||||
|
||||
return that;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecure.crypto;
|
||||
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.whispersystems.textsecure.util.Hex;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
public class MessageMac {
|
||||
|
||||
public static final int MAC_LENGTH = 10;
|
||||
|
||||
private static byte[] calculateMac(byte[] message, int offset, int length, SecretKeySpec macKey) {
|
||||
try {
|
||||
Mac mac = Mac.getInstance("HmacSHA1");
|
||||
mac.init(macKey);
|
||||
|
||||
assert(mac.getMacLength() >= MAC_LENGTH);
|
||||
|
||||
mac.update(message, offset, length);
|
||||
byte[] macBytes = mac.doFinal();
|
||||
byte[] truncatedMacBytes = new byte[MAC_LENGTH];
|
||||
System.arraycopy(macBytes, 0, truncatedMacBytes, 0, truncatedMacBytes.length);
|
||||
|
||||
return truncatedMacBytes;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] buildMessageWithMac(byte[] message, SecretKeySpec macKey) {
|
||||
byte[] macBytes = calculateMac(message, 0, message.length, macKey);
|
||||
byte[] combined = new byte[macBytes.length + message.length];
|
||||
System.arraycopy(message, 0, combined, 0, message.length);
|
||||
System.arraycopy(macBytes, 0, combined, message.length, macBytes.length);
|
||||
|
||||
return combined;
|
||||
}
|
||||
|
||||
public static byte[] getMessageWithoutMac(byte[] message) throws InvalidMacException {
|
||||
if (message == null || message.length <= MAC_LENGTH)
|
||||
throw new InvalidMacException("Message shorter than MAC!");
|
||||
|
||||
byte[] strippedMessage = new byte[message.length - MAC_LENGTH];
|
||||
System.arraycopy(message, 0, strippedMessage, 0, strippedMessage.length);
|
||||
return strippedMessage;
|
||||
}
|
||||
|
||||
public static void verifyMac(byte[] message, SecretKeySpec macKey) throws InvalidMacException {
|
||||
byte[] localMacBytes = calculateMac(message, 0, message.length - MAC_LENGTH, macKey);
|
||||
byte[] receivedMacBytes = new byte[MAC_LENGTH];
|
||||
|
||||
System.arraycopy(message, message.length-MAC_LENGTH, receivedMacBytes, 0, receivedMacBytes.length);
|
||||
|
||||
Log.w("mm", "Local Mac: " + Hex.toString(localMacBytes));
|
||||
Log.w("mm", "Remot Mac: " + Hex.toString(receivedMacBytes));
|
||||
|
||||
if (!Arrays.equals(localMacBytes, receivedMacBytes))
|
||||
throw new InvalidMacException("MAC on message does not match calculated MAC.");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package org.whispersystems.textsecure.crypto;
|
||||
|
||||
import org.spongycastle.crypto.AsymmetricCipherKeyPair;
|
||||
import org.spongycastle.crypto.params.ECPrivateKeyParameters;
|
||||
import org.spongycastle.crypto.params.ECPublicKeyParameters;
|
||||
import org.whispersystems.textsecure.util.Util;
|
||||
|
||||
public class PreKeyPair {
|
||||
|
||||
private final MasterCipher masterCipher;
|
||||
private final ECPrivateKeyParameters privateKey;
|
||||
private final ECPublicKeyParameters publicKey;
|
||||
|
||||
public PreKeyPair(MasterSecret masterSecret, AsymmetricCipherKeyPair keyPair) {
|
||||
this.masterCipher = new MasterCipher(masterSecret);
|
||||
this.publicKey = (ECPublicKeyParameters)keyPair.getPublic();
|
||||
this.privateKey = (ECPrivateKeyParameters)keyPair.getPrivate();
|
||||
}
|
||||
|
||||
public PreKeyPair(MasterSecret masterSecret, byte[] serialized) throws InvalidKeyException {
|
||||
if (serialized.length < KeyUtil.POINT_SIZE + 1)
|
||||
throw new InvalidKeyException("Serialized length: " + serialized.length);
|
||||
|
||||
byte[] privateKeyBytes = new byte[serialized.length - KeyUtil.POINT_SIZE];
|
||||
System.arraycopy(serialized, KeyUtil.POINT_SIZE, privateKeyBytes, 0, privateKeyBytes.length);
|
||||
|
||||
this.masterCipher = new MasterCipher(masterSecret);
|
||||
this.publicKey = KeyUtil.decodePoint(serialized, 0);
|
||||
this.privateKey = masterCipher.decryptKey(privateKeyBytes);
|
||||
}
|
||||
|
||||
public ECPublicKeyParameters getPublicKey() {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
public byte[] serialize() {
|
||||
byte[] publicKeyBytes = KeyUtil.encodePoint(publicKey.getQ());
|
||||
byte[] privateKeyBytes = masterCipher.encryptKey(privateKey);
|
||||
|
||||
return Util.combine(publicKeyBytes, privateKeyBytes);
|
||||
}
|
||||
}
|
||||
113
library/src/org/whispersystems/textsecure/crypto/PreKeyUtil.java
Normal file
113
library/src/org/whispersystems/textsecure/crypto/PreKeyUtil.java
Normal file
@@ -0,0 +1,113 @@
|
||||
package org.whispersystems.textsecure.crypto;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
import org.whispersystems.textsecure.encoded.PreKeyProtos.PreKeyEntity;
|
||||
import org.whispersystems.textsecure.push.PreKeyList;
|
||||
import org.whispersystems.textsecure.storage.InvalidKeyIdException;
|
||||
import org.whispersystems.textsecure.storage.PreKeyRecord;
|
||||
import org.whispersystems.textsecure.util.Base64;
|
||||
|
||||
import java.io.File;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class PreKeyUtil {
|
||||
|
||||
public static final int BATCH_SIZE = 70;
|
||||
|
||||
public static List<PreKeyRecord> generatePreKeys(Context context, MasterSecret masterSecret) {
|
||||
List<PreKeyRecord> records = new LinkedList<PreKeyRecord>();
|
||||
long preKeyIdOffset = getNextPreKeyId(context);
|
||||
|
||||
for (int i=0;i<BATCH_SIZE;i++) {
|
||||
Log.w("PreKeyUtil", "Generating PreKey: " + (preKeyIdOffset + i));
|
||||
PreKeyPair keyPair = new PreKeyPair(masterSecret, KeyUtil.generateKeyPair());
|
||||
PreKeyRecord record = new PreKeyRecord(context, masterSecret, preKeyIdOffset + i, keyPair);
|
||||
|
||||
record.save();
|
||||
records.add(record);
|
||||
}
|
||||
|
||||
return records;
|
||||
}
|
||||
|
||||
public static List<PreKeyRecord> getPreKeys(Context context, MasterSecret masterSecret) {
|
||||
List<PreKeyRecord> records = new LinkedList<PreKeyRecord>();
|
||||
File directory = getPreKeysDirectory(context);
|
||||
String[] keyRecordIds = directory.list();
|
||||
|
||||
for (String keyRecordId : keyRecordIds) {
|
||||
try {
|
||||
records.add(new PreKeyRecord(context, masterSecret, Long.parseLong(keyRecordId)));
|
||||
} catch (InvalidKeyIdException e) {
|
||||
Log.w("PreKeyUtil", e);
|
||||
new File(getPreKeysDirectory(context), keyRecordId).delete();
|
||||
} catch (NumberFormatException nfe) {
|
||||
Log.w("PreKeyUtil", nfe);
|
||||
new File(getPreKeysDirectory(context), keyRecordId).delete();
|
||||
}
|
||||
}
|
||||
|
||||
return records;
|
||||
}
|
||||
|
||||
public static void clearPreKeys(Context context) {
|
||||
File directory = getPreKeysDirectory(context);
|
||||
String[] keyRecords = directory.list();
|
||||
|
||||
for (String keyRecord : keyRecords) {
|
||||
new File(directory, keyRecord).delete();
|
||||
}
|
||||
}
|
||||
|
||||
public static PreKeyList toJson(List<PreKeyRecord> records) {
|
||||
List<String> encoded = new LinkedList<String>();
|
||||
|
||||
for (PreKeyRecord record : records) {
|
||||
PreKeyEntity entity = PreKeyEntity.newBuilder().setId(record.getId())
|
||||
.setKey(ByteString.copyFrom(KeyUtil.encodePoint(record.getKeyPair().getPublicKey().getQ())))
|
||||
.build();
|
||||
|
||||
String encodedEntity = Base64.encodeBytesWithoutPadding(entity.toByteArray());
|
||||
|
||||
encoded.add(encodedEntity);
|
||||
}
|
||||
|
||||
return new PreKeyList(encoded);
|
||||
}
|
||||
|
||||
private static long getNextPreKeyId(Context context) {
|
||||
try {
|
||||
File directory = getPreKeysDirectory(context);
|
||||
String[] keyRecordIds = directory.list();
|
||||
long nextPreKeyId = 0;
|
||||
|
||||
for (String keyRecordId : keyRecordIds) {
|
||||
if (Long.parseLong(keyRecordId) > nextPreKeyId)
|
||||
nextPreKeyId = Long.parseLong(keyRecordId);
|
||||
}
|
||||
|
||||
if (nextPreKeyId == 0)
|
||||
nextPreKeyId = SecureRandom.getInstance("SHA1PRNG").nextInt(Integer.MAX_VALUE/2);
|
||||
|
||||
return nextPreKeyId;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static File getPreKeysDirectory(Context context) {
|
||||
File directory = new File(context.getFilesDir(), PreKeyRecord.PREKEY_DIRECTORY);
|
||||
|
||||
if (!directory.exists())
|
||||
directory.mkdirs();
|
||||
|
||||
return directory;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecure.crypto;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.spongycastle.crypto.params.ECPublicKeyParameters;
|
||||
import org.whispersystems.textsecure.util.Hex;
|
||||
import org.whispersystems.textsecure.util.Conversions;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
public class PublicKey {
|
||||
public static final int KEY_SIZE = 3 + KeyUtil.POINT_SIZE;
|
||||
|
||||
private final ECPublicKeyParameters publicKey;
|
||||
private int id;
|
||||
|
||||
public PublicKey(PublicKey publicKey) {
|
||||
this.id = publicKey.id;
|
||||
|
||||
// FIXME :: This not strictly an accurate copy constructor.
|
||||
this.publicKey = publicKey.publicKey;
|
||||
}
|
||||
|
||||
public PublicKey(int id, ECPublicKeyParameters publicKey) {
|
||||
this.publicKey = publicKey;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public PublicKey(byte[] bytes, int offset) throws InvalidKeyException {
|
||||
Log.w("PublicKey", "PublicKey Length: " + (bytes.length - offset));
|
||||
if ((bytes.length - offset) < KEY_SIZE)
|
||||
throw new InvalidKeyException("Provided bytes are too short.");
|
||||
|
||||
this.id = Conversions.byteArrayToMedium(bytes, offset);
|
||||
this.publicKey = KeyUtil.decodePoint(bytes, offset + 3);
|
||||
}
|
||||
|
||||
public PublicKey(byte[] bytes) throws InvalidKeyException {
|
||||
this(bytes, 0);
|
||||
}
|
||||
|
||||
public void setId(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public ECPublicKeyParameters getKey() {
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
public String getFingerprint() {
|
||||
return Hex.toString(getFingerprintBytes());
|
||||
}
|
||||
|
||||
public byte[] getFingerprintBytes() {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-1");
|
||||
return md.digest(serialize());
|
||||
} catch (NoSuchAlgorithmException nsae) {
|
||||
Log.w("LocalKeyPair", nsae);
|
||||
throw new IllegalArgumentException("SHA-1 isn't supported!");
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] serialize() {
|
||||
byte[] complete = new byte[KEY_SIZE];
|
||||
byte[] serializedPoint = KeyUtil.encodePoint(publicKey.getQ());
|
||||
|
||||
Log.w("PublicKey", "Serializing public key point: " + Hex.toString(serializedPoint));
|
||||
|
||||
Conversions.mediumToByteArray(complete, 0, id);
|
||||
System.arraycopy(serializedPoint, 0, complete, 3, serializedPoint.length);
|
||||
|
||||
return complete;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecure.crypto;
|
||||
|
||||
public interface SerializableKey {
|
||||
public byte[] serialize();
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecure.crypto;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import org.spongycastle.crypto.AsymmetricCipherKeyPair;
|
||||
import org.spongycastle.crypto.agreement.ECDHBasicAgreement;
|
||||
import org.spongycastle.crypto.params.ECPublicKeyParameters;
|
||||
import org.whispersystems.textsecure.protocol.Message;
|
||||
import org.whispersystems.textsecure.storage.CanonicalRecipientAddress;
|
||||
import org.whispersystems.textsecure.storage.InvalidKeyIdException;
|
||||
import org.whispersystems.textsecure.storage.LocalKeyRecord;
|
||||
import org.whispersystems.textsecure.storage.RemoteKeyRecord;
|
||||
import org.whispersystems.textsecure.storage.SessionKey;
|
||||
import org.whispersystems.textsecure.storage.SessionRecord;
|
||||
import org.whispersystems.textsecure.util.Conversions;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
/**
|
||||
* This is where the session encryption magic happens. Implements a compressed version of the OTR protocol.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class SessionCipher {
|
||||
|
||||
public static final Object CIPHER_LOCK = new Object();
|
||||
|
||||
public static final int CIPHER_KEY_LENGTH = 16;
|
||||
public static final int MAC_KEY_LENGTH = 20;
|
||||
|
||||
public static final int ENCRYPTED_MESSAGE_OVERHEAD = Message.HEADER_LENGTH + MessageMac.MAC_LENGTH;
|
||||
// public static final int ENCRYPTED_SINGLE_MESSAGE_BODY_MAX_SIZE = SmsTransportDetails.SINGLE_MESSAGE_MAX_BYTES - ENCRYPTED_MESSAGE_OVERHEAD;
|
||||
|
||||
private final LocalKeyRecord localRecord;
|
||||
private final RemoteKeyRecord remoteRecord;
|
||||
private final SessionRecord sessionRecord;
|
||||
private final MasterSecret masterSecret;
|
||||
private final TransportDetails transportDetails;
|
||||
|
||||
public SessionCipher(Context context, MasterSecret masterSecret, CanonicalRecipientAddress recipient, TransportDetails transportDetails) {
|
||||
Log.w("SessionCipher", "Constructing session cipher...");
|
||||
this.masterSecret = masterSecret;
|
||||
this.localRecord = new LocalKeyRecord(context, masterSecret, recipient);
|
||||
this.remoteRecord = new RemoteKeyRecord(context, recipient);
|
||||
this.sessionRecord = new SessionRecord(context, masterSecret, recipient);
|
||||
this.transportDetails = transportDetails;
|
||||
}
|
||||
|
||||
public byte[] encryptMessage(byte[] messageText) {
|
||||
Log.w("SessionCipher", "Encrypting message...");
|
||||
try {
|
||||
int localId = localRecord.getCurrentKeyPair().getId();
|
||||
int remoteId = remoteRecord.getCurrentRemoteKey().getId();
|
||||
SessionKey sessionKey = getSessionKey(Cipher.ENCRYPT_MODE, localId, remoteId);
|
||||
byte[]paddedMessage = transportDetails.getPaddedMessageBody(messageText);
|
||||
byte[]cipherText = getCiphertext(paddedMessage, sessionKey.getCipherKey());
|
||||
byte[]message = buildMessageFromCiphertext(cipherText);
|
||||
byte[]messageWithMac = MessageMac.buildMessageWithMac(message, sessionKey.getMacKey());
|
||||
|
||||
sessionRecord.setSessionKey(sessionKey);
|
||||
sessionRecord.incrementCounter();
|
||||
sessionRecord.save();
|
||||
|
||||
return transportDetails.encodeMessage(messageWithMac);
|
||||
} catch (IllegalBlockSizeException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
} catch (BadPaddingException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
} catch (InvalidKeyIdException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public byte[] decryptMessage(byte[] messageText) throws InvalidMessageException {
|
||||
Log.w("SessionCipher", "Decrypting message...");
|
||||
try {
|
||||
byte[] decodedMessage = transportDetails.decodeMessage(messageText);
|
||||
Message message = new Message(MessageMac.getMessageWithoutMac(decodedMessage));
|
||||
SessionKey sessionKey = getSessionKey(Cipher.DECRYPT_MODE, message.getReceiverKeyId(), message.getSenderKeyId());
|
||||
|
||||
MessageMac.verifyMac(decodedMessage, sessionKey.getMacKey());
|
||||
|
||||
byte[] plaintextWithPadding = getPlaintext(message.getMessageText(), sessionKey.getCipherKey(), message.getCounter());
|
||||
byte[] plaintext = transportDetails.stripPaddedMessage(plaintextWithPadding);
|
||||
|
||||
remoteRecord.updateCurrentRemoteKey(message.getNextKey());
|
||||
remoteRecord.save();
|
||||
|
||||
localRecord.advanceKeyIfNecessary(message.getReceiverKeyId());
|
||||
localRecord.save();
|
||||
|
||||
sessionRecord.setSessionKey(sessionKey);
|
||||
sessionRecord.setSessionVersion(message.getHighestMutuallySupportedVersion());
|
||||
sessionRecord.save();
|
||||
|
||||
return plaintext;
|
||||
} catch (IOException e) {
|
||||
throw new InvalidMessageException("Encoding Failure", e);
|
||||
} catch (InvalidKeyIdException e) {
|
||||
throw new InvalidMessageException("Bad Key ID", e);
|
||||
} catch (InvalidMacException e) {
|
||||
throw new InvalidMessageException("Bad MAC", e);
|
||||
} catch (IllegalBlockSizeException e) {
|
||||
throw new InvalidMessageException("assert", e);
|
||||
} catch (BadPaddingException e) {
|
||||
throw new InvalidMessageException("assert", e);
|
||||
}
|
||||
}
|
||||
|
||||
private SecretKeySpec deriveMacSecret(SecretKeySpec key) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-1");
|
||||
byte[] secret = md.digest(key.getEncoded());
|
||||
|
||||
return new SecretKeySpec(secret, "HmacSHA1");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalArgumentException("SHA-1 Not Supported!",e);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] buildMessageFromCiphertext(byte[] cipherText) {
|
||||
Message message = new Message(localRecord.getCurrentKeyPair().getId(),
|
||||
remoteRecord.getCurrentRemoteKey().getId(),
|
||||
localRecord.getNextKeyPair().getPublicKey(),
|
||||
sessionRecord.getCounter(),
|
||||
cipherText, sessionRecord.getSessionVersion(), Message.SUPPORTED_VERSION);
|
||||
|
||||
return message.serialize();
|
||||
}
|
||||
|
||||
|
||||
private byte[] getPlaintext(byte[] cipherText, SecretKeySpec key, int counter) throws IllegalBlockSizeException, BadPaddingException {
|
||||
Cipher cipher = getCipher(Cipher.DECRYPT_MODE, key, counter);
|
||||
return cipher.doFinal(cipherText);
|
||||
}
|
||||
|
||||
private byte[] getCiphertext(byte[] message, SecretKeySpec key) throws IllegalBlockSizeException, BadPaddingException {
|
||||
Cipher cipher = getCipher(Cipher.ENCRYPT_MODE, key, sessionRecord.getCounter());
|
||||
return cipher.doFinal(message);
|
||||
}
|
||||
|
||||
private Cipher getCipher(int mode, SecretKeySpec key, int counter) {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
|
||||
|
||||
byte[] ivBytes = new byte[16];
|
||||
Conversions.mediumToByteArray(ivBytes, 0, counter);
|
||||
|
||||
IvParameterSpec iv = new IvParameterSpec(ivBytes);
|
||||
cipher.init(mode, key, iv);
|
||||
|
||||
return cipher;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalArgumentException("AES Not Supported!");
|
||||
} catch (NoSuchPaddingException e) {
|
||||
throw new IllegalArgumentException("NoPadding Not Supported!");
|
||||
} catch (InvalidKeyException e) {
|
||||
Log.w("SessionCipher", e);
|
||||
throw new IllegalArgumentException("Invaid Key?");
|
||||
} catch (InvalidAlgorithmParameterException e) {
|
||||
Log.w("SessionCipher", e);
|
||||
throw new IllegalArgumentException("Bad IV?");
|
||||
}
|
||||
}
|
||||
|
||||
private SecretKeySpec deriveCipherSecret(int mode, BigInteger sharedSecret, int localKeyId, int remoteKeyId) throws InvalidKeyIdException {
|
||||
byte[] sharedSecretBytes = sharedSecret.toByteArray();
|
||||
byte[] derivedBytes = deriveBytes(sharedSecretBytes, 16 * 2);
|
||||
byte[] cipherSecret = new byte[16];
|
||||
|
||||
boolean isLowEnd = isLowEnd(localKeyId, remoteKeyId);
|
||||
isLowEnd = (mode == Cipher.ENCRYPT_MODE ? isLowEnd : !isLowEnd);
|
||||
|
||||
if (isLowEnd) {
|
||||
System.arraycopy(derivedBytes, 16, cipherSecret, 0, 16);
|
||||
} else {
|
||||
System.arraycopy(derivedBytes, 0, cipherSecret, 0, 16);
|
||||
}
|
||||
|
||||
return new SecretKeySpec(cipherSecret, "AES");
|
||||
}
|
||||
|
||||
private boolean isLowEnd(int localKeyId, int remoteKeyId) throws InvalidKeyIdException {
|
||||
ECPublicKeyParameters localPublic = (ECPublicKeyParameters)localRecord.getKeyPairForId(localKeyId).getPublicKey().getKey();
|
||||
ECPublicKeyParameters remotePublic = (ECPublicKeyParameters)remoteRecord.getKeyForId(remoteKeyId).getKey();
|
||||
BigInteger local = localPublic.getQ().getX().toBigInteger();
|
||||
BigInteger remote = remotePublic.getQ().getX().toBigInteger();
|
||||
|
||||
return local.compareTo(remote) < 0;
|
||||
}
|
||||
|
||||
private byte[] deriveBytes(byte[] seed, int bytesNeeded) {
|
||||
MessageDigest md;
|
||||
|
||||
try {
|
||||
md = MessageDigest.getInstance("SHA-256");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
Log.w("SessionCipher",e);
|
||||
throw new IllegalArgumentException("SHA-256 Not Supported!");
|
||||
}
|
||||
|
||||
int rounds = bytesNeeded / md.getDigestLength();
|
||||
|
||||
for (int i=1;i<=rounds;i++) {
|
||||
byte[] roundBytes = Conversions.intToByteArray(i);
|
||||
md.update(roundBytes);
|
||||
md.update(seed);
|
||||
}
|
||||
|
||||
return md.digest();
|
||||
}
|
||||
|
||||
private SessionKey getSessionKey(int mode, int localKeyId, int remoteKeyId) throws InvalidKeyIdException {
|
||||
Log.w("SessionCipher", "Getting session key for local: " + localKeyId + " remote: " + remoteKeyId);
|
||||
SessionKey sessionKey = sessionRecord.getSessionKey(localKeyId, remoteKeyId);
|
||||
if (sessionKey != null) return sessionKey;
|
||||
|
||||
BigInteger sharedSecret = calculateSharedSecret(localKeyId, remoteKeyId);
|
||||
SecretKeySpec cipherKey = deriveCipherSecret(mode, sharedSecret, localKeyId, remoteKeyId);
|
||||
SecretKeySpec macKey = deriveMacSecret(cipherKey);
|
||||
|
||||
return new SessionKey(localKeyId, remoteKeyId, cipherKey, macKey, masterSecret);
|
||||
}
|
||||
|
||||
private BigInteger calculateSharedSecret(int localKeyId, int remoteKeyId) throws InvalidKeyIdException {
|
||||
ECDHBasicAgreement agreement = new ECDHBasicAgreement();
|
||||
AsymmetricCipherKeyPair localKeyPair = localRecord.getKeyPairForId(localKeyId).getKeyPair();
|
||||
ECPublicKeyParameters remoteKey = remoteRecord.getKeyForId(remoteKeyId).getKey();
|
||||
|
||||
agreement.init(localKeyPair.getPrivate());
|
||||
BigInteger secret = KeyUtil.calculateAgreement(agreement, remoteKey);
|
||||
|
||||
return secret;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecure.crypto;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
|
||||
public interface TransportDetails {
|
||||
public byte[] stripPaddedMessage(byte[] messageWithPadding);
|
||||
public byte[] getPaddedMessageBody(byte[] messageBody);
|
||||
|
||||
public byte[] encodeMessage(byte[] messageWithMac);
|
||||
public byte[] decodeMessage(byte[] encodedMessageBytes) throws IOException;
|
||||
}
|
||||
@@ -0,0 +1,451 @@
|
||||
// Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
// source: PreKeyEntity.proto
|
||||
|
||||
package org.whispersystems.textsecure.encoded;
|
||||
|
||||
public final class PreKeyProtos {
|
||||
private PreKeyProtos() {}
|
||||
public static void registerAllExtensions(
|
||||
com.google.protobuf.ExtensionRegistry registry) {
|
||||
}
|
||||
public interface PreKeyEntityOrBuilder
|
||||
extends com.google.protobuf.MessageOrBuilder {
|
||||
|
||||
// optional uint64 id = 1;
|
||||
boolean hasId();
|
||||
long getId();
|
||||
|
||||
// optional bytes key = 2;
|
||||
boolean hasKey();
|
||||
com.google.protobuf.ByteString getKey();
|
||||
}
|
||||
public static final class PreKeyEntity extends
|
||||
com.google.protobuf.GeneratedMessage
|
||||
implements PreKeyEntityOrBuilder {
|
||||
// Use PreKeyEntity.newBuilder() to construct.
|
||||
private PreKeyEntity(Builder builder) {
|
||||
super(builder);
|
||||
}
|
||||
private PreKeyEntity(boolean noInit) {}
|
||||
|
||||
private static final PreKeyEntity defaultInstance;
|
||||
public static PreKeyEntity getDefaultInstance() {
|
||||
return defaultInstance;
|
||||
}
|
||||
|
||||
public PreKeyEntity getDefaultInstanceForType() {
|
||||
return defaultInstance;
|
||||
}
|
||||
|
||||
public static final com.google.protobuf.Descriptors.Descriptor
|
||||
getDescriptor() {
|
||||
return org.whispersystems.textsecure.encoded.PreKeyProtos.internal_static_textsecure_PreKeyEntity_descriptor;
|
||||
}
|
||||
|
||||
protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
|
||||
internalGetFieldAccessorTable() {
|
||||
return org.whispersystems.textsecure.encoded.PreKeyProtos.internal_static_textsecure_PreKeyEntity_fieldAccessorTable;
|
||||
}
|
||||
|
||||
private int bitField0_;
|
||||
// optional uint64 id = 1;
|
||||
public static final int ID_FIELD_NUMBER = 1;
|
||||
private long id_;
|
||||
public boolean hasId() {
|
||||
return ((bitField0_ & 0x00000001) == 0x00000001);
|
||||
}
|
||||
public long getId() {
|
||||
return id_;
|
||||
}
|
||||
|
||||
// optional bytes key = 2;
|
||||
public static final int KEY_FIELD_NUMBER = 2;
|
||||
private com.google.protobuf.ByteString key_;
|
||||
public boolean hasKey() {
|
||||
return ((bitField0_ & 0x00000002) == 0x00000002);
|
||||
}
|
||||
public com.google.protobuf.ByteString getKey() {
|
||||
return key_;
|
||||
}
|
||||
|
||||
private void initFields() {
|
||||
id_ = 0L;
|
||||
key_ = com.google.protobuf.ByteString.EMPTY;
|
||||
}
|
||||
private byte memoizedIsInitialized = -1;
|
||||
public final boolean isInitialized() {
|
||||
byte isInitialized = memoizedIsInitialized;
|
||||
if (isInitialized != -1) return isInitialized == 1;
|
||||
|
||||
memoizedIsInitialized = 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void writeTo(com.google.protobuf.CodedOutputStream output)
|
||||
throws java.io.IOException {
|
||||
getSerializedSize();
|
||||
if (((bitField0_ & 0x00000001) == 0x00000001)) {
|
||||
output.writeUInt64(1, id_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000002) == 0x00000002)) {
|
||||
output.writeBytes(2, key_);
|
||||
}
|
||||
getUnknownFields().writeTo(output);
|
||||
}
|
||||
|
||||
private int memoizedSerializedSize = -1;
|
||||
public int getSerializedSize() {
|
||||
int size = memoizedSerializedSize;
|
||||
if (size != -1) return size;
|
||||
|
||||
size = 0;
|
||||
if (((bitField0_ & 0x00000001) == 0x00000001)) {
|
||||
size += com.google.protobuf.CodedOutputStream
|
||||
.computeUInt64Size(1, id_);
|
||||
}
|
||||
if (((bitField0_ & 0x00000002) == 0x00000002)) {
|
||||
size += com.google.protobuf.CodedOutputStream
|
||||
.computeBytesSize(2, key_);
|
||||
}
|
||||
size += getUnknownFields().getSerializedSize();
|
||||
memoizedSerializedSize = size;
|
||||
return size;
|
||||
}
|
||||
|
||||
private static final long serialVersionUID = 0L;
|
||||
@java.lang.Override
|
||||
protected java.lang.Object writeReplace()
|
||||
throws java.io.ObjectStreamException {
|
||||
return super.writeReplace();
|
||||
}
|
||||
|
||||
public static org.whispersystems.textsecure.encoded.PreKeyProtos.PreKeyEntity parseFrom(
|
||||
com.google.protobuf.ByteString data)
|
||||
throws com.google.protobuf.InvalidProtocolBufferException {
|
||||
return newBuilder().mergeFrom(data).buildParsed();
|
||||
}
|
||||
public static org.whispersystems.textsecure.encoded.PreKeyProtos.PreKeyEntity parseFrom(
|
||||
com.google.protobuf.ByteString data,
|
||||
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
|
||||
throws com.google.protobuf.InvalidProtocolBufferException {
|
||||
return newBuilder().mergeFrom(data, extensionRegistry)
|
||||
.buildParsed();
|
||||
}
|
||||
public static org.whispersystems.textsecure.encoded.PreKeyProtos.PreKeyEntity parseFrom(byte[] data)
|
||||
throws com.google.protobuf.InvalidProtocolBufferException {
|
||||
return newBuilder().mergeFrom(data).buildParsed();
|
||||
}
|
||||
public static org.whispersystems.textsecure.encoded.PreKeyProtos.PreKeyEntity parseFrom(
|
||||
byte[] data,
|
||||
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
|
||||
throws com.google.protobuf.InvalidProtocolBufferException {
|
||||
return newBuilder().mergeFrom(data, extensionRegistry)
|
||||
.buildParsed();
|
||||
}
|
||||
public static org.whispersystems.textsecure.encoded.PreKeyProtos.PreKeyEntity parseFrom(java.io.InputStream input)
|
||||
throws java.io.IOException {
|
||||
return newBuilder().mergeFrom(input).buildParsed();
|
||||
}
|
||||
public static org.whispersystems.textsecure.encoded.PreKeyProtos.PreKeyEntity parseFrom(
|
||||
java.io.InputStream input,
|
||||
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
|
||||
throws java.io.IOException {
|
||||
return newBuilder().mergeFrom(input, extensionRegistry)
|
||||
.buildParsed();
|
||||
}
|
||||
public static org.whispersystems.textsecure.encoded.PreKeyProtos.PreKeyEntity parseDelimitedFrom(java.io.InputStream input)
|
||||
throws java.io.IOException {
|
||||
Builder builder = newBuilder();
|
||||
if (builder.mergeDelimitedFrom(input)) {
|
||||
return builder.buildParsed();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
public static org.whispersystems.textsecure.encoded.PreKeyProtos.PreKeyEntity parseDelimitedFrom(
|
||||
java.io.InputStream input,
|
||||
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
|
||||
throws java.io.IOException {
|
||||
Builder builder = newBuilder();
|
||||
if (builder.mergeDelimitedFrom(input, extensionRegistry)) {
|
||||
return builder.buildParsed();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
public static org.whispersystems.textsecure.encoded.PreKeyProtos.PreKeyEntity parseFrom(
|
||||
com.google.protobuf.CodedInputStream input)
|
||||
throws java.io.IOException {
|
||||
return newBuilder().mergeFrom(input).buildParsed();
|
||||
}
|
||||
public static org.whispersystems.textsecure.encoded.PreKeyProtos.PreKeyEntity parseFrom(
|
||||
com.google.protobuf.CodedInputStream input,
|
||||
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
|
||||
throws java.io.IOException {
|
||||
return newBuilder().mergeFrom(input, extensionRegistry)
|
||||
.buildParsed();
|
||||
}
|
||||
|
||||
public static Builder newBuilder() { return Builder.create(); }
|
||||
public Builder newBuilderForType() { return newBuilder(); }
|
||||
public static Builder newBuilder(org.whispersystems.textsecure.encoded.PreKeyProtos.PreKeyEntity prototype) {
|
||||
return newBuilder().mergeFrom(prototype);
|
||||
}
|
||||
public Builder toBuilder() { return newBuilder(this); }
|
||||
|
||||
@java.lang.Override
|
||||
protected Builder newBuilderForType(
|
||||
com.google.protobuf.GeneratedMessage.BuilderParent parent) {
|
||||
Builder builder = new Builder(parent);
|
||||
return builder;
|
||||
}
|
||||
public static final class Builder extends
|
||||
com.google.protobuf.GeneratedMessage.Builder<Builder>
|
||||
implements org.whispersystems.textsecure.encoded.PreKeyProtos.PreKeyEntityOrBuilder {
|
||||
public static final com.google.protobuf.Descriptors.Descriptor
|
||||
getDescriptor() {
|
||||
return org.whispersystems.textsecure.encoded.PreKeyProtos.internal_static_textsecure_PreKeyEntity_descriptor;
|
||||
}
|
||||
|
||||
protected com.google.protobuf.GeneratedMessage.FieldAccessorTable
|
||||
internalGetFieldAccessorTable() {
|
||||
return org.whispersystems.textsecure.encoded.PreKeyProtos.internal_static_textsecure_PreKeyEntity_fieldAccessorTable;
|
||||
}
|
||||
|
||||
// Construct using org.whispersystems.textsecure.encoded.PreKeyProtos.PreKeyEntity.newBuilder()
|
||||
private Builder() {
|
||||
maybeForceBuilderInitialization();
|
||||
}
|
||||
|
||||
private Builder(BuilderParent parent) {
|
||||
super(parent);
|
||||
maybeForceBuilderInitialization();
|
||||
}
|
||||
private void maybeForceBuilderInitialization() {
|
||||
if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) {
|
||||
}
|
||||
}
|
||||
private static Builder create() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public Builder clear() {
|
||||
super.clear();
|
||||
id_ = 0L;
|
||||
bitField0_ = (bitField0_ & ~0x00000001);
|
||||
key_ = com.google.protobuf.ByteString.EMPTY;
|
||||
bitField0_ = (bitField0_ & ~0x00000002);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder clone() {
|
||||
return create().mergeFrom(buildPartial());
|
||||
}
|
||||
|
||||
public com.google.protobuf.Descriptors.Descriptor
|
||||
getDescriptorForType() {
|
||||
return org.whispersystems.textsecure.encoded.PreKeyProtos.PreKeyEntity.getDescriptor();
|
||||
}
|
||||
|
||||
public org.whispersystems.textsecure.encoded.PreKeyProtos.PreKeyEntity getDefaultInstanceForType() {
|
||||
return org.whispersystems.textsecure.encoded.PreKeyProtos.PreKeyEntity.getDefaultInstance();
|
||||
}
|
||||
|
||||
public org.whispersystems.textsecure.encoded.PreKeyProtos.PreKeyEntity build() {
|
||||
org.whispersystems.textsecure.encoded.PreKeyProtos.PreKeyEntity result = buildPartial();
|
||||
if (!result.isInitialized()) {
|
||||
throw newUninitializedMessageException(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private org.whispersystems.textsecure.encoded.PreKeyProtos.PreKeyEntity buildParsed()
|
||||
throws com.google.protobuf.InvalidProtocolBufferException {
|
||||
org.whispersystems.textsecure.encoded.PreKeyProtos.PreKeyEntity result = buildPartial();
|
||||
if (!result.isInitialized()) {
|
||||
throw newUninitializedMessageException(
|
||||
result).asInvalidProtocolBufferException();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public org.whispersystems.textsecure.encoded.PreKeyProtos.PreKeyEntity buildPartial() {
|
||||
org.whispersystems.textsecure.encoded.PreKeyProtos.PreKeyEntity result = new org.whispersystems.textsecure.encoded.PreKeyProtos.PreKeyEntity(this);
|
||||
int from_bitField0_ = bitField0_;
|
||||
int to_bitField0_ = 0;
|
||||
if (((from_bitField0_ & 0x00000001) == 0x00000001)) {
|
||||
to_bitField0_ |= 0x00000001;
|
||||
}
|
||||
result.id_ = id_;
|
||||
if (((from_bitField0_ & 0x00000002) == 0x00000002)) {
|
||||
to_bitField0_ |= 0x00000002;
|
||||
}
|
||||
result.key_ = key_;
|
||||
result.bitField0_ = to_bitField0_;
|
||||
onBuilt();
|
||||
return result;
|
||||
}
|
||||
|
||||
public Builder mergeFrom(com.google.protobuf.Message other) {
|
||||
if (other instanceof org.whispersystems.textsecure.encoded.PreKeyProtos.PreKeyEntity) {
|
||||
return mergeFrom((org.whispersystems.textsecure.encoded.PreKeyProtos.PreKeyEntity)other);
|
||||
} else {
|
||||
super.mergeFrom(other);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public Builder mergeFrom(org.whispersystems.textsecure.encoded.PreKeyProtos.PreKeyEntity other) {
|
||||
if (other == org.whispersystems.textsecure.encoded.PreKeyProtos.PreKeyEntity.getDefaultInstance()) return this;
|
||||
if (other.hasId()) {
|
||||
setId(other.getId());
|
||||
}
|
||||
if (other.hasKey()) {
|
||||
setKey(other.getKey());
|
||||
}
|
||||
this.mergeUnknownFields(other.getUnknownFields());
|
||||
return this;
|
||||
}
|
||||
|
||||
public final boolean isInitialized() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public Builder mergeFrom(
|
||||
com.google.protobuf.CodedInputStream input,
|
||||
com.google.protobuf.ExtensionRegistryLite extensionRegistry)
|
||||
throws java.io.IOException {
|
||||
com.google.protobuf.UnknownFieldSet.Builder unknownFields =
|
||||
com.google.protobuf.UnknownFieldSet.newBuilder(
|
||||
this.getUnknownFields());
|
||||
while (true) {
|
||||
int tag = input.readTag();
|
||||
switch (tag) {
|
||||
case 0:
|
||||
this.setUnknownFields(unknownFields.build());
|
||||
onChanged();
|
||||
return this;
|
||||
default: {
|
||||
if (!parseUnknownField(input, unknownFields,
|
||||
extensionRegistry, tag)) {
|
||||
this.setUnknownFields(unknownFields.build());
|
||||
onChanged();
|
||||
return this;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 8: {
|
||||
bitField0_ |= 0x00000001;
|
||||
id_ = input.readUInt64();
|
||||
break;
|
||||
}
|
||||
case 18: {
|
||||
bitField0_ |= 0x00000002;
|
||||
key_ = input.readBytes();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int bitField0_;
|
||||
|
||||
// optional uint64 id = 1;
|
||||
private long id_ ;
|
||||
public boolean hasId() {
|
||||
return ((bitField0_ & 0x00000001) == 0x00000001);
|
||||
}
|
||||
public long getId() {
|
||||
return id_;
|
||||
}
|
||||
public Builder setId(long value) {
|
||||
bitField0_ |= 0x00000001;
|
||||
id_ = value;
|
||||
onChanged();
|
||||
return this;
|
||||
}
|
||||
public Builder clearId() {
|
||||
bitField0_ = (bitField0_ & ~0x00000001);
|
||||
id_ = 0L;
|
||||
onChanged();
|
||||
return this;
|
||||
}
|
||||
|
||||
// optional bytes key = 2;
|
||||
private com.google.protobuf.ByteString key_ = com.google.protobuf.ByteString.EMPTY;
|
||||
public boolean hasKey() {
|
||||
return ((bitField0_ & 0x00000002) == 0x00000002);
|
||||
}
|
||||
public com.google.protobuf.ByteString getKey() {
|
||||
return key_;
|
||||
}
|
||||
public Builder setKey(com.google.protobuf.ByteString value) {
|
||||
if (value == null) {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
bitField0_ |= 0x00000002;
|
||||
key_ = value;
|
||||
onChanged();
|
||||
return this;
|
||||
}
|
||||
public Builder clearKey() {
|
||||
bitField0_ = (bitField0_ & ~0x00000002);
|
||||
key_ = getDefaultInstance().getKey();
|
||||
onChanged();
|
||||
return this;
|
||||
}
|
||||
|
||||
// @@protoc_insertion_point(builder_scope:textsecure.PreKeyEntity)
|
||||
}
|
||||
|
||||
static {
|
||||
defaultInstance = new PreKeyEntity(true);
|
||||
defaultInstance.initFields();
|
||||
}
|
||||
|
||||
// @@protoc_insertion_point(class_scope:textsecure.PreKeyEntity)
|
||||
}
|
||||
|
||||
private static com.google.protobuf.Descriptors.Descriptor
|
||||
internal_static_textsecure_PreKeyEntity_descriptor;
|
||||
private static
|
||||
com.google.protobuf.GeneratedMessage.FieldAccessorTable
|
||||
internal_static_textsecure_PreKeyEntity_fieldAccessorTable;
|
||||
|
||||
public static com.google.protobuf.Descriptors.FileDescriptor
|
||||
getDescriptor() {
|
||||
return descriptor;
|
||||
}
|
||||
private static com.google.protobuf.Descriptors.FileDescriptor
|
||||
descriptor;
|
||||
static {
|
||||
java.lang.String[] descriptorData = {
|
||||
"\n\022PreKeyEntity.proto\022\ntextsecure\"\'\n\014PreK" +
|
||||
"eyEntity\022\n\n\002id\030\001 \001(\004\022\013\n\003key\030\002 \001(\014B5\n%org" +
|
||||
".whispersystems.textsecure.encodedB\014PreK" +
|
||||
"eyProtos"
|
||||
};
|
||||
com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
|
||||
new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() {
|
||||
public com.google.protobuf.ExtensionRegistry assignDescriptors(
|
||||
com.google.protobuf.Descriptors.FileDescriptor root) {
|
||||
descriptor = root;
|
||||
internal_static_textsecure_PreKeyEntity_descriptor =
|
||||
getDescriptor().getMessageTypes().get(0);
|
||||
internal_static_textsecure_PreKeyEntity_fieldAccessorTable = new
|
||||
com.google.protobuf.GeneratedMessage.FieldAccessorTable(
|
||||
internal_static_textsecure_PreKeyEntity_descriptor,
|
||||
new java.lang.String[] { "Id", "Key", },
|
||||
org.whispersystems.textsecure.encoded.PreKeyProtos.PreKeyEntity.class,
|
||||
org.whispersystems.textsecure.encoded.PreKeyProtos.PreKeyEntity.Builder.class);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
com.google.protobuf.Descriptors.FileDescriptor
|
||||
.internalBuildGeneratedFileFrom(descriptorData,
|
||||
new com.google.protobuf.Descriptors.FileDescriptor[] {
|
||||
}, assigner);
|
||||
}
|
||||
|
||||
// @@protoc_insertion_point(outer_class_scope)
|
||||
}
|
||||
151
library/src/org/whispersystems/textsecure/protocol/Message.java
Normal file
151
library/src/org/whispersystems/textsecure/protocol/Message.java
Normal file
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecure.protocol;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.InvalidKeyException;
|
||||
import org.whispersystems.textsecure.crypto.InvalidMessageException;
|
||||
import org.whispersystems.textsecure.crypto.PublicKey;
|
||||
import org.whispersystems.textsecure.util.Conversions;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Parses and serializes the encrypted message format.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class Message {
|
||||
|
||||
public static final int SUPPORTED_VERSION = 1;
|
||||
|
||||
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;
|
||||
public static final int HEADER_LENGTH = VERSION_LENGTH + SENDER_KEY_ID_LENGTH + RECEIVER_KEY_ID_LENGTH + COUNTER_LENGTH + NEXT_KEY_LENGTH;
|
||||
|
||||
private static final int VERSION_OFFSET = 0;
|
||||
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 TEXT_OFFSET = COUNTER_OFFSET + COUNTER_LENGTH;
|
||||
|
||||
private int senderKeyId;
|
||||
private int receiverKeyId;
|
||||
private int counter;
|
||||
private int messageVersion;
|
||||
private int supportedVersion;
|
||||
private byte[] message;
|
||||
|
||||
private PublicKey nextKey;
|
||||
|
||||
public Message(int senderKeyId, int receiverKeyId, PublicKey nextKey, int counter, byte[] message, int messageVersion, int supportedVersion) {
|
||||
this.senderKeyId = senderKeyId;
|
||||
this.receiverKeyId = receiverKeyId;
|
||||
this.nextKey = nextKey;
|
||||
this.counter = counter;
|
||||
this.message = message;
|
||||
this.messageVersion = messageVersion;
|
||||
this.supportedVersion = supportedVersion;
|
||||
}
|
||||
|
||||
public Message(byte[] messageBytes) throws InvalidMessageException {
|
||||
try {
|
||||
if (messageBytes.length <= HEADER_LENGTH)
|
||||
throw new InvalidMessageException("Message is shorter than headers.");
|
||||
|
||||
this.messageVersion = Conversions.highBitsToInt(messageBytes[VERSION_OFFSET]);
|
||||
this.supportedVersion = Conversions.lowBitsToInt(messageBytes[VERSION_OFFSET]);
|
||||
|
||||
Log.w("Message", "Message Version: " + messageVersion);
|
||||
Log.w("Message", "Supported Version: " + supportedVersion);
|
||||
|
||||
if (messageVersion > SUPPORTED_VERSION)
|
||||
throw new InvalidMessageException("Message protocol version not supported: " + messageVersion);
|
||||
|
||||
this.senderKeyId = Conversions.byteArrayToMedium(messageBytes, SENDER_KEY_ID_OFFSET);
|
||||
this.receiverKeyId = Conversions.byteArrayToMedium(messageBytes, RECEIVER_KEY_ID_OFFSET);
|
||||
this.counter = Conversions.byteArrayToMedium(messageBytes, COUNTER_OFFSET);
|
||||
|
||||
Log.w("Message", "Parsed current version: " + messageVersion + " supported version: " + supportedVersion);
|
||||
|
||||
byte[] nextKeyBytes = new byte[NEXT_KEY_LENGTH];
|
||||
byte[] textBytes = new byte[messageBytes.length - HEADER_LENGTH];
|
||||
|
||||
System.arraycopy(messageBytes, NEXT_KEY_OFFSET, nextKeyBytes, 0, nextKeyBytes.length);
|
||||
System.arraycopy(messageBytes, TEXT_OFFSET, textBytes, 0, textBytes.length);
|
||||
|
||||
Log.w("Message", "Pulling next key out of message...");
|
||||
this.nextKey = new PublicKey(nextKeyBytes);
|
||||
this.message = textBytes;
|
||||
} catch (InvalidKeyException ike) {
|
||||
throw new AssertionError(ike);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] serialize() {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(HEADER_LENGTH + message.length);
|
||||
|
||||
Log.w("Message", "Constructing Message Version: (" + messageVersion + "," + supportedVersion + ")");
|
||||
|
||||
byte versionByte = Conversions.intsToByteHighAndLow(messageVersion, supportedVersion);
|
||||
byte[] senderKeyIdBytes = Conversions.mediumToByteArray(senderKeyId);
|
||||
byte[] receiverKeyIdBytes = Conversions.mediumToByteArray(receiverKeyId);
|
||||
Log.w("Message", "Serializing next key into message...");
|
||||
byte[] nextKeyBytes = nextKey.serialize();
|
||||
byte[] counterBytes = Conversions.mediumToByteArray(counter);
|
||||
|
||||
buffer.put(versionByte);
|
||||
buffer.put(senderKeyIdBytes);
|
||||
buffer.put(receiverKeyIdBytes);
|
||||
buffer.put(nextKeyBytes);
|
||||
buffer.put(counterBytes);
|
||||
buffer.put(message);
|
||||
|
||||
return buffer.array();
|
||||
}
|
||||
|
||||
public int getHighestMutuallySupportedVersion() {
|
||||
return Math.min(SUPPORTED_VERSION, this.supportedVersion);
|
||||
}
|
||||
|
||||
public int getSenderKeyId() {
|
||||
return this.senderKeyId;
|
||||
}
|
||||
|
||||
public int getReceiverKeyId() {
|
||||
return this.receiverKeyId;
|
||||
}
|
||||
|
||||
public PublicKey getNextKey() {
|
||||
return this.nextKey;
|
||||
}
|
||||
|
||||
public int getCounter() {
|
||||
return this.counter;
|
||||
}
|
||||
|
||||
public byte[] getMessageText() {
|
||||
return this.message;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.whispersystems.textsecure.storage;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
public interface CanonicalRecipientAddress {
|
||||
public long getCanonicalAddress(Context context);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecure.storage;
|
||||
|
||||
public class InvalidKeyIdException extends Exception {
|
||||
|
||||
public InvalidKeyIdException() {
|
||||
}
|
||||
|
||||
public InvalidKeyIdException(String detailMessage) {
|
||||
super(detailMessage);
|
||||
}
|
||||
|
||||
public InvalidKeyIdException(Throwable throwable) {
|
||||
super(throwable);
|
||||
}
|
||||
|
||||
public InvalidKeyIdException(String detailMessage, Throwable throwable) {
|
||||
super(detailMessage, throwable);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecure.storage;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.InvalidKeyException;
|
||||
import org.whispersystems.textsecure.crypto.KeyPair;
|
||||
import org.whispersystems.textsecure.crypto.KeyUtil;
|
||||
import org.whispersystems.textsecure.crypto.MasterCipher;
|
||||
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;
|
||||
|
||||
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, CanonicalRecipientAddress recipient) {
|
||||
super(context, SESSIONS_DIRECTORY, getFileNameForRecipient(context, recipient));
|
||||
this.masterSecret = masterSecret;
|
||||
this.masterCipher = new MasterCipher(masterSecret);
|
||||
loadData();
|
||||
}
|
||||
|
||||
public static boolean hasRecord(Context context, CanonicalRecipientAddress recipient) {
|
||||
Log.w("LocalKeyRecord", "Checking: " + getFileNameForRecipient(context, recipient));
|
||||
return Record.hasRecord(context, SESSIONS_DIRECTORY, getFileNameForRecipient(context, recipient));
|
||||
}
|
||||
|
||||
public static void delete(Context context, CanonicalRecipientAddress recipient) {
|
||||
Record.delete(context, SESSIONS_DIRECTORY, getFileNameForRecipient(context, recipient));
|
||||
}
|
||||
|
||||
private static String getFileNameForRecipient(Context context, CanonicalRecipientAddress recipient) {
|
||||
return recipient.getCanonicalAddress(context) + "-local";
|
||||
}
|
||||
|
||||
public void advanceKeyIfNecessary(int keyId) {
|
||||
Log.w("LocalKeyRecord", "Remote client acknowledges receiving key id: " + keyId);
|
||||
if (keyId == localNextKeyPair.getId()) {
|
||||
this.localCurrentKeyPair = this.localNextKeyPair;
|
||||
this.localNextKeyPair = new KeyPair(this.localNextKeyPair.getId()+1, KeyUtil.generateKeyPair(), 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package org.whispersystems.textsecure.storage;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.InvalidKeyException;
|
||||
import org.whispersystems.textsecure.crypto.MasterSecret;
|
||||
import org.whispersystems.textsecure.crypto.PreKeyPair;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.channels.FileChannel;
|
||||
|
||||
public class PreKeyRecord extends Record {
|
||||
|
||||
private static final Object FILE_LOCK = new Object();
|
||||
private static final int CURRENT_VERSION_MARKER = 1;
|
||||
|
||||
private final MasterSecret masterSecret;
|
||||
|
||||
private PreKeyPair keyPair;
|
||||
private long id;
|
||||
|
||||
public PreKeyRecord(Context context, MasterSecret masterSecret, long id)
|
||||
throws InvalidKeyIdException
|
||||
{
|
||||
super(context, PREKEY_DIRECTORY, id+"");
|
||||
|
||||
this.id = id;
|
||||
this.masterSecret = masterSecret;
|
||||
|
||||
loadData();
|
||||
}
|
||||
|
||||
public PreKeyRecord(Context context, MasterSecret masterSecret,
|
||||
long id, PreKeyPair keyPair)
|
||||
{
|
||||
super(context, PREKEY_DIRECTORY, id+"");
|
||||
this.id = id;
|
||||
this.keyPair = keyPair;
|
||||
this.masterSecret = masterSecret;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public PreKeyPair getKeyPair() {
|
||||
return keyPair;
|
||||
}
|
||||
|
||||
public static boolean hasRecord(Context context, long id) {
|
||||
Log.w("PreKeyRecord", "Checking: " + id);
|
||||
return Record.hasRecord(context, PREKEY_DIRECTORY, id+"");
|
||||
}
|
||||
|
||||
public static void delete(Context context, long id) {
|
||||
Record.delete(context, PREKEY_DIRECTORY, id+"");
|
||||
}
|
||||
|
||||
public void save() {
|
||||
synchronized (FILE_LOCK) {
|
||||
try {
|
||||
RandomAccessFile file = openRandomAccessFile();
|
||||
FileChannel out = file.getChannel();
|
||||
out.position(0);
|
||||
|
||||
writeInteger(CURRENT_VERSION_MARKER, out);
|
||||
writeKeyPair(keyPair, out);
|
||||
|
||||
out.force(true);
|
||||
out.truncate(out.position());
|
||||
out.close();
|
||||
file.close();
|
||||
} catch (IOException ioe) {
|
||||
Log.w("PreKeyRecord", ioe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void loadData() throws InvalidKeyIdException {
|
||||
synchronized (FILE_LOCK) {
|
||||
try {
|
||||
FileInputStream in = this.openInputStream();
|
||||
int recordVersion = readInteger(in);
|
||||
|
||||
if (recordVersion != CURRENT_VERSION_MARKER) {
|
||||
Log.w("PreKeyRecord", "Invalid version: " + recordVersion);
|
||||
return;
|
||||
}
|
||||
|
||||
keyPair = readKeyPair(in);
|
||||
in.close();
|
||||
} catch (FileNotFoundException e) {
|
||||
Log.w("PreKeyRecord", e);
|
||||
throw new InvalidKeyIdException(e);
|
||||
} catch (IOException ioe) {
|
||||
Log.w("PreKeyRecord", ioe);
|
||||
throw new InvalidKeyIdException(ioe);
|
||||
} catch (InvalidKeyException ike) {
|
||||
Log.w("LocalKeyRecord", ike);
|
||||
throw new InvalidKeyIdException(ike);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void writeKeyPair(PreKeyPair keyPair, FileChannel out) throws IOException {
|
||||
byte[] serialized = keyPair.serialize();
|
||||
writeBlob(serialized, out);
|
||||
}
|
||||
|
||||
private PreKeyPair readKeyPair(FileInputStream in)
|
||||
throws IOException, InvalidKeyException
|
||||
{
|
||||
byte[] keyPairBytes = readBlob(in);
|
||||
return new PreKeyPair(masterSecret, keyPairBytes);
|
||||
}
|
||||
|
||||
}
|
||||
100
library/src/org/whispersystems/textsecure/storage/Record.java
Normal file
100
library/src/org/whispersystems/textsecure/storage/Record.java
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecure.storage;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.whispersystems.textsecure.util.Conversions;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
|
||||
public abstract class Record {
|
||||
|
||||
protected static final String SESSIONS_DIRECTORY = "sessions";
|
||||
public static final String PREKEY_DIRECTORY = "prekeys";
|
||||
|
||||
protected final String address;
|
||||
protected final String directory;
|
||||
protected final Context context;
|
||||
|
||||
public Record(Context context, String directory, String address) {
|
||||
this.context = context;
|
||||
this.directory = directory;
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
public void delete() {
|
||||
delete(this.context, this.directory, this.address);
|
||||
}
|
||||
|
||||
protected static void delete(Context context, String directory, String address) {
|
||||
getAddressFile(context, directory, address).delete();
|
||||
}
|
||||
|
||||
protected static boolean hasRecord(Context context, String directory, String address) {
|
||||
return getAddressFile(context, directory, address).exists();
|
||||
}
|
||||
|
||||
protected RandomAccessFile openRandomAccessFile() throws FileNotFoundException {
|
||||
return new RandomAccessFile(getAddressFile(), "rw");
|
||||
}
|
||||
|
||||
protected FileInputStream openInputStream() throws FileNotFoundException {
|
||||
return new FileInputStream(getAddressFile().getAbsolutePath());
|
||||
}
|
||||
|
||||
private File getAddressFile() {
|
||||
return getAddressFile(context, directory, address);
|
||||
}
|
||||
|
||||
private static File getAddressFile(Context context, String directory, String address) {
|
||||
return new File(context.getFilesDir().getAbsolutePath() + File.separatorChar + directory, address);
|
||||
}
|
||||
|
||||
protected byte[] readBlob(FileInputStream in) throws IOException {
|
||||
int length = readInteger(in);
|
||||
byte[] blobBytes = new byte[length];
|
||||
|
||||
in.read(blobBytes, 0, blobBytes.length);
|
||||
return blobBytes;
|
||||
}
|
||||
|
||||
protected void writeBlob(byte[] blobBytes, FileChannel out) throws IOException {
|
||||
writeInteger(blobBytes.length, out);
|
||||
ByteBuffer buffer = ByteBuffer.wrap(blobBytes);
|
||||
out.write(buffer);
|
||||
}
|
||||
|
||||
protected int readInteger(FileInputStream in) throws IOException {
|
||||
byte[] integer = new byte[4];
|
||||
in.read(integer, 0, integer.length);
|
||||
return Conversions.byteArrayToInt(integer);
|
||||
}
|
||||
|
||||
protected void writeInteger(int value, FileChannel out) throws IOException {
|
||||
byte[] valueBytes = Conversions.intToByteArray(value);
|
||||
ByteBuffer buffer = ByteBuffer.wrap(valueBytes);
|
||||
out.write(buffer);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecure.storage;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.InvalidKeyException;
|
||||
import org.whispersystems.textsecure.crypto.PublicKey;
|
||||
import org.whispersystems.textsecure.util.Hex;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.channels.FileChannel;
|
||||
|
||||
/**
|
||||
* Represents the current and last public key belonging to the "remote"
|
||||
* endpoint in an encrypted session. These are stored on disk.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class RemoteKeyRecord extends Record {
|
||||
private static final Object FILE_LOCK = new Object();
|
||||
|
||||
private PublicKey remoteKeyCurrent;
|
||||
private PublicKey remoteKeyLast;
|
||||
|
||||
public RemoteKeyRecord(Context context, CanonicalRecipientAddress recipient) {
|
||||
super(context, SESSIONS_DIRECTORY, getFileNameForRecipient(context, recipient));
|
||||
loadData();
|
||||
}
|
||||
|
||||
public static void delete(Context context, CanonicalRecipientAddress recipient) {
|
||||
delete(context, SESSIONS_DIRECTORY, getFileNameForRecipient(context, recipient));
|
||||
}
|
||||
|
||||
public static boolean hasRecord(Context context, CanonicalRecipientAddress recipient) {
|
||||
Log.w("LocalKeyRecord", "Checking: " + getFileNameForRecipient(context, recipient));
|
||||
return hasRecord(context, SESSIONS_DIRECTORY, getFileNameForRecipient(context, recipient));
|
||||
}
|
||||
|
||||
private static String getFileNameForRecipient(Context context, CanonicalRecipientAddress recipient) {
|
||||
return recipient.getCanonicalAddress(context) + "-remote";
|
||||
}
|
||||
|
||||
public void updateCurrentRemoteKey(PublicKey remoteKey) {
|
||||
Log.w("RemoteKeyRecord", "Updating current remote key: " + remoteKey.getId());
|
||||
if (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 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecure.storage;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.MasterCipher;
|
||||
import org.whispersystems.textsecure.crypto.MasterSecret;
|
||||
import org.whispersystems.textsecure.crypto.SessionCipher;
|
||||
import org.whispersystems.textsecure.util.Conversions;
|
||||
import org.whispersystems.textsecure.util.Util;
|
||||
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
/**
|
||||
* Represents the currently negotiated session key for a given
|
||||
* local key id and remote key id. This is stored encrypted on
|
||||
* disk.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class SessionKey {
|
||||
|
||||
private int localKeyId;
|
||||
private int remoteKeyId;
|
||||
private SecretKeySpec cipherKey;
|
||||
private SecretKeySpec macKey;
|
||||
private MasterCipher masterCipher;
|
||||
|
||||
public SessionKey(int localKeyId, int remoteKeyId, SecretKeySpec cipherKey, SecretKeySpec macKey, MasterSecret masterSecret) {
|
||||
this.localKeyId = localKeyId;
|
||||
this.remoteKeyId = remoteKeyId;
|
||||
this.cipherKey = cipherKey;
|
||||
this.macKey = macKey;
|
||||
this.masterCipher = new MasterCipher(masterSecret);
|
||||
}
|
||||
|
||||
public SessionKey(byte[] bytes, MasterSecret masterSecret) {
|
||||
this.masterCipher = new MasterCipher(masterSecret);
|
||||
deserialize(bytes);
|
||||
}
|
||||
|
||||
public byte[] serialize() {
|
||||
byte[] localKeyIdBytes = Conversions.mediumToByteArray(localKeyId);
|
||||
byte[] remoteKeyIdBytes = Conversions.mediumToByteArray(remoteKeyId);
|
||||
byte[] cipherKeyBytes = cipherKey.getEncoded();
|
||||
byte[] macKeyBytes = macKey.getEncoded();
|
||||
byte[] combined = Util.combine(localKeyIdBytes, remoteKeyIdBytes, cipherKeyBytes, macKeyBytes);
|
||||
|
||||
return masterCipher.encryptBytes(combined);
|
||||
}
|
||||
|
||||
private void deserialize(byte[] bytes) {
|
||||
byte[] decrypted = masterCipher.encryptBytes(bytes);
|
||||
this.localKeyId = Conversions.byteArrayToMedium(decrypted, 0);
|
||||
this.remoteKeyId = Conversions.byteArrayToMedium(decrypted, 3);
|
||||
|
||||
byte[] keyBytes = new byte[SessionCipher.CIPHER_KEY_LENGTH];
|
||||
System.arraycopy(decrypted, 6, keyBytes, 0, keyBytes.length);
|
||||
|
||||
byte[] macBytes = new byte[SessionCipher.MAC_KEY_LENGTH];
|
||||
System.arraycopy(decrypted, 6 + keyBytes.length, macBytes, 0, macBytes.length);
|
||||
|
||||
this.cipherKey = new SecretKeySpec(keyBytes, "AES");
|
||||
this.macKey = new SecretKeySpec(macBytes, "HmacSHA1");
|
||||
}
|
||||
|
||||
public int getLocalKeyId() {
|
||||
return this.localKeyId;
|
||||
}
|
||||
|
||||
public int getRemoteKeyId() {
|
||||
return this.remoteKeyId;
|
||||
}
|
||||
|
||||
public SecretKeySpec getCipherKey() {
|
||||
return this.cipherKey;
|
||||
}
|
||||
|
||||
public SecretKeySpec getMacKey() {
|
||||
return this.macKey;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecure.storage;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.IdentityKey;
|
||||
import org.whispersystems.textsecure.crypto.InvalidKeyException;
|
||||
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.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class SessionRecord extends Record {
|
||||
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 sessionVersion;
|
||||
|
||||
private IdentityKey identityKey;
|
||||
private SessionKey sessionKeyRecord;
|
||||
private boolean verifiedSessionKey;
|
||||
|
||||
private final MasterSecret masterSecret;
|
||||
|
||||
public SessionRecord(Context context, MasterSecret masterSecret, CanonicalRecipientAddress recipient) {
|
||||
this(context, masterSecret, getRecipientId(context, recipient));
|
||||
}
|
||||
|
||||
public SessionRecord(Context context, MasterSecret masterSecret, long recipientId) {
|
||||
super(context, SESSIONS_DIRECTORY, recipientId+"");
|
||||
this.masterSecret = masterSecret;
|
||||
this.sessionVersion = 31337;
|
||||
loadData();
|
||||
}
|
||||
|
||||
public static void delete(Context context, CanonicalRecipientAddress recipient) {
|
||||
delete(context, SESSIONS_DIRECTORY, getRecipientId(context, recipient)+"");
|
||||
}
|
||||
|
||||
public static boolean hasSession(Context context, CanonicalRecipientAddress recipient) {
|
||||
Log.w("LocalKeyRecord", "Checking: " + getRecipientId(context, recipient));
|
||||
return hasRecord(context, SESSIONS_DIRECTORY, getRecipientId(context, recipient)+"");
|
||||
}
|
||||
|
||||
private static long getRecipientId(Context context, CanonicalRecipientAddress recipient) {
|
||||
return recipient.getCanonicalAddress(context);
|
||||
}
|
||||
|
||||
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 (sessionVersion == 31337 ? 0 : sessionVersion);
|
||||
}
|
||||
|
||||
public void setSessionVersion(int sessionVersion) {
|
||||
this.sessionVersion = 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 void setVerifiedSessionKey(boolean verifiedSessionKey) {
|
||||
// this.verifiedSessionKey = verifiedSessionKey;
|
||||
// }
|
||||
|
||||
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(sessionVersion, 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.sessionVersion = 31337;
|
||||
|
||||
if (in.available() != 0)
|
||||
this.sessionKeyRecord = new SessionKey(readBlob(in), masterSecret);
|
||||
|
||||
in.close();
|
||||
} else {
|
||||
this.counter = readInteger(in);
|
||||
this.localFingerprint = readBlob(in);
|
||||
this.remoteFingerprint = readBlob(in);
|
||||
this.sessionVersion = readInteger(in);
|
||||
|
||||
if (versionMarker >= 0X55555556) {
|
||||
readIdentityKey(in);
|
||||
this.verifiedSessionKey = (readInteger(in) == 1);
|
||||
}
|
||||
|
||||
if (in.available() != 0)
|
||||
this.sessionKeyRecord = new SessionKey(readBlob(in), masterSecret);
|
||||
|
||||
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 localKeyId, int remoteKeyId) {
|
||||
if (this.sessionKeyRecord == null) return null;
|
||||
|
||||
if ((this.sessionKeyRecord.getLocalKeyId() == localKeyId) &&
|
||||
(this.sessionKeyRecord.getRemoteKeyId() == remoteKeyId))
|
||||
return this.sessionKeyRecord;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
106
library/src/org/whispersystems/textsecure/util/Hex.java
Normal file
106
library/src/org/whispersystems/textsecure/util/Hex.java
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecure.util;
|
||||
|
||||
/**
|
||||
* Utility for generating hex dumps.
|
||||
*/
|
||||
public class Hex {
|
||||
|
||||
private final static int HEX_DIGITS_START = 10;
|
||||
private final static int ASCII_TEXT_START = HEX_DIGITS_START + (16*2 + (16/2));
|
||||
|
||||
final static String EOL = System.getProperty("line.separator");
|
||||
|
||||
private final static char[] HEX_DIGITS = {
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
|
||||
};
|
||||
|
||||
public static String toString(byte[] bytes) {
|
||||
return toString(bytes, 0, bytes.length);
|
||||
}
|
||||
|
||||
public static String toString(byte[] bytes, int offset, int length) {
|
||||
StringBuffer buf = new StringBuffer();
|
||||
for (int i = 0; i < length; i++) {
|
||||
appendHexChar(buf, bytes[offset + i]);
|
||||
buf.append(' ');
|
||||
}
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
public static String dump(byte[] bytes) {
|
||||
return dump(bytes, 0, bytes.length);
|
||||
}
|
||||
|
||||
public static String dump(byte[] bytes, int offset, int length) {
|
||||
StringBuffer buf = new StringBuffer();
|
||||
int lines = ((length - 1) / 16) + 1;
|
||||
int lineOffset;
|
||||
int lineLength;
|
||||
|
||||
for (int i = 0; i < lines; i++) {
|
||||
lineOffset = (i * 16) + offset;
|
||||
lineLength = Math.min(16, (length - (i * 16)));
|
||||
appendDumpLine(buf, i, bytes, lineOffset, lineLength);
|
||||
buf.append(EOL);
|
||||
}
|
||||
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
private static void appendDumpLine(StringBuffer buf, int line, byte[] bytes, int lineOffset, int lineLength) {
|
||||
buf.append(HEX_DIGITS[(line >> 28) & 0xf]);
|
||||
buf.append(HEX_DIGITS[(line >> 24) & 0xf]);
|
||||
buf.append(HEX_DIGITS[(line >> 20) & 0xf]);
|
||||
buf.append(HEX_DIGITS[(line >> 16) & 0xf]);
|
||||
buf.append(HEX_DIGITS[(line >> 12) & 0xf]);
|
||||
buf.append(HEX_DIGITS[(line >> 8) & 0xf]);
|
||||
buf.append(HEX_DIGITS[(line >> 4) & 0xf]);
|
||||
buf.append(HEX_DIGITS[(line ) & 0xf]);
|
||||
buf.append(": ");
|
||||
|
||||
for (int i = 0; i < 16; i++) {
|
||||
int idx = i + lineOffset;
|
||||
if (i < lineLength) {
|
||||
int b = bytes[idx];
|
||||
appendHexChar(buf, b);
|
||||
} else {
|
||||
buf.append(" ");
|
||||
}
|
||||
if ((i % 2) == 1) {
|
||||
buf.append(' ');
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < 16 && i < lineLength; i++) {
|
||||
int idx = i + lineOffset;
|
||||
int b = bytes[idx];
|
||||
if (b >= 0x20 && b <= 0x7e) {
|
||||
buf.append((char)b);
|
||||
} else {
|
||||
buf.append('.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void appendHexChar(StringBuffer buf, int b) {
|
||||
buf.append(HEX_DIGITS[(b >> 4) & 0xf]);
|
||||
buf.append(HEX_DIGITS[b & 0xf]);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,6 +16,35 @@ import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
public class Util {
|
||||
|
||||
public static byte[] combine(byte[] one, byte[] two) {
|
||||
byte[] combined = new byte[one.length + two.length];
|
||||
System.arraycopy(one, 0, combined, 0, one.length);
|
||||
System.arraycopy(two, 0, combined, one.length, two.length);
|
||||
|
||||
return combined;
|
||||
}
|
||||
|
||||
public static byte[] combine(byte[] one, byte[] two, byte[] three) {
|
||||
byte[] combined = new byte[one.length + two.length + three.length];
|
||||
System.arraycopy(one, 0, combined, 0, one.length);
|
||||
System.arraycopy(two, 0, combined, one.length, two.length);
|
||||
System.arraycopy(three, 0, combined, one.length + two.length, three.length);
|
||||
|
||||
return combined;
|
||||
}
|
||||
|
||||
public static byte[] combine(byte[] one, byte[] two, byte[] three, byte[] four) {
|
||||
byte[] combined = new byte[one.length + two.length + three.length + four.length];
|
||||
System.arraycopy(one, 0, combined, 0, one.length);
|
||||
System.arraycopy(two, 0, combined, one.length, two.length);
|
||||
System.arraycopy(three, 0, combined, one.length + two.length, three.length);
|
||||
System.arraycopy(four, 0, combined, one.length + two.length + three.length, four.length);
|
||||
|
||||
return combined;
|
||||
|
||||
}
|
||||
|
||||
public static boolean isEmpty(String value) {
|
||||
return value == null || value.trim().length() == 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user