mirror of
https://github.com/oxen-io/session-android.git
synced 2025-10-20 11:31:58 +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:
@@ -23,12 +23,17 @@ import java.security.NoSuchAlgorithmException;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
|
||||
import org.bouncycastle.crypto.agreement.ECDHBasicAgreement;
|
||||
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
|
||||
import org.spongycastle.crypto.AsymmetricCipherKeyPair;
|
||||
import org.spongycastle.crypto.agreement.ECDHBasicAgreement;
|
||||
import org.spongycastle.crypto.params.ECPublicKeyParameters;
|
||||
import org.whispersystems.textsecure.crypto.InvalidKeyException;
|
||||
import org.whispersystems.textsecure.crypto.InvalidMessageException;
|
||||
import org.whispersystems.textsecure.crypto.KeyUtil;
|
||||
import org.whispersystems.textsecure.crypto.MasterCipher;
|
||||
import org.whispersystems.textsecure.crypto.MasterSecret;
|
||||
import org.whispersystems.textsecure.crypto.PublicKey;
|
||||
import org.whispersystems.textsecure.util.Base64;
|
||||
import org.whispersystems.textsecure.util.Conversions;
|
||||
import org.thoughtcrime.securesms.util.InvalidMessageException;
|
||||
|
||||
/**
|
||||
* This class is used to asymmetricly encrypt local data. This is used in the case
|
||||
@@ -58,7 +63,7 @@ public class AsymmetricMasterCipher {
|
||||
this.asymmetricMasterSecret = asymmetricMasterSecret;
|
||||
}
|
||||
|
||||
public String decryptBody(String body) throws IOException, org.thoughtcrime.securesms.crypto.InvalidMessageException {
|
||||
public String decryptBody(String body) throws IOException, InvalidMessageException {
|
||||
try {
|
||||
byte[] combined = Base64.decode(body);
|
||||
PublicKey theirPublicKey = new PublicKey(combined, 0);
|
||||
@@ -74,9 +79,9 @@ public class AsymmetricMasterCipher {
|
||||
|
||||
return new String(decryptedBodyBytes);
|
||||
} catch (InvalidKeyException ike) {
|
||||
throw new org.thoughtcrime.securesms.crypto.InvalidMessageException(ike);
|
||||
throw new InvalidMessageException(ike);
|
||||
} catch (InvalidMessageException e) {
|
||||
throw new org.thoughtcrime.securesms.crypto.InvalidMessageException(e);
|
||||
throw new InvalidMessageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -16,7 +16,8 @@
|
||||
*/
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
|
||||
import org.spongycastle.crypto.params.ECPrivateKeyParameters;
|
||||
import org.whispersystems.textsecure.crypto.PublicKey;
|
||||
|
||||
/**
|
||||
* When a user first initializes TextSecure, a few secrets
|
||||
|
@@ -37,6 +37,8 @@ import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.MasterSecret;
|
||||
|
||||
/**
|
||||
* Class for streaming an encrypted MMS "part" off the disk.
|
||||
*
|
||||
|
@@ -18,10 +18,8 @@ package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.EncryptingSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
@@ -35,9 +33,14 @@ import org.thoughtcrime.securesms.recipients.RecipientFactory;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
|
||||
import org.thoughtcrime.securesms.recipients.Recipients;
|
||||
import org.thoughtcrime.securesms.sms.SmsTransportDetails;
|
||||
import org.thoughtcrime.securesms.util.Hex;
|
||||
import org.whispersystems.textsecure.crypto.InvalidKeyException;
|
||||
import org.whispersystems.textsecure.crypto.InvalidMessageException;
|
||||
import org.whispersystems.textsecure.crypto.SessionCipher;
|
||||
import org.whispersystems.textsecure.util.Hex;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.WorkerThread;
|
||||
import org.whispersystems.textsecure.crypto.KeyUtil;
|
||||
import org.whispersystems.textsecure.crypto.MasterSecret;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
@@ -48,7 +51,6 @@ import ws.com.google.android.mms.MmsException;
|
||||
import ws.com.google.android.mms.pdu.MultimediaMessagePdu;
|
||||
import ws.com.google.android.mms.pdu.PduParser;
|
||||
import ws.com.google.android.mms.pdu.RetrieveConf;
|
||||
import ws.com.google.android.mms.pdu.SendReq;
|
||||
|
||||
/**
|
||||
* A work queue for processing a number of encryption operations.
|
||||
@@ -170,10 +172,10 @@ public class DecryptingQueue {
|
||||
MmsDatabase database = DatabaseFactory.getMmsDatabase(context);
|
||||
|
||||
try {
|
||||
String messageFrom = pdu.getFrom().getString();
|
||||
Recipients recipients = RecipientFactory.getRecipientsFromString(context, messageFrom, false);
|
||||
Recipient recipient = recipients.getPrimaryRecipient();
|
||||
byte[] ciphertextPduBytes = getEncryptedData();
|
||||
String messageFrom = pdu.getFrom().getString();
|
||||
Recipients recipients = RecipientFactory.getRecipientsFromString(context, messageFrom, false);
|
||||
Recipient recipient = recipients.getPrimaryRecipient();
|
||||
byte[] ciphertextPduBytes = getEncryptedData();
|
||||
|
||||
if (ciphertextPduBytes == null) {
|
||||
Log.w("DecryptingQueue", "No encoded PNG data found on parts.");
|
||||
@@ -261,7 +263,7 @@ public class DecryptingQueue {
|
||||
try {
|
||||
Log.w("DecryptingQueue", "Parsing recipient for originator: " + originator);
|
||||
Recipients recipients = RecipientFactory.getRecipientsFromString(context, originator, false);
|
||||
Recipient recipient = recipients.getPrimaryRecipient();
|
||||
Recipient recipient = recipients.getPrimaryRecipient();
|
||||
Log.w("DecryptingQueue", "Parsed Recipient: " + recipient.getNumber());
|
||||
|
||||
if (!KeyUtil.isSessionFor(context, recipient)) {
|
||||
|
@@ -32,6 +32,8 @@ import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.MasterSecret;
|
||||
|
||||
/**
|
||||
* A class for streaming an encrypted MMS "part" to disk.
|
||||
*
|
||||
|
@@ -1,117 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
|
||||
import org.thoughtcrime.securesms.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);
|
||||
}
|
||||
|
||||
}
|
@@ -16,29 +16,36 @@
|
||||
*/
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
|
||||
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
|
||||
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
|
||||
import org.bouncycastle.crypto.signers.ECDSASigner;
|
||||
import org.thoughtcrime.bouncycastle.asn1.ASN1Encodable;
|
||||
import org.thoughtcrime.bouncycastle.asn1.ASN1Object;
|
||||
import org.thoughtcrime.bouncycastle.asn1.ASN1Sequence;
|
||||
import org.thoughtcrime.bouncycastle.asn1.DERInteger;
|
||||
import org.thoughtcrime.bouncycastle.asn1.DERSequence;
|
||||
import org.whispersystems.textsecure.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.Combiner;
|
||||
import org.whispersystems.textsecure.util.Conversions;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.Editor;
|
||||
import android.util.Log;
|
||||
|
||||
import org.spongycastle.asn1.ASN1Encoding;
|
||||
import org.spongycastle.asn1.ASN1Object;
|
||||
import org.spongycastle.asn1.ASN1Primitive;
|
||||
import org.spongycastle.asn1.ASN1Sequence;
|
||||
import org.spongycastle.asn1.DERInteger;
|
||||
import org.spongycastle.asn1.DERSequence;
|
||||
import org.spongycastle.crypto.AsymmetricCipherKeyPair;
|
||||
import org.spongycastle.crypto.params.ECPrivateKeyParameters;
|
||||
import org.spongycastle.crypto.params.ECPublicKeyParameters;
|
||||
import org.spongycastle.crypto.signers.ECDSASigner;
|
||||
import org.whispersystems.textsecure.crypto.IdentityKey;
|
||||
import org.whispersystems.textsecure.crypto.InvalidKeyException;
|
||||
import org.whispersystems.textsecure.crypto.KeyUtil;
|
||||
import org.whispersystems.textsecure.crypto.MasterCipher;
|
||||
import org.whispersystems.textsecure.crypto.MasterSecret;
|
||||
import org.whispersystems.textsecure.crypto.PublicKey;
|
||||
import org.whispersystems.textsecure.util.Base64;
|
||||
import org.whispersystems.textsecure.util.Conversions;
|
||||
import org.whispersystems.textsecure.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
/**
|
||||
* Utility class for working with identity keys.
|
||||
*
|
||||
@@ -108,7 +115,7 @@ public class IdentityKeyUtil {
|
||||
|
||||
verifier.init(false, identityKey.getPublicKeyParameters());
|
||||
|
||||
ASN1Sequence sequence = (ASN1Sequence)ASN1Object.fromByteArray(signatureBytes);
|
||||
ASN1Sequence sequence = (ASN1Sequence) ASN1Primitive.fromByteArray(signatureBytes);
|
||||
BigInteger[] signatureIntegers = new BigInteger[]{
|
||||
((DERInteger)sequence.getObjectAt(0)).getValue(),
|
||||
((DERInteger)sequence.getObjectAt(1)).getValue()
|
||||
@@ -139,13 +146,13 @@ public class IdentityKeyUtil {
|
||||
|
||||
BigInteger[] messageSignatureInts = signer.generateSignature(messageHash);
|
||||
DERInteger[] derMessageSignatureInts = new DERInteger[]{ new DERInteger(messageSignatureInts[0]), new DERInteger(messageSignatureInts[1]) };
|
||||
byte[] messageSignatureBytes = new DERSequence(derMessageSignatureInts).getEncoded(ASN1Encodable.DER);
|
||||
byte[] messageSignatureBytes = new DERSequence(derMessageSignatureInts).getEncoded(ASN1Encoding.DER);
|
||||
byte[] messageSignature = new byte[2 + messageSignatureBytes.length];
|
||||
|
||||
Conversions.shortToByteArray(messageSignature, 0, messageSignatureBytes.length);
|
||||
System.arraycopy(messageSignatureBytes, 0, messageSignature, 2, messageSignatureBytes.length);
|
||||
|
||||
byte[] combined = Combiner.combine(keyExchangeBytes, publicKeyBytes, messageSignature);
|
||||
byte[] combined = Util.combine(keyExchangeBytes, publicKeyBytes, messageSignature);
|
||||
|
||||
return combined;
|
||||
} catch (IOException ioe) {
|
||||
|
@@ -1,40 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
}
|
@@ -1,40 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
}
|
@@ -1,40 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
}
|
@@ -22,10 +22,12 @@ import android.content.DialogInterface;
|
||||
import android.util.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.keys.LocalKeyRecord;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.sms.OutgoingKeyExchangeMessage;
|
||||
import org.whispersystems.textsecure.crypto.KeyUtil;
|
||||
import org.whispersystems.textsecure.crypto.MasterSecret;
|
||||
import org.whispersystems.textsecure.storage.LocalKeyRecord;
|
||||
|
||||
public class KeyExchangeInitiator {
|
||||
|
||||
|
@@ -19,8 +19,12 @@ package org.thoughtcrime.securesms.crypto;
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.database.keys.LocalKeyRecord;
|
||||
import org.thoughtcrime.securesms.protocol.Message;
|
||||
import org.whispersystems.textsecure.crypto.IdentityKey;
|
||||
import org.whispersystems.textsecure.crypto.InvalidKeyException;
|
||||
import org.whispersystems.textsecure.crypto.MasterSecret;
|
||||
import org.whispersystems.textsecure.crypto.PublicKey;
|
||||
import org.whispersystems.textsecure.storage.LocalKeyRecord;
|
||||
import org.whispersystems.textsecure.protocol.Message;
|
||||
import org.whispersystems.textsecure.util.Base64;
|
||||
import org.whispersystems.textsecure.util.Conversions;
|
||||
|
||||
@@ -54,9 +58,9 @@ public class KeyExchangeMessage {
|
||||
|
||||
private final int messageVersion;
|
||||
private final int supportedVersion;
|
||||
private final PublicKey publicKey;
|
||||
private final PublicKey publicKey;
|
||||
private final String serialized;
|
||||
private IdentityKey identityKey;
|
||||
private IdentityKey identityKey;
|
||||
|
||||
public KeyExchangeMessage(Context context, MasterSecret masterSecret, int messageVersion, LocalKeyRecord record, int highIdBits) {
|
||||
this.publicKey = new PublicKey(record.getCurrentKeyPair().getPublicKey());
|
||||
|
@@ -20,12 +20,13 @@ import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.util.Log;
|
||||
|
||||
import org.bouncycastle.util.Arrays;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.keys.LocalKeyRecord;
|
||||
import org.thoughtcrime.securesms.database.keys.RemoteKeyRecord;
|
||||
import org.thoughtcrime.securesms.database.keys.SessionRecord;
|
||||
import org.thoughtcrime.securesms.protocol.Message;
|
||||
import org.whispersystems.textsecure.crypto.KeyUtil;
|
||||
import org.whispersystems.textsecure.crypto.MasterSecret;
|
||||
import org.whispersystems.textsecure.storage.LocalKeyRecord;
|
||||
import org.whispersystems.textsecure.storage.RemoteKeyRecord;
|
||||
import org.whispersystems.textsecure.storage.SessionRecord;
|
||||
import org.whispersystems.textsecure.protocol.Message;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
|
@@ -1,84 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
|
||||
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
|
||||
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
|
||||
import org.thoughtcrime.securesms.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;
|
||||
}
|
||||
|
||||
}
|
@@ -1,165 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
|
||||
import org.bouncycastle.crypto.agreement.ECDHBasicAgreement;
|
||||
import org.bouncycastle.crypto.generators.ECKeyPairGenerator;
|
||||
import org.bouncycastle.crypto.params.ECDomainParameters;
|
||||
import org.bouncycastle.crypto.params.ECKeyGenerationParameters;
|
||||
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
|
||||
import org.bouncycastle.math.ec.ECCurve;
|
||||
import org.bouncycastle.math.ec.ECFieldElement;
|
||||
import org.bouncycastle.math.ec.ECPoint;
|
||||
import org.thoughtcrime.securesms.database.keys.LocalKeyRecord;
|
||||
import org.thoughtcrime.securesms.database.keys.PreKeyRecord;
|
||||
import org.thoughtcrime.securesms.database.keys.RemoteKeyRecord;
|
||||
import org.thoughtcrime.securesms.database.keys.SessionRecord;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
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, Recipient 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, Recipient 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,
|
||||
Recipient recipient)
|
||||
{
|
||||
return isSessionFor(context, recipient) &&
|
||||
new SessionRecord(context, masterSecret, recipient).getIdentityKey() != null;
|
||||
}
|
||||
|
||||
public static LocalKeyRecord initializeRecordFor(Recipient 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());
|
||||
}
|
||||
|
||||
}
|
@@ -1,221 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
import 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.bouncycastle.crypto.params.ECPrivateKeyParameters;
|
||||
import org.whispersystems.textsecure.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.Hex;
|
||||
import org.thoughtcrime.securesms.util.InvalidMessageException;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
@@ -1,120 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import org.bouncycastle.util.Arrays;
|
||||
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
@@ -21,9 +21,16 @@ import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.Editor;
|
||||
import android.util.Log;
|
||||
|
||||
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
|
||||
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
|
||||
import org.spongycastle.crypto.AsymmetricCipherKeyPair;
|
||||
import org.spongycastle.crypto.params.ECPrivateKeyParameters;
|
||||
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 org.whispersystems.textsecure.crypto.PublicKey;
|
||||
import org.whispersystems.textsecure.util.Base64;
|
||||
import org.whispersystems.textsecure.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
@@ -203,8 +210,8 @@ public class MasterSecretUtil {
|
||||
SharedPreferences settings = context.getSharedPreferences(PREFERENCES_NAME, 0);
|
||||
String encodedValue = settings.getString(key, "");
|
||||
|
||||
if (encodedValue == "") return null;
|
||||
else return Base64.decode(encodedValue);
|
||||
if (Util.isEmpty(encodedValue)) return null;
|
||||
else return Base64.decode(encodedValue);
|
||||
}
|
||||
|
||||
private static byte[] generateEncryptionSecret() {
|
||||
@@ -252,7 +259,7 @@ public class MasterSecretUtil {
|
||||
return cipher;
|
||||
}
|
||||
|
||||
private static byte[] encryptWithPassphrase(Context context, byte[] data, String passphrase) throws NoSuchAlgorithmException, GeneralSecurityException {
|
||||
private static byte[] encryptWithPassphrase(Context context, byte[] data, String passphrase) throws GeneralSecurityException {
|
||||
byte[] encryptionSalt = generateSalt();
|
||||
Cipher cipher = getCipherFromPassphrase(passphrase, encryptionSalt, Cipher.ENCRYPT_MODE);
|
||||
byte[] cipherText = cipher.doFinal(data);
|
||||
|
@@ -1,85 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.thoughtcrime.securesms.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.");
|
||||
}
|
||||
|
||||
}
|
@@ -1,43 +0,0 @@
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
|
||||
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
|
||||
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
|
||||
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
|
||||
import org.thoughtcrime.securesms.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);
|
||||
}
|
||||
}
|
@@ -1,113 +0,0 @@
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
import org.thoughtcrime.securesms.database.keys.InvalidKeyIdException;
|
||||
import org.thoughtcrime.securesms.database.keys.PreKeyRecord;
|
||||
import org.thoughtcrime.securesms.encoded.PreKeyProtos.PreKeyEntity;
|
||||
import org.whispersystems.textsecure.push.PreKeyList;
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
@@ -1,96 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
|
||||
import org.thoughtcrime.securesms.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;
|
||||
}
|
||||
}
|
@@ -16,9 +16,12 @@
|
||||
*/
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.IdentityKey;
|
||||
import org.whispersystems.textsecure.crypto.PublicKey;
|
||||
|
||||
public class PublicKeyAndVersion {
|
||||
|
||||
public IdentityKey identityKey;
|
||||
public IdentityKey identityKey;
|
||||
public final PublicKey key;
|
||||
public final int version;
|
||||
public final int maxVersion;
|
||||
|
@@ -1,21 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
public interface SerializableKey {
|
||||
public byte[] serialize();
|
||||
}
|
@@ -1,266 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
|
||||
import org.bouncycastle.crypto.agreement.ECDHBasicAgreement;
|
||||
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
|
||||
import org.thoughtcrime.securesms.database.keys.InvalidKeyIdException;
|
||||
import org.thoughtcrime.securesms.database.keys.LocalKeyRecord;
|
||||
import org.thoughtcrime.securesms.database.keys.RemoteKeyRecord;
|
||||
import org.thoughtcrime.securesms.database.keys.SessionKey;
|
||||
import org.thoughtcrime.securesms.database.keys.SessionRecord;
|
||||
import org.thoughtcrime.securesms.protocol.Message;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.whispersystems.textsecure.util.Conversions;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* This is where the session encryption magic happens. Implements a compressed version of the OTR protocol.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class SessionCipher {
|
||||
|
||||
public static final Object CIPHER_LOCK = new Object();
|
||||
|
||||
public static final int CIPHER_KEY_LENGTH = 16;
|
||||
public static final int MAC_KEY_LENGTH = 20;
|
||||
|
||||
public static final int ENCRYPTED_MESSAGE_OVERHEAD = Message.HEADER_LENGTH + MessageMac.MAC_LENGTH;
|
||||
// public static final int ENCRYPTED_SINGLE_MESSAGE_BODY_MAX_SIZE = SmsTransportDetails.SINGLE_MESSAGE_MAX_BYTES - ENCRYPTED_MESSAGE_OVERHEAD;
|
||||
|
||||
private final LocalKeyRecord localRecord;
|
||||
private final RemoteKeyRecord remoteRecord;
|
||||
private final SessionRecord sessionRecord;
|
||||
private final MasterSecret masterSecret;
|
||||
private final TransportDetails transportDetails;
|
||||
|
||||
public SessionCipher(Context context, MasterSecret masterSecret, Recipient recipient, TransportDetails transportDetails) {
|
||||
Log.w("SessionCipher", "Constructing session cipher...");
|
||||
this.masterSecret = masterSecret;
|
||||
this.localRecord = new LocalKeyRecord(context, masterSecret, recipient);
|
||||
this.remoteRecord = new RemoteKeyRecord(context, recipient);
|
||||
this.sessionRecord = new SessionRecord(context, masterSecret, recipient);
|
||||
this.transportDetails = transportDetails;
|
||||
}
|
||||
|
||||
public byte[] encryptMessage(byte[] messageText) {
|
||||
Log.w("SessionCipher", "Encrypting message...");
|
||||
try {
|
||||
int localId = localRecord.getCurrentKeyPair().getId();
|
||||
int remoteId = remoteRecord.getCurrentRemoteKey().getId();
|
||||
SessionKey sessionKey = getSessionKey(Cipher.ENCRYPT_MODE, localId, remoteId);
|
||||
byte[]paddedMessage = transportDetails.getPaddedMessageBody(messageText);
|
||||
byte[]cipherText = getCiphertext(paddedMessage, sessionKey.getCipherKey());
|
||||
byte[]message = buildMessageFromCiphertext(cipherText);
|
||||
byte[]messageWithMac = MessageMac.buildMessageWithMac(message, sessionKey.getMacKey());
|
||||
|
||||
sessionRecord.setSessionKey(sessionKey);
|
||||
sessionRecord.incrementCounter();
|
||||
sessionRecord.save();
|
||||
|
||||
return transportDetails.encodeMessage(messageWithMac);
|
||||
} catch (IllegalBlockSizeException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
} catch (BadPaddingException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
} catch (InvalidKeyIdException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public byte[] decryptMessage(byte[] messageText) throws InvalidMessageException {
|
||||
Log.w("SessionCipher", "Decrypting message...");
|
||||
try {
|
||||
byte[] decodedMessage = transportDetails.decodeMessage(messageText);
|
||||
Message message = new Message(MessageMac.getMessageWithoutMac(decodedMessage));
|
||||
SessionKey sessionKey = getSessionKey(Cipher.DECRYPT_MODE, message.getReceiverKeyId(), message.getSenderKeyId());
|
||||
|
||||
MessageMac.verifyMac(decodedMessage, sessionKey.getMacKey());
|
||||
|
||||
byte[] plaintextWithPadding = getPlaintext(message.getMessageText(), sessionKey.getCipherKey(), message.getCounter());
|
||||
byte[] plaintext = transportDetails.stripPaddedMessage(plaintextWithPadding);
|
||||
|
||||
remoteRecord.updateCurrentRemoteKey(message.getNextKey());
|
||||
remoteRecord.save();
|
||||
|
||||
localRecord.advanceKeyIfNecessary(message.getReceiverKeyId());
|
||||
localRecord.save();
|
||||
|
||||
sessionRecord.setSessionKey(sessionKey);
|
||||
sessionRecord.setSessionVersion(message.getHighestMutuallySupportedVersion());
|
||||
sessionRecord.save();
|
||||
|
||||
return plaintext;
|
||||
} catch (IOException e) {
|
||||
throw new InvalidMessageException("Encoding Failure", e);
|
||||
} catch (InvalidKeyIdException e) {
|
||||
throw new InvalidMessageException("Bad Key ID", e);
|
||||
} catch (InvalidMacException e) {
|
||||
throw new InvalidMessageException("Bad MAC", e);
|
||||
} catch (IllegalBlockSizeException e) {
|
||||
throw new InvalidMessageException("assert", e);
|
||||
} catch (BadPaddingException e) {
|
||||
throw new InvalidMessageException("assert", e);
|
||||
}
|
||||
}
|
||||
|
||||
private SecretKeySpec deriveMacSecret(SecretKeySpec key) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-1");
|
||||
byte[] secret = md.digest(key.getEncoded());
|
||||
|
||||
return new SecretKeySpec(secret, "HmacSHA1");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalArgumentException("SHA-1 Not Supported!",e);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] buildMessageFromCiphertext(byte[] cipherText) {
|
||||
Message message = new Message(localRecord.getCurrentKeyPair().getId(),
|
||||
remoteRecord.getCurrentRemoteKey().getId(),
|
||||
localRecord.getNextKeyPair().getPublicKey(),
|
||||
sessionRecord.getCounter(),
|
||||
cipherText, sessionRecord.getSessionVersion(), Message.SUPPORTED_VERSION);
|
||||
|
||||
return message.serialize();
|
||||
}
|
||||
|
||||
|
||||
private byte[] getPlaintext(byte[] cipherText, SecretKeySpec key, int counter) throws IllegalBlockSizeException, BadPaddingException {
|
||||
Cipher cipher = getCipher(Cipher.DECRYPT_MODE, key, counter);
|
||||
return cipher.doFinal(cipherText);
|
||||
}
|
||||
|
||||
private byte[] getCiphertext(byte[] message, SecretKeySpec key) throws IllegalBlockSizeException, BadPaddingException {
|
||||
Cipher cipher = getCipher(Cipher.ENCRYPT_MODE, key, sessionRecord.getCounter());
|
||||
return cipher.doFinal(message);
|
||||
}
|
||||
|
||||
private Cipher getCipher(int mode, SecretKeySpec key, int counter) {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
|
||||
|
||||
byte[] ivBytes = new byte[16];
|
||||
Conversions.mediumToByteArray(ivBytes, 0, counter);
|
||||
|
||||
IvParameterSpec iv = new IvParameterSpec(ivBytes);
|
||||
cipher.init(mode, key, iv);
|
||||
|
||||
return cipher;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalArgumentException("AES Not Supported!");
|
||||
} catch (NoSuchPaddingException e) {
|
||||
throw new IllegalArgumentException("NoPadding Not Supported!");
|
||||
} catch (InvalidKeyException e) {
|
||||
Log.w("SessionCipher", e);
|
||||
throw new IllegalArgumentException("Invaid Key?");
|
||||
} catch (InvalidAlgorithmParameterException e) {
|
||||
Log.w("SessionCipher", e);
|
||||
throw new IllegalArgumentException("Bad IV?");
|
||||
}
|
||||
}
|
||||
|
||||
private SecretKeySpec deriveCipherSecret(int mode, BigInteger sharedSecret, int localKeyId, int remoteKeyId) throws InvalidKeyIdException {
|
||||
byte[] sharedSecretBytes = sharedSecret.toByteArray();
|
||||
byte[] derivedBytes = deriveBytes(sharedSecretBytes, 16 * 2);
|
||||
byte[] cipherSecret = new byte[16];
|
||||
|
||||
boolean isLowEnd = isLowEnd(localKeyId, remoteKeyId);
|
||||
isLowEnd = (mode == Cipher.ENCRYPT_MODE ? isLowEnd : !isLowEnd);
|
||||
|
||||
if (isLowEnd) {
|
||||
System.arraycopy(derivedBytes, 16, cipherSecret, 0, 16);
|
||||
} else {
|
||||
System.arraycopy(derivedBytes, 0, cipherSecret, 0, 16);
|
||||
}
|
||||
|
||||
return new SecretKeySpec(cipherSecret, "AES");
|
||||
}
|
||||
|
||||
private boolean isLowEnd(int localKeyId, int remoteKeyId) throws InvalidKeyIdException {
|
||||
ECPublicKeyParameters localPublic = (ECPublicKeyParameters)localRecord.getKeyPairForId(localKeyId).getPublicKey().getKey();
|
||||
ECPublicKeyParameters remotePublic = (ECPublicKeyParameters)remoteRecord.getKeyForId(remoteKeyId).getKey();
|
||||
BigInteger local = localPublic.getQ().getX().toBigInteger();
|
||||
BigInteger remote = remotePublic.getQ().getX().toBigInteger();
|
||||
|
||||
return local.compareTo(remote) < 0;
|
||||
}
|
||||
|
||||
private byte[] deriveBytes(byte[] seed, int bytesNeeded) {
|
||||
MessageDigest md;
|
||||
|
||||
try {
|
||||
md = MessageDigest.getInstance("SHA-256");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
Log.w("SessionCipher",e);
|
||||
throw new IllegalArgumentException("SHA-256 Not Supported!");
|
||||
}
|
||||
|
||||
int rounds = bytesNeeded / md.getDigestLength();
|
||||
|
||||
for (int i=1;i<=rounds;i++) {
|
||||
byte[] roundBytes = Conversions.intToByteArray(i);
|
||||
md.update(roundBytes);
|
||||
md.update(seed);
|
||||
}
|
||||
|
||||
return md.digest();
|
||||
}
|
||||
|
||||
private SessionKey getSessionKey(int mode, int localKeyId, int remoteKeyId) throws InvalidKeyIdException {
|
||||
Log.w("SessionCipher", "Getting session key for local: " + localKeyId + " remote: " + remoteKeyId);
|
||||
SessionKey sessionKey = sessionRecord.getSessionKey(localKeyId, remoteKeyId);
|
||||
if (sessionKey != null) return sessionKey;
|
||||
|
||||
BigInteger sharedSecret = calculateSharedSecret(localKeyId, remoteKeyId);
|
||||
SecretKeySpec cipherKey = deriveCipherSecret(mode, sharedSecret, localKeyId, remoteKeyId);
|
||||
SecretKeySpec macKey = deriveMacSecret(cipherKey);
|
||||
|
||||
return new SessionKey(localKeyId, remoteKeyId, cipherKey, macKey, masterSecret);
|
||||
}
|
||||
|
||||
private BigInteger calculateSharedSecret(int localKeyId, int remoteKeyId) throws InvalidKeyIdException {
|
||||
ECDHBasicAgreement agreement = new ECDHBasicAgreement();
|
||||
AsymmetricCipherKeyPair localKeyPair = localRecord.getKeyPairForId(localKeyId).getKeyPair();
|
||||
ECPublicKeyParameters remoteKey = remoteRecord.getKeyForId(remoteKeyId).getKey();
|
||||
|
||||
agreement.init(localKeyPair.getPrivate());
|
||||
BigInteger secret = KeyUtil.calculateAgreement(agreement, remoteKey);
|
||||
|
||||
return secret;
|
||||
}
|
||||
}
|
@@ -1,28 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
import 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;
|
||||
}
|
Reference in New Issue
Block a user