mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-26 03:40:56 +01:00
Move all files to natural position.
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
* Copyright (C) 2013 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.libsignal.InvalidMessageException;
|
||||
import org.whispersystems.libsignal.ecc.Curve;
|
||||
import org.whispersystems.libsignal.ecc.ECKeyPair;
|
||||
import org.whispersystems.libsignal.ecc.ECPrivateKey;
|
||||
import org.whispersystems.libsignal.ecc.ECPublicKey;
|
||||
import org.thoughtcrime.securesms.util.Conversions;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
/**
|
||||
* This class is used to asymmetrically encrypt local data. This is used in the case
|
||||
* where TextSecure receives an SMS, but the user's local encryption passphrase is
|
||||
* not cached (either because of a timeout, or because it hasn't yet been entered).
|
||||
*
|
||||
* In this case, we have access to the public key of a local keypair. We encrypt
|
||||
* the message with this, and put it into the DB. When the user enters their passphrase,
|
||||
* we can get access to the private key of the local keypair, decrypt the message, and
|
||||
* replace it into the DB with symmetric encryption.
|
||||
*
|
||||
* The encryption protocol is as follows:
|
||||
*
|
||||
* 1) Generate an ephemeral keypair.
|
||||
* 2) Do ECDH with the public key of the local durable keypair.
|
||||
* 3) Do KMF with the ECDH result to obtain a master secret.
|
||||
* 4) Encrypt the message with that master secret.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*
|
||||
*/
|
||||
public class AsymmetricMasterCipher {
|
||||
|
||||
private final AsymmetricMasterSecret asymmetricMasterSecret;
|
||||
|
||||
public AsymmetricMasterCipher(AsymmetricMasterSecret asymmetricMasterSecret) {
|
||||
this.asymmetricMasterSecret = asymmetricMasterSecret;
|
||||
}
|
||||
|
||||
public byte[] encryptBytes(byte[] body) {
|
||||
try {
|
||||
ECPublicKey theirPublic = asymmetricMasterSecret.getDjbPublicKey();
|
||||
ECKeyPair ourKeyPair = Curve.generateKeyPair();
|
||||
byte[] secret = Curve.calculateAgreement(theirPublic, ourKeyPair.getPrivateKey());
|
||||
MasterCipher masterCipher = getMasterCipherForSecret(secret);
|
||||
byte[] encryptedBodyBytes = masterCipher.encryptBytes(body);
|
||||
|
||||
PublicKey ourPublicKey = new PublicKey(31337, ourKeyPair.getPublicKey());
|
||||
byte[] publicKeyBytes = ourPublicKey.serialize();
|
||||
|
||||
return Util.combine(publicKeyBytes, encryptedBodyBytes);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] decryptBytes(byte[] combined) throws IOException, InvalidMessageException {
|
||||
try {
|
||||
byte[][] parts = Util.split(combined, PublicKey.KEY_SIZE, combined.length - PublicKey.KEY_SIZE);
|
||||
PublicKey theirPublicKey = new PublicKey(parts[0], 0);
|
||||
|
||||
ECPrivateKey ourPrivateKey = asymmetricMasterSecret.getPrivateKey();
|
||||
byte[] secret = Curve.calculateAgreement(theirPublicKey.getKey(), ourPrivateKey);
|
||||
MasterCipher masterCipher = getMasterCipherForSecret(secret);
|
||||
|
||||
return masterCipher.decryptBytes(parts[1]);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public String decryptBody(String body) throws IOException, InvalidMessageException {
|
||||
byte[] combined = Base64.decode(body);
|
||||
return new String(decryptBytes(combined));
|
||||
}
|
||||
|
||||
public String encryptBody(String body) {
|
||||
return Base64.encodeBytes(encryptBytes(body.getBytes()));
|
||||
}
|
||||
|
||||
private MasterCipher getMasterCipherForSecret(byte[] secretBytes) {
|
||||
SecretKeySpec cipherKey = deriveCipherKey(secretBytes);
|
||||
SecretKeySpec macKey = deriveMacKey(secretBytes);
|
||||
MasterSecret masterSecret = new MasterSecret(cipherKey, macKey);
|
||||
|
||||
return new MasterCipher(masterSecret);
|
||||
}
|
||||
|
||||
private SecretKeySpec deriveMacKey(byte[] secretBytes) {
|
||||
byte[] digestedBytes = getDigestedBytes(secretBytes, 1);
|
||||
byte[] macKeyBytes = new byte[20];
|
||||
|
||||
System.arraycopy(digestedBytes, 0, macKeyBytes, 0, macKeyBytes.length);
|
||||
return new SecretKeySpec(macKeyBytes, "HmacSHA1");
|
||||
}
|
||||
|
||||
private SecretKeySpec deriveCipherKey(byte[] secretBytes) {
|
||||
byte[] digestedBytes = getDigestedBytes(secretBytes, 0);
|
||||
byte[] cipherKeyBytes = new byte[16];
|
||||
|
||||
System.arraycopy(digestedBytes, 0, cipherKeyBytes, 0, cipherKeyBytes.length);
|
||||
return new SecretKeySpec(cipherKeyBytes, "AES");
|
||||
}
|
||||
|
||||
private byte[] getDigestedBytes(byte[] secretBytes, int iteration) {
|
||||
try {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(secretBytes, "HmacSHA256"));
|
||||
return mac.doFinal(Conversions.intToByteArray(iteration));
|
||||
} catch (NoSuchAlgorithmException | java.security.InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
* Copyright (C) 2013 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
import org.whispersystems.libsignal.ecc.ECPrivateKey;
|
||||
import org.whispersystems.libsignal.ecc.ECPublicKey;
|
||||
|
||||
/**
|
||||
* 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 the ECC keypair.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*
|
||||
*/
|
||||
|
||||
public class AsymmetricMasterSecret {
|
||||
|
||||
private final ECPublicKey djbPublicKey;
|
||||
private final ECPrivateKey djbPrivateKey;
|
||||
|
||||
|
||||
public AsymmetricMasterSecret(ECPublicKey djbPublicKey, ECPrivateKey djbPrivateKey)
|
||||
{
|
||||
this.djbPublicKey = djbPublicKey;
|
||||
this.djbPrivateKey = djbPrivateKey;
|
||||
}
|
||||
|
||||
public ECPublicKey getDjbPublicKey() {
|
||||
return djbPublicKey;
|
||||
}
|
||||
|
||||
|
||||
public ECPrivateKey getPrivateKey() {
|
||||
return djbPrivateKey;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import android.util.Base64;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
|
||||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Encapsulates the key material used to encrypt attachments on disk.
|
||||
*
|
||||
* There are two logical pieces of material, a deprecated set of keys used to encrypt
|
||||
* legacy attachments, and a key that is used to encrypt attachments going forward.
|
||||
*/
|
||||
public class AttachmentSecret {
|
||||
|
||||
@JsonProperty
|
||||
@JsonSerialize(using = ByteArraySerializer.class)
|
||||
@JsonDeserialize(using = ByteArrayDeserializer.class)
|
||||
private byte[] classicCipherKey;
|
||||
|
||||
@JsonProperty
|
||||
@JsonSerialize(using = ByteArraySerializer.class)
|
||||
@JsonDeserialize(using = ByteArrayDeserializer.class)
|
||||
private byte[] classicMacKey;
|
||||
|
||||
@JsonProperty
|
||||
@JsonSerialize(using = ByteArraySerializer.class)
|
||||
@JsonDeserialize(using = ByteArrayDeserializer.class)
|
||||
private byte[] modernKey;
|
||||
|
||||
public AttachmentSecret(byte[] classicCipherKey, byte[] classicMacKey, byte[] modernKey)
|
||||
{
|
||||
this.classicCipherKey = classicCipherKey;
|
||||
this.classicMacKey = classicMacKey;
|
||||
this.modernKey = modernKey;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public AttachmentSecret() {
|
||||
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
byte[] getClassicCipherKey() {
|
||||
return classicCipherKey;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
byte[] getClassicMacKey() {
|
||||
return classicMacKey;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public byte[] getModernKey() {
|
||||
return modernKey;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
void setClassicCipherKey(byte[] classicCipherKey) {
|
||||
this.classicCipherKey = classicCipherKey;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
void setClassicMacKey(byte[] classicMacKey) {
|
||||
this.classicMacKey = classicMacKey;
|
||||
}
|
||||
|
||||
public String serialize() {
|
||||
try {
|
||||
return JsonUtils.toJson(this);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
static AttachmentSecret fromString(@NonNull String value) {
|
||||
try {
|
||||
return JsonUtils.fromJson(value, AttachmentSecret.class);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ByteArraySerializer extends JsonSerializer<byte[]> {
|
||||
@Override
|
||||
public void serialize(byte[] value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
|
||||
gen.writeString(Base64.encodeToString(value, Base64.NO_WRAP | Base64.NO_PADDING));
|
||||
}
|
||||
}
|
||||
|
||||
private static class ByteArrayDeserializer extends JsonDeserializer<byte[]> {
|
||||
|
||||
@Override
|
||||
public byte[] deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||
return Base64.decode(p.getValueAsString(), Base64.NO_WRAP | Base64.NO_PADDING);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
|
||||
/**
|
||||
* A provider that is responsible for creating or retrieving the AttachmentSecret model.
|
||||
*
|
||||
* On modern Android, the serialized secrets are themselves encrypted using a key that lives
|
||||
* in the system KeyStore, for whatever that is worth.
|
||||
*/
|
||||
public class AttachmentSecretProvider {
|
||||
|
||||
private static AttachmentSecretProvider provider;
|
||||
|
||||
public static synchronized AttachmentSecretProvider getInstance(@NonNull Context context) {
|
||||
if (provider == null) provider = new AttachmentSecretProvider(context.getApplicationContext());
|
||||
return provider;
|
||||
}
|
||||
|
||||
private final Context context;
|
||||
|
||||
private AttachmentSecret attachmentSecret;
|
||||
|
||||
private AttachmentSecretProvider(@NonNull Context context) {
|
||||
this.context = context.getApplicationContext();
|
||||
}
|
||||
|
||||
public synchronized AttachmentSecret getOrCreateAttachmentSecret() {
|
||||
if (attachmentSecret != null) return attachmentSecret;
|
||||
|
||||
String unencryptedSecret = TextSecurePreferences.getAttachmentUnencryptedSecret(context);
|
||||
String encryptedSecret = TextSecurePreferences.getAttachmentEncryptedSecret(context);
|
||||
|
||||
if (unencryptedSecret != null) attachmentSecret = getUnencryptedAttachmentSecret(context, unencryptedSecret);
|
||||
else if (encryptedSecret != null) attachmentSecret = getEncryptedAttachmentSecret(encryptedSecret);
|
||||
else attachmentSecret = createAndStoreAttachmentSecret(context);
|
||||
|
||||
return attachmentSecret;
|
||||
}
|
||||
|
||||
public synchronized AttachmentSecret setClassicKey(@NonNull Context context, @NonNull byte[] classicCipherKey, @NonNull byte[] classicMacKey) {
|
||||
AttachmentSecret currentSecret = getOrCreateAttachmentSecret();
|
||||
currentSecret.setClassicCipherKey(classicCipherKey);
|
||||
currentSecret.setClassicMacKey(classicMacKey);
|
||||
|
||||
storeAttachmentSecret(context, attachmentSecret);
|
||||
|
||||
return attachmentSecret;
|
||||
}
|
||||
|
||||
private AttachmentSecret getUnencryptedAttachmentSecret(@NonNull Context context, @NonNull String unencryptedSecret)
|
||||
{
|
||||
AttachmentSecret attachmentSecret = AttachmentSecret.fromString(unencryptedSecret);
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
return attachmentSecret;
|
||||
} else {
|
||||
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(attachmentSecret.serialize().getBytes());
|
||||
|
||||
TextSecurePreferences.setAttachmentEncryptedSecret(context, encryptedSecret.serialize());
|
||||
TextSecurePreferences.setAttachmentUnencryptedSecret(context, null);
|
||||
|
||||
return attachmentSecret;
|
||||
}
|
||||
}
|
||||
|
||||
private AttachmentSecret getEncryptedAttachmentSecret(@NonNull String serializedEncryptedSecret) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
throw new AssertionError("OS downgrade not supported. KeyStore sealed data exists on platform < M!");
|
||||
} else {
|
||||
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.SealedData.fromString(serializedEncryptedSecret);
|
||||
return AttachmentSecret.fromString(new String(KeyStoreHelper.unseal(encryptedSecret)));
|
||||
}
|
||||
}
|
||||
|
||||
private AttachmentSecret createAndStoreAttachmentSecret(@NonNull Context context) {
|
||||
SecureRandom random = new SecureRandom();
|
||||
byte[] secret = new byte[32];
|
||||
random.nextBytes(secret);
|
||||
|
||||
AttachmentSecret attachmentSecret = new AttachmentSecret(null, null, secret);
|
||||
storeAttachmentSecret(context, attachmentSecret);
|
||||
|
||||
return attachmentSecret;
|
||||
}
|
||||
|
||||
private void storeAttachmentSecret(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(attachmentSecret.serialize().getBytes());
|
||||
TextSecurePreferences.setAttachmentEncryptedSecret(context, encryptedSecret.serialize());
|
||||
} else {
|
||||
TextSecurePreferences.setAttachmentUnencryptedSecret(context, attachmentSecret.serialize());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* 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 androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.LimitedInputStream;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.CipherInputStream;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class ClassicDecryptingPartInputStream {
|
||||
|
||||
private static final String TAG = ClassicDecryptingPartInputStream.class.getSimpleName();
|
||||
|
||||
private static final int IV_LENGTH = 16;
|
||||
private static final int MAC_LENGTH = 20;
|
||||
|
||||
public static InputStream createFor(@NonNull AttachmentSecret attachmentSecret, @NonNull File file)
|
||||
throws IOException
|
||||
{
|
||||
try {
|
||||
if (file.length() <= IV_LENGTH + MAC_LENGTH) {
|
||||
throw new IOException("File too short");
|
||||
}
|
||||
|
||||
verifyMac(attachmentSecret, file);
|
||||
|
||||
FileInputStream fileStream = new FileInputStream(file);
|
||||
byte[] ivBytes = new byte[IV_LENGTH];
|
||||
readFully(fileStream, ivBytes);
|
||||
|
||||
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
IvParameterSpec iv = new IvParameterSpec(ivBytes);
|
||||
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(attachmentSecret.getClassicCipherKey(), "AES"), iv);
|
||||
|
||||
return new CipherInputStreamWrapper(new LimitedInputStream(fileStream, file.length() - MAC_LENGTH - IV_LENGTH), cipher);
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void verifyMac(AttachmentSecret attachmentSecret, File file) throws IOException {
|
||||
Mac mac = initializeMac(new SecretKeySpec(attachmentSecret.getClassicMacKey(), "HmacSHA1"));
|
||||
FileInputStream macStream = new FileInputStream(file);
|
||||
InputStream dataStream = new LimitedInputStream(new FileInputStream(file), file.length() - MAC_LENGTH);
|
||||
byte[] theirMac = new byte[MAC_LENGTH];
|
||||
|
||||
if (macStream.skip(file.length() - MAC_LENGTH) != file.length() - MAC_LENGTH) {
|
||||
throw new IOException("Unable to seek");
|
||||
}
|
||||
|
||||
readFully(macStream, theirMac);
|
||||
|
||||
byte[] buffer = new byte[4096];
|
||||
int read;
|
||||
|
||||
while ((read = dataStream.read(buffer)) != -1) {
|
||||
mac.update(buffer, 0, read);
|
||||
}
|
||||
|
||||
byte[] ourMac = mac.doFinal();
|
||||
|
||||
if (!MessageDigest.isEqual(ourMac, theirMac)) {
|
||||
throw new IOException("Bad MAC");
|
||||
}
|
||||
|
||||
macStream.close();
|
||||
dataStream.close();
|
||||
}
|
||||
|
||||
private static Mac initializeMac(SecretKeySpec key) {
|
||||
try {
|
||||
Mac hmac = Mac.getInstance("HmacSHA1");
|
||||
hmac.init(key);
|
||||
|
||||
return hmac;
|
||||
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void readFully(InputStream in, byte[] buffer) throws IOException {
|
||||
int offset = 0;
|
||||
|
||||
for (;;) {
|
||||
int read = in.read(buffer, offset, buffer.length-offset);
|
||||
|
||||
if (read + offset < buffer.length) offset += read;
|
||||
else return;
|
||||
}
|
||||
}
|
||||
|
||||
// Note (4/3/17) -- Older versions of Android have a busted OpenSSL provider that
|
||||
// throws a RuntimeException on a BadPaddingException, so we have to catch
|
||||
// that here in case someone calls close() before reaching the end of the
|
||||
// stream (since close() implicitly calls doFinal())
|
||||
//
|
||||
// See Signal-Android Issue #6477
|
||||
// Android: https://android-review.googlesource.com/#/c/65321/
|
||||
private static class CipherInputStreamWrapper extends CipherInputStream {
|
||||
|
||||
CipherInputStreamWrapper(InputStream is, Cipher c) {
|
||||
super(is, c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
try {
|
||||
super.close();
|
||||
} catch (Throwable t) {
|
||||
Log.w(TAG, t);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long skipAmount)
|
||||
throws IOException
|
||||
{
|
||||
long remaining = skipAmount;
|
||||
|
||||
if (skipAmount <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
byte[] skipBuffer = new byte[4092];
|
||||
|
||||
while (remaining > 0) {
|
||||
int read = super.read(skipBuffer, 0, Util.toIntExact(Math.min(skipBuffer.length, remaining)));
|
||||
|
||||
if (read < 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
remaining -= read;
|
||||
}
|
||||
|
||||
return skipAmount - remaining;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.util.Hex;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class DatabaseSecret {
|
||||
|
||||
private final byte[] key;
|
||||
private final String encoded;
|
||||
|
||||
public DatabaseSecret(@NonNull byte[] key) {
|
||||
this.key = key;
|
||||
this.encoded = Hex.toStringCondensed(key);
|
||||
}
|
||||
|
||||
public DatabaseSecret(@NonNull String encoded) throws IOException {
|
||||
this.key = Hex.fromStringCondensed(encoded);
|
||||
this.encoded = encoded;
|
||||
}
|
||||
|
||||
public String asString() {
|
||||
return encoded;
|
||||
}
|
||||
|
||||
public byte[] asBytes() {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
public class DatabaseSecretProvider {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = DatabaseSecretProvider.class.getSimpleName();
|
||||
|
||||
private final Context context;
|
||||
|
||||
public DatabaseSecretProvider(@NonNull Context context) {
|
||||
this.context = context.getApplicationContext();
|
||||
}
|
||||
|
||||
public DatabaseSecret getOrCreateDatabaseSecret() {
|
||||
String unencryptedSecret = TextSecurePreferences.getDatabaseUnencryptedSecret(context);
|
||||
String encryptedSecret = TextSecurePreferences.getDatabaseEncryptedSecret(context);
|
||||
|
||||
if (unencryptedSecret != null) return getUnencryptedDatabaseSecret(context, unencryptedSecret);
|
||||
else if (encryptedSecret != null) return getEncryptedDatabaseSecret(encryptedSecret);
|
||||
else return createAndStoreDatabaseSecret(context);
|
||||
}
|
||||
|
||||
private DatabaseSecret getUnencryptedDatabaseSecret(@NonNull Context context, @NonNull String unencryptedSecret)
|
||||
{
|
||||
try {
|
||||
DatabaseSecret databaseSecret = new DatabaseSecret(unencryptedSecret);
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
return databaseSecret;
|
||||
} else {
|
||||
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(databaseSecret.asBytes());
|
||||
|
||||
TextSecurePreferences.setDatabaseEncryptedSecret(context, encryptedSecret.serialize());
|
||||
TextSecurePreferences.setDatabaseUnencryptedSecret(context, null);
|
||||
|
||||
return databaseSecret;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private DatabaseSecret getEncryptedDatabaseSecret(@NonNull String serializedEncryptedSecret) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
throw new AssertionError("OS downgrade not supported. KeyStore sealed data exists on platform < M!");
|
||||
} else {
|
||||
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.SealedData.fromString(serializedEncryptedSecret);
|
||||
return new DatabaseSecret(KeyStoreHelper.unseal(encryptedSecret));
|
||||
}
|
||||
}
|
||||
|
||||
private DatabaseSecret createAndStoreDatabaseSecret(@NonNull Context context) {
|
||||
SecureRandom random = new SecureRandom();
|
||||
byte[] secret = new byte[32];
|
||||
random.nextBytes(secret);
|
||||
|
||||
DatabaseSecret databaseSecret = new DatabaseSecret(secret);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(databaseSecret.asBytes());
|
||||
TextSecurePreferences.setDatabaseEncryptedSecret(context, encryptedSecret.serialize());
|
||||
} else {
|
||||
TextSecurePreferences.setDatabaseUnencryptedSecret(context, databaseSecret.asString());
|
||||
}
|
||||
|
||||
return databaseSecret;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Copyright (C) 2014 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
|
||||
public class IdentityKeyParcelable implements Parcelable {
|
||||
|
||||
public static final Parcelable.Creator<IdentityKeyParcelable> CREATOR = new Parcelable.Creator<IdentityKeyParcelable>() {
|
||||
public IdentityKeyParcelable createFromParcel(Parcel in) {
|
||||
try {
|
||||
return new IdentityKeyParcelable(in);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public IdentityKeyParcelable[] newArray(int size) {
|
||||
return new IdentityKeyParcelable[size];
|
||||
}
|
||||
};
|
||||
|
||||
private final IdentityKey identityKey;
|
||||
|
||||
public IdentityKeyParcelable(IdentityKey identityKey) {
|
||||
this.identityKey = identityKey;
|
||||
}
|
||||
|
||||
public IdentityKeyParcelable(Parcel in) throws InvalidKeyException {
|
||||
int serializedLength = in.readInt();
|
||||
byte[] serialized = new byte[serializedLength];
|
||||
|
||||
in.readByteArray(serialized);
|
||||
this.identityKey = new IdentityKey(serialized, 0);
|
||||
}
|
||||
|
||||
public IdentityKey get() {
|
||||
return identityKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeInt(identityKey.serialize().length);
|
||||
dest.writeByteArray(identityKey.serialize());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
* Copyright (C) 2013 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.Editor;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.IdentityKeyPair;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.libsignal.ecc.Curve;
|
||||
import org.whispersystems.libsignal.ecc.ECKeyPair;
|
||||
import org.whispersystems.libsignal.ecc.ECPrivateKey;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Utility class for working with identity keys.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class IdentityKeyUtil {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = IdentityKeyUtil.class.getSimpleName();
|
||||
|
||||
private static final String IDENTITY_PUBLIC_KEY_CIPHERTEXT_LEGACY_PREF = "pref_identity_public_curve25519";
|
||||
private static final String IDENTITY_PRIVATE_KEY_CIPHERTEXT_LEGACY_PREF = "pref_identity_private_curve25519";
|
||||
|
||||
private static final String IDENTITY_PUBLIC_KEY_PREF = "pref_identity_public_v3";
|
||||
private static final String IDENTITY_PRIVATE_KEY_PREF = "pref_identity_private_v3";
|
||||
|
||||
public static boolean hasIdentityKey(Context context) {
|
||||
SharedPreferences preferences = context.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0);
|
||||
|
||||
return
|
||||
preferences.contains(IDENTITY_PUBLIC_KEY_PREF) &&
|
||||
preferences.contains(IDENTITY_PRIVATE_KEY_PREF);
|
||||
}
|
||||
|
||||
public static @NonNull IdentityKey getIdentityKey(@NonNull Context context) {
|
||||
if (!hasIdentityKey(context)) throw new AssertionError("There isn't one!");
|
||||
|
||||
try {
|
||||
byte[] publicKeyBytes = Base64.decode(retrieve(context, IDENTITY_PUBLIC_KEY_PREF));
|
||||
return new IdentityKey(publicKeyBytes, 0);
|
||||
} catch (IOException | InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static @NonNull IdentityKeyPair getIdentityKeyPair(@NonNull Context context) {
|
||||
if (!hasIdentityKey(context)) throw new AssertionError("There isn't one!");
|
||||
|
||||
try {
|
||||
IdentityKey publicKey = getIdentityKey(context);
|
||||
ECPrivateKey privateKey = Curve.decodePrivatePoint(Base64.decode(retrieve(context, IDENTITY_PRIVATE_KEY_PREF)));
|
||||
|
||||
return new IdentityKeyPair(publicKey, privateKey);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void generateIdentityKeys(Context context) {
|
||||
ECKeyPair djbKeyPair = Curve.generateKeyPair();
|
||||
IdentityKey djbIdentityKey = new IdentityKey(djbKeyPair.getPublicKey());
|
||||
ECPrivateKey djbPrivateKey = djbKeyPair.getPrivateKey();
|
||||
|
||||
save(context, IDENTITY_PUBLIC_KEY_PREF, Base64.encodeBytes(djbIdentityKey.serialize()));
|
||||
save(context, IDENTITY_PRIVATE_KEY_PREF, Base64.encodeBytes(djbPrivateKey.serialize()));
|
||||
}
|
||||
|
||||
public static void migrateIdentityKeys(@NonNull Context context,
|
||||
@NonNull MasterSecret masterSecret)
|
||||
{
|
||||
if (!hasIdentityKey(context)) {
|
||||
if (hasLegacyIdentityKeys(context)) {
|
||||
IdentityKeyPair legacyPair = getLegacyIdentityKeyPair(context, masterSecret);
|
||||
|
||||
save(context, IDENTITY_PUBLIC_KEY_PREF, Base64.encodeBytes(legacyPair.getPublicKey().serialize()));
|
||||
save(context, IDENTITY_PRIVATE_KEY_PREF, Base64.encodeBytes(legacyPair.getPrivateKey().serialize()));
|
||||
|
||||
delete(context, IDENTITY_PUBLIC_KEY_CIPHERTEXT_LEGACY_PREF);
|
||||
delete(context, IDENTITY_PRIVATE_KEY_CIPHERTEXT_LEGACY_PREF);
|
||||
} else {
|
||||
generateIdentityKeys(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static List<BackupProtos.SharedPreference> getBackupRecord(@NonNull Context context) {
|
||||
SharedPreferences preferences = context.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0);
|
||||
|
||||
return new LinkedList<BackupProtos.SharedPreference>() {{
|
||||
add(BackupProtos.SharedPreference.newBuilder()
|
||||
.setFile(MasterSecretUtil.PREFERENCES_NAME)
|
||||
.setKey(IDENTITY_PUBLIC_KEY_PREF)
|
||||
.setValue(preferences.getString(IDENTITY_PUBLIC_KEY_PREF, null))
|
||||
.build());
|
||||
add(BackupProtos.SharedPreference.newBuilder()
|
||||
.setFile(MasterSecretUtil.PREFERENCES_NAME)
|
||||
.setKey(IDENTITY_PRIVATE_KEY_PREF)
|
||||
.setValue(preferences.getString(IDENTITY_PRIVATE_KEY_PREF, null))
|
||||
.build());
|
||||
}};
|
||||
}
|
||||
|
||||
private static boolean hasLegacyIdentityKeys(Context context) {
|
||||
return
|
||||
retrieve(context, IDENTITY_PUBLIC_KEY_CIPHERTEXT_LEGACY_PREF) != null &&
|
||||
retrieve(context, IDENTITY_PRIVATE_KEY_CIPHERTEXT_LEGACY_PREF) != null;
|
||||
}
|
||||
|
||||
private static IdentityKeyPair getLegacyIdentityKeyPair(@NonNull Context context,
|
||||
@NonNull MasterSecret masterSecret)
|
||||
{
|
||||
try {
|
||||
MasterCipher masterCipher = new MasterCipher(masterSecret);
|
||||
byte[] publicKeyBytes = Base64.decode(retrieve(context, IDENTITY_PUBLIC_KEY_CIPHERTEXT_LEGACY_PREF));
|
||||
IdentityKey identityKey = new IdentityKey(publicKeyBytes, 0);
|
||||
ECPrivateKey privateKey = masterCipher.decryptKey(Base64.decode(retrieve(context, IDENTITY_PRIVATE_KEY_CIPHERTEXT_LEGACY_PREF)));
|
||||
|
||||
return new IdentityKeyPair(identityKey, privateKey);
|
||||
} catch (IOException | InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String retrieve(Context context, String key) {
|
||||
SharedPreferences preferences = context.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0);
|
||||
return preferences.getString(key, null);
|
||||
}
|
||||
|
||||
private static void save(Context context, String key, String value) {
|
||||
SharedPreferences preferences = context.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0);
|
||||
Editor preferencesEditor = preferences.edit();
|
||||
|
||||
preferencesEditor.putString(key, value);
|
||||
if (!preferencesEditor.commit()) throw new AssertionError("failed to save identity key/value to shared preferences");
|
||||
}
|
||||
|
||||
private static void delete(Context context, String key) {
|
||||
context.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0).edit().remove(key).commit();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* 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 InvalidPassphraseException extends Exception {
|
||||
|
||||
public InvalidPassphraseException() {
|
||||
super();
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public InvalidPassphraseException(String detailMessage) {
|
||||
super(detailMessage);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public InvalidPassphraseException(Throwable throwable) {
|
||||
super(throwable);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
public InvalidPassphraseException(String detailMessage, Throwable throwable) {
|
||||
super(detailMessage, throwable);
|
||||
// TODO Auto-generated constructor stub
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
|
||||
import android.os.Build;
|
||||
import android.security.keystore.KeyGenParameterSpec;
|
||||
import android.security.keystore.KeyProperties;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import android.util.Base64;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
|
||||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.NoSuchProviderException;
|
||||
import java.security.UnrecoverableEntryException;
|
||||
import java.security.cert.CertificateException;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
|
||||
public class KeyStoreHelper {
|
||||
|
||||
private static final String ANDROID_KEY_STORE = "AndroidKeyStore";
|
||||
private static final String KEY_ALIAS = "SignalSecret";
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
public static SealedData seal(@NonNull byte[] input) {
|
||||
SecretKey secretKey = getOrCreateKeyStoreEntry();
|
||||
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
|
||||
|
||||
byte[] iv = cipher.getIV();
|
||||
byte[] data = cipher.doFinal(input);
|
||||
|
||||
return new SealedData(iv, data);
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
public static byte[] unseal(@NonNull SealedData sealedData) {
|
||||
SecretKey secretKey = getKeyStoreEntry();
|
||||
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(128, sealedData.iv));
|
||||
|
||||
return cipher.doFinal(sealedData.data);
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private static SecretKey getOrCreateKeyStoreEntry() {
|
||||
if (hasKeyStoreEntry()) return getKeyStoreEntry();
|
||||
else return createKeyStoreEntry();
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private static SecretKey createKeyStoreEntry() {
|
||||
try {
|
||||
KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE);
|
||||
KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
|
||||
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
||||
.build();
|
||||
|
||||
keyGenerator.init(keyGenParameterSpec);
|
||||
|
||||
return keyGenerator.generateKey();
|
||||
} catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private static SecretKey getKeyStoreEntry() {
|
||||
try {
|
||||
KeyStore keyStore = KeyStore.getInstance(ANDROID_KEY_STORE);
|
||||
keyStore.load(null);
|
||||
|
||||
return ((KeyStore.SecretKeyEntry) keyStore.getEntry(KEY_ALIAS, null)).getSecretKey();
|
||||
} catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException | UnrecoverableEntryException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
private static boolean hasKeyStoreEntry() {
|
||||
try {
|
||||
KeyStore ks = KeyStore.getInstance(ANDROID_KEY_STORE);
|
||||
ks.load(null);
|
||||
|
||||
return ks.containsAlias(KEY_ALIAS) && ks.entryInstanceOf(KEY_ALIAS, KeyStore.SecretKeyEntry.class);
|
||||
} catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class SealedData {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = SealedData.class.getSimpleName();
|
||||
|
||||
@JsonProperty
|
||||
@JsonSerialize(using = ByteArraySerializer.class)
|
||||
@JsonDeserialize(using = ByteArrayDeserializer.class)
|
||||
private byte[] iv;
|
||||
|
||||
@JsonProperty
|
||||
@JsonSerialize(using = ByteArraySerializer.class)
|
||||
@JsonDeserialize(using = ByteArrayDeserializer.class)
|
||||
private byte[] data;
|
||||
|
||||
SealedData(@NonNull byte[] iv, @NonNull byte[] data) {
|
||||
this.iv = iv;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public SealedData() {}
|
||||
|
||||
public String serialize() {
|
||||
try {
|
||||
return JsonUtils.toJson(this);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static SealedData fromString(@NonNull String value) {
|
||||
try {
|
||||
return JsonUtils.fromJson(value, SealedData.class);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ByteArraySerializer extends JsonSerializer<byte[]> {
|
||||
@Override
|
||||
public void serialize(byte[] value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
|
||||
gen.writeString(Base64.encodeToString(value, Base64.NO_WRAP | Base64.NO_PADDING));
|
||||
}
|
||||
}
|
||||
|
||||
private static class ByteArrayDeserializer extends JsonDeserializer<byte[]> {
|
||||
|
||||
@Override
|
||||
public byte[] deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||
return Base64.decode(p.getValueAsString(), Base64.NO_WRAP | Base64.NO_PADDING);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
* Copyright (C) 2013 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.Hex;
|
||||
import org.whispersystems.libsignal.InvalidMessageException;
|
||||
import org.whispersystems.libsignal.ecc.Curve;
|
||||
import org.whispersystems.libsignal.ecc.ECPrivateKey;
|
||||
|
||||
import java.io.IOException;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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 static final String TAG = MasterCipher.class.getSimpleName();
|
||||
|
||||
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 | NoSuchAlgorithmException nspe) {
|
||||
throw new AssertionError(nspe);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] encryptKey(ECPrivateKey privateKey) {
|
||||
return encryptBytes(privateKey.serialize());
|
||||
}
|
||||
|
||||
public String encryptBody(@NonNull String body) {
|
||||
return encryptAndEncodeBytes(body.getBytes());
|
||||
}
|
||||
|
||||
public String decryptBody(String body) throws InvalidMessageException {
|
||||
return new String(decodeAndDecryptBytes(body));
|
||||
}
|
||||
|
||||
public ECPrivateKey decryptKey(byte[] key)
|
||||
throws org.whispersystems.libsignal.InvalidKeyException
|
||||
{
|
||||
try {
|
||||
return Curve.decodePrivatePoint(decryptBytes(key));
|
||||
} catch (InvalidMessageException ime) {
|
||||
throw new org.whispersystems.libsignal.InvalidKeyException(ime);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] decryptBytes(@NonNull 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.i(TAG, "Our Mac: " + Hex.toString(ourMac));
|
||||
Log.i(TAG, "Thr Mac: " + Hex.toString(theirMac));
|
||||
return Arrays.equals(ourMac, theirMac);
|
||||
}
|
||||
|
||||
public byte[] getMacFor(String content) {
|
||||
Log.w(TAG, "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(@NonNull byte[] bytes) {
|
||||
byte[] encryptedAndMacBody = encryptBytes(bytes);
|
||||
return Base64.encodeBytes(encryptedAndMacBody);
|
||||
}
|
||||
|
||||
private byte[] verifyMacBody(@NonNull Mac hmac, @NonNull byte[] encryptedAndMac) throws InvalidMessageException {
|
||||
if (encryptedAndMac.length < hmac.getMacLength()) {
|
||||
throw new InvalidMessageException("length(encrypted body + MAC) < length(MAC)");
|
||||
}
|
||||
|
||||
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.thoughtcrime.securesms.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,370 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
* Copyright (C) 2013 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.libsignal.ecc.Curve;
|
||||
import org.whispersystems.libsignal.ecc.ECKeyPair;
|
||||
import org.whispersystems.libsignal.ecc.ECPrivateKey;
|
||||
import org.whispersystems.libsignal.ecc.ECPublicKey;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.SecretKeyFactory;
|
||||
import javax.crypto.spec.PBEKeySpec;
|
||||
import javax.crypto.spec.PBEParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
/**
|
||||
* Helper class for generating and securely storing a MasterSecret.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class MasterSecretUtil {
|
||||
|
||||
public static final String UNENCRYPTED_PASSPHRASE = "unencrypted";
|
||||
public static final String PREFERENCES_NAME = "SecureSMS-Preferences";
|
||||
|
||||
private static final String ASYMMETRIC_LOCAL_PUBLIC_DJB = "asymmetric_master_secret_curve25519_public";
|
||||
private static final String ASYMMETRIC_LOCAL_PRIVATE_DJB = "asymmetric_master_secret_curve25519_private";
|
||||
|
||||
public static MasterSecret changeMasterSecretPassphrase(Context context,
|
||||
MasterSecret masterSecret,
|
||||
String newPassphrase)
|
||||
{
|
||||
try {
|
||||
byte[] combinedSecrets = Util.combine(masterSecret.getEncryptionKey().getEncoded(),
|
||||
masterSecret.getMacKey().getEncoded());
|
||||
|
||||
byte[] encryptionSalt = generateSalt();
|
||||
int iterations = generateIterationCount(newPassphrase, encryptionSalt);
|
||||
byte[] encryptedMasterSecret = encryptWithPassphrase(encryptionSalt, iterations, combinedSecrets, newPassphrase);
|
||||
byte[] macSalt = generateSalt();
|
||||
byte[] encryptedAndMacdMasterSecret = macWithPassphrase(macSalt, iterations, encryptedMasterSecret, newPassphrase);
|
||||
|
||||
save(context, "encryption_salt", encryptionSalt);
|
||||
save(context, "mac_salt", macSalt);
|
||||
save(context, "passphrase_iterations", iterations);
|
||||
save(context, "master_secret", encryptedAndMacdMasterSecret);
|
||||
save(context, "passphrase_initialized", true);
|
||||
|
||||
return masterSecret;
|
||||
} catch (GeneralSecurityException gse) {
|
||||
throw new AssertionError(gse);
|
||||
}
|
||||
}
|
||||
|
||||
public static MasterSecret changeMasterSecretPassphrase(Context context,
|
||||
String originalPassphrase,
|
||||
String newPassphrase)
|
||||
throws InvalidPassphraseException
|
||||
{
|
||||
MasterSecret masterSecret = getMasterSecret(context, originalPassphrase);
|
||||
changeMasterSecretPassphrase(context, masterSecret, newPassphrase);
|
||||
|
||||
return masterSecret;
|
||||
}
|
||||
|
||||
public static MasterSecret getMasterSecret(Context context, String passphrase)
|
||||
throws InvalidPassphraseException
|
||||
{
|
||||
try {
|
||||
byte[] encryptedAndMacdMasterSecret = retrieve(context, "master_secret");
|
||||
byte[] macSalt = retrieve(context, "mac_salt");
|
||||
int iterations = retrieve(context, "passphrase_iterations", 100);
|
||||
byte[] encryptedMasterSecret = verifyMac(macSalt, iterations, encryptedAndMacdMasterSecret, passphrase);
|
||||
byte[] encryptionSalt = retrieve(context, "encryption_salt");
|
||||
byte[] combinedSecrets = decryptWithPassphrase(encryptionSalt, iterations, encryptedMasterSecret, passphrase);
|
||||
byte[] encryptionSecret = Util.split(combinedSecrets, 16, 20)[0];
|
||||
byte[] macSecret = Util.split(combinedSecrets, 16, 20)[1];
|
||||
|
||||
return new MasterSecret(new SecretKeySpec(encryptionSecret, "AES"),
|
||||
new SecretKeySpec(macSecret, "HmacSHA1"));
|
||||
} catch (GeneralSecurityException e) {
|
||||
Log.w("keyutil", e);
|
||||
return null; //XXX
|
||||
} catch (IOException e) {
|
||||
Log.w("keyutil", e);
|
||||
return null; //XXX
|
||||
}
|
||||
}
|
||||
|
||||
public static AsymmetricMasterSecret getAsymmetricMasterSecret(@NonNull Context context,
|
||||
@Nullable MasterSecret masterSecret)
|
||||
{
|
||||
try {
|
||||
byte[] djbPublicBytes = retrieve(context, ASYMMETRIC_LOCAL_PUBLIC_DJB);
|
||||
byte[] djbPrivateBytes = retrieve(context, ASYMMETRIC_LOCAL_PRIVATE_DJB);
|
||||
|
||||
ECPublicKey djbPublicKey = null;
|
||||
ECPrivateKey djbPrivateKey = null;
|
||||
|
||||
if (djbPublicBytes != null) {
|
||||
djbPublicKey = Curve.decodePoint(djbPublicBytes, 0);
|
||||
}
|
||||
|
||||
if (masterSecret != null) {
|
||||
MasterCipher masterCipher = new MasterCipher(masterSecret);
|
||||
|
||||
if (djbPrivateBytes != null) {
|
||||
djbPrivateKey = masterCipher.decryptKey(djbPrivateBytes);
|
||||
}
|
||||
}
|
||||
|
||||
return new AsymmetricMasterSecret(djbPublicKey, djbPrivateKey);
|
||||
} catch (InvalidKeyException | IOException ike) {
|
||||
throw new AssertionError(ike);
|
||||
}
|
||||
}
|
||||
|
||||
public static AsymmetricMasterSecret generateAsymmetricMasterSecret(Context context,
|
||||
MasterSecret masterSecret)
|
||||
{
|
||||
MasterCipher masterCipher = new MasterCipher(masterSecret);
|
||||
ECKeyPair keyPair = Curve.generateKeyPair();
|
||||
|
||||
save(context, ASYMMETRIC_LOCAL_PUBLIC_DJB, keyPair.getPublicKey().serialize());
|
||||
save(context, ASYMMETRIC_LOCAL_PRIVATE_DJB, masterCipher.encryptKey(keyPair.getPrivateKey()));
|
||||
|
||||
return new AsymmetricMasterSecret(keyPair.getPublicKey(), keyPair.getPrivateKey());
|
||||
}
|
||||
|
||||
public static MasterSecret generateMasterSecret(Context context, String passphrase) {
|
||||
try {
|
||||
byte[] encryptionSecret = generateEncryptionSecret();
|
||||
byte[] macSecret = generateMacSecret();
|
||||
byte[] masterSecret = Util.combine(encryptionSecret, macSecret);
|
||||
byte[] encryptionSalt = generateSalt();
|
||||
int iterations = generateIterationCount(passphrase, encryptionSalt);
|
||||
byte[] encryptedMasterSecret = encryptWithPassphrase(encryptionSalt, iterations, masterSecret, passphrase);
|
||||
byte[] macSalt = generateSalt();
|
||||
byte[] encryptedAndMacdMasterSecret = macWithPassphrase(macSalt, iterations, encryptedMasterSecret, passphrase);
|
||||
|
||||
save(context, "encryption_salt", encryptionSalt);
|
||||
save(context, "mac_salt", macSalt);
|
||||
save(context, "passphrase_iterations", iterations);
|
||||
save(context, "master_secret", encryptedAndMacdMasterSecret);
|
||||
save(context, "passphrase_initialized", true);
|
||||
|
||||
return new MasterSecret(new SecretKeySpec(encryptionSecret, "AES"),
|
||||
new SecretKeySpec(macSecret, "HmacSHA1"));
|
||||
} catch (GeneralSecurityException e) {
|
||||
Log.w("keyutil", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean hasAsymmericMasterSecret(Context context) {
|
||||
SharedPreferences settings = context.getSharedPreferences(PREFERENCES_NAME, 0);
|
||||
return settings.contains(ASYMMETRIC_LOCAL_PUBLIC_DJB);
|
||||
}
|
||||
|
||||
public static boolean isPassphraseInitialized(Context context) {
|
||||
SharedPreferences preferences = context.getSharedPreferences(PREFERENCES_NAME, 0);
|
||||
return preferences.getBoolean("passphrase_initialized", false);
|
||||
}
|
||||
|
||||
private static void save(Context context, String key, int value) {
|
||||
if (!context.getSharedPreferences(PREFERENCES_NAME, 0)
|
||||
.edit()
|
||||
.putInt(key, value)
|
||||
.commit())
|
||||
{
|
||||
throw new AssertionError("failed to save a shared pref in MasterSecretUtil");
|
||||
}
|
||||
}
|
||||
|
||||
private static void save(Context context, String key, byte[] value) {
|
||||
if (!context.getSharedPreferences(PREFERENCES_NAME, 0)
|
||||
.edit()
|
||||
.putString(key, Base64.encodeBytes(value))
|
||||
.commit())
|
||||
{
|
||||
throw new AssertionError("failed to save a shared pref in MasterSecretUtil");
|
||||
}
|
||||
}
|
||||
|
||||
private static void save(Context context, String key, boolean value) {
|
||||
if (!context.getSharedPreferences(PREFERENCES_NAME, 0)
|
||||
.edit()
|
||||
.putBoolean(key, value)
|
||||
.commit())
|
||||
{
|
||||
throw new AssertionError("failed to save a shared pref in MasterSecretUtil");
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] retrieve(Context context, String key) throws IOException {
|
||||
SharedPreferences settings = context.getSharedPreferences(PREFERENCES_NAME, 0);
|
||||
String encodedValue = settings.getString(key, "");
|
||||
|
||||
if (TextUtils.isEmpty(encodedValue)) return null;
|
||||
else return Base64.decode(encodedValue);
|
||||
}
|
||||
|
||||
private static int retrieve(Context context, String key, int defaultValue) throws IOException {
|
||||
SharedPreferences settings = context.getSharedPreferences(PREFERENCES_NAME, 0);
|
||||
return settings.getInt(key, defaultValue);
|
||||
}
|
||||
|
||||
private static byte[] generateEncryptionSecret() {
|
||||
try {
|
||||
KeyGenerator generator = KeyGenerator.getInstance("AES");
|
||||
generator.init(128);
|
||||
|
||||
SecretKey key = generator.generateKey();
|
||||
return key.getEncoded();
|
||||
} catch (NoSuchAlgorithmException ex) {
|
||||
Log.w("keyutil", ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] generateMacSecret() {
|
||||
try {
|
||||
KeyGenerator generator = KeyGenerator.getInstance("HmacSHA1");
|
||||
return generator.generateKey().getEncoded();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
Log.w("keyutil", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] generateSalt() {
|
||||
SecureRandom random = new SecureRandom();
|
||||
byte[] salt = new byte[16];
|
||||
random.nextBytes(salt);
|
||||
|
||||
return salt;
|
||||
}
|
||||
|
||||
private static int generateIterationCount(String passphrase, byte[] salt) {
|
||||
int TARGET_ITERATION_TIME = 50; //ms
|
||||
int MINIMUM_ITERATION_COUNT = 100; //default for low-end devices
|
||||
int BENCHMARK_ITERATION_COUNT = 10000; //baseline starting iteration count
|
||||
|
||||
try {
|
||||
PBEKeySpec keyspec = new PBEKeySpec(passphrase.toCharArray(), salt, BENCHMARK_ITERATION_COUNT);
|
||||
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBEWITHSHA1AND128BITAES-CBC-BC");
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
skf.generateSecret(keyspec);
|
||||
long finishTime = System.currentTimeMillis();
|
||||
|
||||
int scaledIterationTarget = (int) (((double)BENCHMARK_ITERATION_COUNT / (double)(finishTime - startTime)) * TARGET_ITERATION_TIME);
|
||||
|
||||
if (scaledIterationTarget < MINIMUM_ITERATION_COUNT) return MINIMUM_ITERATION_COUNT;
|
||||
else return scaledIterationTarget;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
Log.w("MasterSecretUtil", e);
|
||||
return MINIMUM_ITERATION_COUNT;
|
||||
} catch (InvalidKeySpecException e) {
|
||||
Log.w("MasterSecretUtil", e);
|
||||
return MINIMUM_ITERATION_COUNT;
|
||||
}
|
||||
}
|
||||
|
||||
private static SecretKey getKeyFromPassphrase(String passphrase, byte[] salt, int iterations)
|
||||
throws GeneralSecurityException
|
||||
{
|
||||
PBEKeySpec keyspec = new PBEKeySpec(passphrase.toCharArray(), salt, iterations);
|
||||
SecretKeyFactory skf = SecretKeyFactory.getInstance("PBEWITHSHA1AND128BITAES-CBC-BC");
|
||||
return skf.generateSecret(keyspec);
|
||||
}
|
||||
|
||||
private static Cipher getCipherFromPassphrase(String passphrase, byte[] salt, int iterations, int opMode)
|
||||
throws GeneralSecurityException
|
||||
{
|
||||
SecretKey key = getKeyFromPassphrase(passphrase, salt, iterations);
|
||||
Cipher cipher = Cipher.getInstance(key.getAlgorithm());
|
||||
cipher.init(opMode, key, new PBEParameterSpec(salt, iterations));
|
||||
|
||||
return cipher;
|
||||
}
|
||||
|
||||
private static byte[] encryptWithPassphrase(byte[] encryptionSalt, int iterations, byte[] data, String passphrase)
|
||||
throws GeneralSecurityException
|
||||
{
|
||||
Cipher cipher = getCipherFromPassphrase(passphrase, encryptionSalt, iterations, Cipher.ENCRYPT_MODE);
|
||||
return cipher.doFinal(data);
|
||||
}
|
||||
|
||||
private static byte[] decryptWithPassphrase(byte[] encryptionSalt, int iterations, byte[] data, String passphrase)
|
||||
throws GeneralSecurityException, IOException
|
||||
{
|
||||
Cipher cipher = getCipherFromPassphrase(passphrase, encryptionSalt, iterations, Cipher.DECRYPT_MODE);
|
||||
return cipher.doFinal(data);
|
||||
}
|
||||
|
||||
private static Mac getMacForPassphrase(String passphrase, byte[] salt, int iterations)
|
||||
throws GeneralSecurityException
|
||||
{
|
||||
SecretKey key = getKeyFromPassphrase(passphrase, salt, iterations);
|
||||
byte[] pbkdf2 = key.getEncoded();
|
||||
SecretKeySpec hmacKey = new SecretKeySpec(pbkdf2, "HmacSHA1");
|
||||
Mac hmac = Mac.getInstance("HmacSHA1");
|
||||
hmac.init(hmacKey);
|
||||
|
||||
return hmac;
|
||||
}
|
||||
|
||||
private static byte[] verifyMac(byte[] macSalt, int iterations, byte[] encryptedAndMacdData, String passphrase) throws InvalidPassphraseException, GeneralSecurityException, IOException {
|
||||
Mac hmac = getMacForPassphrase(passphrase, macSalt, iterations);
|
||||
|
||||
byte[] encryptedData = new byte[encryptedAndMacdData.length - hmac.getMacLength()];
|
||||
System.arraycopy(encryptedAndMacdData, 0, encryptedData, 0, encryptedData.length);
|
||||
|
||||
byte[] givenMac = new byte[hmac.getMacLength()];
|
||||
System.arraycopy(encryptedAndMacdData, encryptedAndMacdData.length-hmac.getMacLength(), givenMac, 0, givenMac.length);
|
||||
|
||||
byte[] localMac = hmac.doFinal(encryptedData);
|
||||
|
||||
if (Arrays.equals(givenMac, localMac)) return encryptedData;
|
||||
else throw new InvalidPassphraseException("MAC Error");
|
||||
}
|
||||
|
||||
private static byte[] macWithPassphrase(byte[] macSalt, int iterations, byte[] data, String passphrase) throws GeneralSecurityException {
|
||||
Mac hmac = getMacForPassphrase(passphrase, macSalt, iterations);
|
||||
byte[] mac = hmac.doFinal(data);
|
||||
byte[] result = new byte[data.length + mac.length];
|
||||
|
||||
System.arraycopy(data, 0, result, 0, data.length);
|
||||
System.arraycopy(mac, 0, result, data.length, mac.length);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.util.Conversions;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.CipherInputStream;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class ModernDecryptingPartInputStream {
|
||||
|
||||
public static InputStream createFor(@NonNull AttachmentSecret attachmentSecret, @NonNull byte[] random, @NonNull File file, long offset)
|
||||
throws IOException
|
||||
{
|
||||
return createFor(attachmentSecret, random, new FileInputStream(file), offset);
|
||||
}
|
||||
|
||||
public static InputStream createFor(@NonNull AttachmentSecret attachmentSecret, @NonNull File file, long offset)
|
||||
throws IOException
|
||||
{
|
||||
FileInputStream inputStream = new FileInputStream(file);
|
||||
byte[] random = new byte[32];
|
||||
|
||||
readFully(inputStream, random);
|
||||
|
||||
return createFor(attachmentSecret, random, inputStream, offset);
|
||||
}
|
||||
|
||||
private static InputStream createFor(@NonNull AttachmentSecret attachmentSecret, @NonNull byte[] random, @NonNull InputStream inputStream, long offset) throws IOException {
|
||||
try {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(attachmentSecret.getModernKey(), "HmacSHA256"));
|
||||
|
||||
byte[] iv = new byte[16];
|
||||
int remainder = (int) (offset % 16);
|
||||
Conversions.longTo4ByteArray(iv, 12, offset / 16);
|
||||
|
||||
byte[] key = mac.doFinal(random);
|
||||
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
|
||||
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));
|
||||
|
||||
long skipped = inputStream.skip(offset - remainder);
|
||||
|
||||
if (skipped != offset - remainder) {
|
||||
throw new IOException("Skip failed: " + skipped + " vs " + (offset - remainder));
|
||||
}
|
||||
|
||||
CipherInputStream cipherInputStream = new CipherInputStream(inputStream, cipher);
|
||||
byte[] remainderBuffer = new byte[remainder];
|
||||
|
||||
readFully(cipherInputStream, remainderBuffer);
|
||||
|
||||
return cipherInputStream;
|
||||
} catch (NoSuchAlgorithmException | InvalidKeyException | InvalidAlgorithmParameterException | NoSuchPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void readFully(InputStream in, byte[] buffer) throws IOException {
|
||||
int offset = 0;
|
||||
|
||||
for (;;) {
|
||||
int read = in.read(buffer, offset, buffer.length-offset);
|
||||
|
||||
if (read == -1) throw new IOException("Prematurely reached end of stream!");
|
||||
else if (read + offset < buffer.length) offset += read;
|
||||
else return;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import android.util.Pair;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.CipherOutputStream;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
/**
|
||||
* Constructs an OutputStream that encrypts data written to it with the AttachmentSecret provided.
|
||||
*
|
||||
* The on-disk format is very simple, and intentionally no longer includes authentication.
|
||||
*/
|
||||
public class ModernEncryptingPartOutputStream {
|
||||
|
||||
public static Pair<byte[], OutputStream> createFor(@NonNull AttachmentSecret attachmentSecret, @NonNull File file, boolean inline)
|
||||
throws IOException
|
||||
{
|
||||
byte[] random = new byte[32];
|
||||
new SecureRandom().nextBytes(random);
|
||||
|
||||
try {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(attachmentSecret.getModernKey(), "HmacSHA256"));
|
||||
|
||||
FileOutputStream fileOutputStream = new FileOutputStream(file);
|
||||
byte[] iv = new byte[16];
|
||||
byte[] key = mac.doFinal(random);
|
||||
|
||||
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));
|
||||
|
||||
if (inline) {
|
||||
fileOutputStream.write(random);
|
||||
}
|
||||
|
||||
return new Pair<>(random, new CipherOutputStream(fileOutputStream, cipher));
|
||||
} catch (NoSuchAlgorithmException | InvalidKeyException | InvalidAlgorithmParameterException | NoSuchPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright (C) 2013-2018 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.storage.TextSecurePreKeyStore;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.libsignal.IdentityKeyPair;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.libsignal.ecc.Curve;
|
||||
import org.whispersystems.libsignal.ecc.ECKeyPair;
|
||||
import org.whispersystems.libsignal.state.PreKeyRecord;
|
||||
import org.whispersystems.libsignal.state.PreKeyStore;
|
||||
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
|
||||
import org.whispersystems.libsignal.state.SignedPreKeyStore;
|
||||
import org.whispersystems.libsignal.util.Medium;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class PreKeyUtil {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = PreKeyUtil.class.getSimpleName();
|
||||
|
||||
private static final int BATCH_SIZE = 100;
|
||||
|
||||
public synchronized static List<PreKeyRecord> generatePreKeys(Context context) {
|
||||
PreKeyStore preKeyStore = new TextSecurePreKeyStore(context);
|
||||
List<PreKeyRecord> records = new LinkedList<>();
|
||||
int preKeyIdOffset = TextSecurePreferences.getNextPreKeyId(context);
|
||||
|
||||
for (int i=0;i<BATCH_SIZE;i++) {
|
||||
int preKeyId = (preKeyIdOffset + i) % Medium.MAX_VALUE;
|
||||
ECKeyPair keyPair = Curve.generateKeyPair();
|
||||
PreKeyRecord record = new PreKeyRecord(preKeyId, keyPair);
|
||||
|
||||
preKeyStore.storePreKey(preKeyId, record);
|
||||
records.add(record);
|
||||
}
|
||||
|
||||
TextSecurePreferences.setNextPreKeyId(context, (preKeyIdOffset + BATCH_SIZE + 1) % Medium.MAX_VALUE);
|
||||
|
||||
return records;
|
||||
}
|
||||
|
||||
public synchronized static SignedPreKeyRecord generateSignedPreKey(Context context, IdentityKeyPair identityKeyPair, boolean active) {
|
||||
try {
|
||||
SignedPreKeyStore signedPreKeyStore = new TextSecurePreKeyStore(context);
|
||||
int signedPreKeyId = TextSecurePreferences.getNextSignedPreKeyId(context);
|
||||
ECKeyPair keyPair = Curve.generateKeyPair();
|
||||
byte[] signature = Curve.calculateSignature(identityKeyPair.getPrivateKey(), keyPair.getPublicKey().serialize());
|
||||
SignedPreKeyRecord record = new SignedPreKeyRecord(signedPreKeyId, System.currentTimeMillis(), keyPair, signature);
|
||||
|
||||
signedPreKeyStore.storeSignedPreKey(signedPreKeyId, record);
|
||||
TextSecurePreferences.setNextSignedPreKeyId(context, (signedPreKeyId + 1) % Medium.MAX_VALUE);
|
||||
|
||||
if (active) {
|
||||
TextSecurePreferences.setActiveSignedPreKeyId(context, signedPreKeyId);
|
||||
}
|
||||
|
||||
return record;
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized void setActiveSignedPreKeyId(Context context, int id) {
|
||||
TextSecurePreferences.setActiveSignedPreKeyId(context, id);
|
||||
}
|
||||
|
||||
public static synchronized int getActiveSignedPreKeyId(Context context) {
|
||||
return TextSecurePreferences.getActiveSignedPreKeyId(context);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class ProfileKeyUtil {
|
||||
|
||||
public static synchronized boolean hasProfileKey(@NonNull Context context) {
|
||||
return TextSecurePreferences.getProfileKey(context) != null;
|
||||
}
|
||||
|
||||
public static synchronized @NonNull byte[] getProfileKey(@NonNull Context context) {
|
||||
try {
|
||||
String encodedProfileKey = TextSecurePreferences.getProfileKey(context);
|
||||
|
||||
if (encodedProfileKey == null) {
|
||||
encodedProfileKey = Util.getSecret(32);
|
||||
TextSecurePreferences.setProfileKey(context, encodedProfileKey);
|
||||
}
|
||||
|
||||
return Base64.decode(encodedProfileKey);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized @NonNull byte[] rotateProfileKey(@NonNull Context context) {
|
||||
TextSecurePreferences.setProfileKey(context, null);
|
||||
return getProfileKey(context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
* Copyright (C) 2013 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.Hex;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.libsignal.ecc.Curve;
|
||||
import org.whispersystems.libsignal.ecc.ECPublicKey;
|
||||
import org.thoughtcrime.securesms.util.Conversions;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
public class PublicKey {
|
||||
|
||||
private static final String TAG = PublicKey.class.getSimpleName();
|
||||
|
||||
public static final int KEY_SIZE = 3 + ECPublicKey.KEY_SIZE;
|
||||
|
||||
private final ECPublicKey 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, ECPublicKey publicKey) {
|
||||
this.publicKey = publicKey;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public PublicKey(byte[] bytes, int offset) throws InvalidKeyException {
|
||||
Log.i(TAG, "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 = Curve.decodePoint(bytes, offset + 3);
|
||||
}
|
||||
|
||||
public PublicKey(byte[] bytes) throws InvalidKeyException {
|
||||
this(bytes, 0);
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return publicKey.getType();
|
||||
}
|
||||
|
||||
public void setId(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public ECPublicKey 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[] keyIdBytes = Conversions.mediumToByteArray(id);
|
||||
byte[] serializedPoint = publicKey.serialize();
|
||||
|
||||
Log.i(TAG, "Serializing public key point: " + Hex.toString(serializedPoint));
|
||||
|
||||
return Util.combine(keyIdBytes, serializedPoint);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
|
||||
/**
|
||||
* This class processes key exchange interactions.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class SecurityEvent {
|
||||
|
||||
public static final String SECURITY_UPDATE_EVENT = "org.thoughtcrime.securesms.KEY_EXCHANGE_UPDATE";
|
||||
|
||||
public static void broadcastSecurityUpdateEvent(Context context) {
|
||||
Intent intent = new Intent(SECURITY_UPDATE_EVENT);
|
||||
intent.setPackage(context.getPackageName());
|
||||
context.sendBroadcast(intent, KeyCachingService.KEY_PERMISSION);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
import org.whispersystems.libsignal.state.SessionStore;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
||||
public class SessionUtil {
|
||||
|
||||
public static boolean hasSession(@NonNull Context context, @NonNull RecipientId id) {
|
||||
SessionStore sessionStore = new TextSecureSessionStore(context);
|
||||
SignalProtocolAddress axolotlAddress = new SignalProtocolAddress(Recipient.resolved(id).requireServiceId(), SignalServiceAddress.DEFAULT_DEVICE_ID);
|
||||
|
||||
return sessionStore.containsSession(axolotlAddress);
|
||||
}
|
||||
|
||||
public static void archiveSiblingSessions(Context context, SignalProtocolAddress address) {
|
||||
TextSecureSessionStore sessionStore = new TextSecureSessionStore(context);
|
||||
sessionStore.archiveSiblingSessions(address);
|
||||
}
|
||||
|
||||
public static void archiveAllSessions(Context context) {
|
||||
new TextSecureSessionStore(context).archiveAllSessions();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package org.thoughtcrime.securesms.crypto;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.signal.libsignal.metadata.certificate.CertificateValidator;
|
||||
import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
|
||||
import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.libsignal.ecc.Curve;
|
||||
import org.whispersystems.libsignal.ecc.ECPublicKey;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class UnidentifiedAccessUtil {
|
||||
|
||||
private static final String TAG = UnidentifiedAccessUtil.class.getSimpleName();
|
||||
|
||||
public static CertificateValidator getCertificateValidator() {
|
||||
try {
|
||||
ECPublicKey unidentifiedSenderTrustRoot = Curve.decodePoint(Base64.decode(BuildConfig.UNIDENTIFIED_SENDER_TRUST_ROOT), 0);
|
||||
return new CertificateValidator(unidentifiedSenderTrustRoot);
|
||||
} catch (InvalidKeyException | IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static Optional<UnidentifiedAccessPair> getAccessFor(@NonNull Context context,
|
||||
@NonNull Recipient recipient)
|
||||
{
|
||||
try {
|
||||
byte[] theirUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient);
|
||||
byte[] ourUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(context);
|
||||
byte[] ourUnidentifiedAccessCertificate = recipient.resolve().isUuidSupported() && Recipient.self().isUuidSupported()
|
||||
? TextSecurePreferences.getUnidentifiedAccessCertificate(context)
|
||||
: TextSecurePreferences.getUnidentifiedAccessCertificateLegacy(context);
|
||||
|
||||
if (TextSecurePreferences.isUniversalUnidentifiedAccess(context)) {
|
||||
ourUnidentifiedAccessKey = Util.getSecretBytes(16);
|
||||
}
|
||||
|
||||
Log.i(TAG, "Their access key present? " + (theirUnidentifiedAccessKey != null) +
|
||||
" | Our access key present? " + (ourUnidentifiedAccessKey != null) +
|
||||
" | Our certificate present? " + (ourUnidentifiedAccessCertificate != null) +
|
||||
" | UUID certificate supported? " + recipient.isUuidSupported());
|
||||
|
||||
if (theirUnidentifiedAccessKey != null &&
|
||||
ourUnidentifiedAccessKey != null &&
|
||||
ourUnidentifiedAccessCertificate != null)
|
||||
{
|
||||
return Optional.of(new UnidentifiedAccessPair(new UnidentifiedAccess(theirUnidentifiedAccessKey,
|
||||
ourUnidentifiedAccessCertificate),
|
||||
new UnidentifiedAccess(ourUnidentifiedAccessKey,
|
||||
ourUnidentifiedAccessCertificate)));
|
||||
}
|
||||
|
||||
return Optional.absent();
|
||||
} catch (InvalidCertificateException e) {
|
||||
Log.w(TAG, e);
|
||||
return Optional.absent();
|
||||
}
|
||||
}
|
||||
|
||||
public static Optional<UnidentifiedAccessPair> getAccessForSync(@NonNull Context context) {
|
||||
try {
|
||||
byte[] ourUnidentifiedAccessKey = getSelfUnidentifiedAccessKey(context);
|
||||
byte[] ourUnidentifiedAccessCertificate = Recipient.self().isUuidSupported() ? TextSecurePreferences.getUnidentifiedAccessCertificate(context)
|
||||
: TextSecurePreferences.getUnidentifiedAccessCertificateLegacy(context);
|
||||
|
||||
if (TextSecurePreferences.isUniversalUnidentifiedAccess(context)) {
|
||||
ourUnidentifiedAccessKey = Util.getSecretBytes(16);
|
||||
}
|
||||
|
||||
if (ourUnidentifiedAccessKey != null && ourUnidentifiedAccessCertificate != null) {
|
||||
return Optional.of(new UnidentifiedAccessPair(new UnidentifiedAccess(ourUnidentifiedAccessKey,
|
||||
ourUnidentifiedAccessCertificate),
|
||||
new UnidentifiedAccess(ourUnidentifiedAccessKey,
|
||||
ourUnidentifiedAccessCertificate)));
|
||||
}
|
||||
|
||||
return Optional.absent();
|
||||
} catch (InvalidCertificateException e) {
|
||||
Log.w(TAG, e);
|
||||
return Optional.absent();
|
||||
}
|
||||
}
|
||||
|
||||
public static @NonNull byte[] getSelfUnidentifiedAccessKey(@NonNull Context context) {
|
||||
return UnidentifiedAccess.deriveAccessKeyFrom(ProfileKeyUtil.getProfileKey(context));
|
||||
}
|
||||
|
||||
private static @Nullable byte[] getTargetUnidentifiedAccessKey(@NonNull Recipient recipient) {
|
||||
byte[] theirProfileKey = recipient.resolve().getProfileKey();
|
||||
|
||||
switch (recipient.resolve().getUnidentifiedAccessMode()) {
|
||||
case UNKNOWN:
|
||||
if (theirProfileKey == null) return Util.getSecretBytes(16);
|
||||
else return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey);
|
||||
case DISABLED:
|
||||
return null;
|
||||
case ENABLED:
|
||||
if (theirProfileKey == null) return null;
|
||||
else return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey);
|
||||
case UNRESTRICTED:
|
||||
return Util.getSecretBytes(16);
|
||||
default:
|
||||
throw new AssertionError("Unknown mode: " + recipient.getUnidentifiedAccessMode().getMode());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package org.thoughtcrime.securesms.crypto.storage;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.IdentityKeyPair;
|
||||
import org.whispersystems.libsignal.InvalidKeyIdException;
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
import org.whispersystems.libsignal.state.IdentityKeyStore;
|
||||
import org.whispersystems.libsignal.state.PreKeyRecord;
|
||||
import org.whispersystems.libsignal.state.PreKeyStore;
|
||||
import org.whispersystems.libsignal.state.SessionRecord;
|
||||
import org.whispersystems.libsignal.state.SessionStore;
|
||||
import org.whispersystems.libsignal.state.SignalProtocolStore;
|
||||
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
|
||||
import org.whispersystems.libsignal.state.SignedPreKeyStore;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SignalProtocolStoreImpl implements SignalProtocolStore {
|
||||
|
||||
private final PreKeyStore preKeyStore;
|
||||
private final SignedPreKeyStore signedPreKeyStore;
|
||||
private final IdentityKeyStore identityKeyStore;
|
||||
private final SessionStore sessionStore;
|
||||
|
||||
public SignalProtocolStoreImpl(Context context) {
|
||||
this.preKeyStore = new TextSecurePreKeyStore(context);
|
||||
this.signedPreKeyStore = new TextSecurePreKeyStore(context);
|
||||
this.identityKeyStore = new TextSecureIdentityKeyStore(context);
|
||||
this.sessionStore = new TextSecureSessionStore(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentityKeyPair getIdentityKeyPair() {
|
||||
return identityKeyStore.getIdentityKeyPair();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLocalRegistrationId() {
|
||||
return identityKeyStore.getLocalRegistrationId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
|
||||
return identityKeyStore.saveIdentity(address, identityKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) {
|
||||
return identityKeyStore.isTrustedIdentity(address, identityKey, direction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentityKey getIdentity(SignalProtocolAddress address) {
|
||||
return identityKeyStore.getIdentity(address);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException {
|
||||
return preKeyStore.loadPreKey(preKeyId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storePreKey(int preKeyId, PreKeyRecord record) {
|
||||
preKeyStore.storePreKey(preKeyId, record);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsPreKey(int preKeyId) {
|
||||
return preKeyStore.containsPreKey(preKeyId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removePreKey(int preKeyId) {
|
||||
preKeyStore.removePreKey(preKeyId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SessionRecord loadSession(SignalProtocolAddress axolotlAddress) {
|
||||
return sessionStore.loadSession(axolotlAddress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Integer> getSubDeviceSessions(String number) {
|
||||
return sessionStore.getSubDeviceSessions(number);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storeSession(SignalProtocolAddress axolotlAddress, SessionRecord record) {
|
||||
sessionStore.storeSession(axolotlAddress, record);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsSession(SignalProtocolAddress axolotlAddress) {
|
||||
return sessionStore.containsSession(axolotlAddress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteSession(SignalProtocolAddress axolotlAddress) {
|
||||
sessionStore.deleteSession(axolotlAddress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteAllSessions(String number) {
|
||||
sessionStore.deleteAllSessions(number);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException {
|
||||
return signedPreKeyStore.loadSignedPreKey(signedPreKeyId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SignedPreKeyRecord> loadSignedPreKeys() {
|
||||
return signedPreKeyStore.loadSignedPreKeys();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) {
|
||||
signedPreKeyStore.storeSignedPreKey(signedPreKeyId, record);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsSignedPreKey(int signedPreKeyId) {
|
||||
return signedPreKeyStore.containsSignedPreKey(signedPreKeyId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeSignedPreKey(int signedPreKeyId) {
|
||||
signedPreKeyStore.removeSignedPreKey(signedPreKeyId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package org.thoughtcrime.securesms.crypto.storage;
|
||||
|
||||
import android.content.Context;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.crypto.SessionUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.IdentityKeyPair;
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
import org.whispersystems.libsignal.state.IdentityKeyStore;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class TextSecureIdentityKeyStore implements IdentityKeyStore {
|
||||
|
||||
private static final int TIMESTAMP_THRESHOLD_SECONDS = 5;
|
||||
|
||||
private static final String TAG = TextSecureIdentityKeyStore.class.getSimpleName();
|
||||
private static final Object LOCK = new Object();
|
||||
|
||||
private final Context context;
|
||||
|
||||
public TextSecureIdentityKeyStore(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentityKeyPair getIdentityKeyPair() {
|
||||
return IdentityKeyUtil.getIdentityKeyPair(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLocalRegistrationId() {
|
||||
return TextSecurePreferences.getLocalRegistrationId(context);
|
||||
}
|
||||
|
||||
public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey, boolean nonBlockingApproval) {
|
||||
synchronized (LOCK) {
|
||||
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
|
||||
Recipient recipient = Recipient.external(context, address.getName());
|
||||
Optional<IdentityRecord> identityRecord = identityDatabase.getIdentity(recipient.getId());
|
||||
|
||||
if (!identityRecord.isPresent()) {
|
||||
Log.i(TAG, "Saving new identity...");
|
||||
identityDatabase.saveIdentity(recipient.getId(), identityKey, VerifiedStatus.DEFAULT, true, System.currentTimeMillis(), nonBlockingApproval);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!identityRecord.get().getIdentityKey().equals(identityKey)) {
|
||||
Log.i(TAG, "Replacing existing identity...");
|
||||
VerifiedStatus verifiedStatus;
|
||||
|
||||
if (identityRecord.get().getVerifiedStatus() == VerifiedStatus.VERIFIED ||
|
||||
identityRecord.get().getVerifiedStatus() == VerifiedStatus.UNVERIFIED)
|
||||
{
|
||||
verifiedStatus = VerifiedStatus.UNVERIFIED;
|
||||
} else {
|
||||
verifiedStatus = VerifiedStatus.DEFAULT;
|
||||
}
|
||||
|
||||
identityDatabase.saveIdentity(recipient.getId(), identityKey, verifiedStatus, false, System.currentTimeMillis(), nonBlockingApproval);
|
||||
IdentityUtil.markIdentityUpdate(context, recipient);
|
||||
SessionUtil.archiveSiblingSessions(context, address);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isNonBlockingApprovalRequired(identityRecord.get())) {
|
||||
Log.i(TAG, "Setting approval status...");
|
||||
identityDatabase.setApproval(recipient.getId(), nonBlockingApproval);
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean saveIdentity(SignalProtocolAddress address, IdentityKey identityKey) {
|
||||
return saveIdentity(address, identityKey, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTrustedIdentity(SignalProtocolAddress address, IdentityKey identityKey, Direction direction) {
|
||||
synchronized (LOCK) {
|
||||
if (DatabaseFactory.getRecipientDatabase(context).containsPhoneOrUuid(address.getName())) {
|
||||
IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(context);
|
||||
RecipientId ourRecipientId = Recipient.self().getId();
|
||||
RecipientId theirRecipientId = Recipient.external(context, address.getName()).getId();
|
||||
|
||||
if (ourRecipientId.equals(theirRecipientId)) {
|
||||
return identityKey.equals(IdentityKeyUtil.getIdentityKey(context));
|
||||
}
|
||||
|
||||
switch (direction) {
|
||||
case SENDING: return isTrustedForSending(identityKey, identityDatabase.getIdentity(theirRecipientId));
|
||||
case RECEIVING: return true;
|
||||
default: throw new AssertionError("Unknown direction: " + direction);
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Tried to check if identity is trusted for " + address.getName() + ", but no matching recipient existed!");
|
||||
switch (direction) {
|
||||
case SENDING: return false;
|
||||
case RECEIVING: return true;
|
||||
default: throw new AssertionError("Unknown direction: " + direction);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentityKey getIdentity(SignalProtocolAddress address) {
|
||||
if (DatabaseFactory.getRecipientDatabase(context).containsPhoneOrUuid(address.getName())) {
|
||||
RecipientId recipientId = Recipient.external(context, address.getName()).getId();
|
||||
Optional<IdentityRecord> record = DatabaseFactory.getIdentityDatabase(context).getIdentity(recipientId);
|
||||
|
||||
if (record.isPresent()) {
|
||||
return record.get().getIdentityKey();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Tried to get identity for " + address.getName() + ", but no matching recipient existed!");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isTrustedForSending(IdentityKey identityKey, Optional<IdentityRecord> identityRecord) {
|
||||
if (!identityRecord.isPresent()) {
|
||||
Log.w(TAG, "Nothing here, returning true...");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!identityKey.equals(identityRecord.get().getIdentityKey())) {
|
||||
Log.w(TAG, "Identity keys don't match...");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (identityRecord.get().getVerifiedStatus() == VerifiedStatus.UNVERIFIED) {
|
||||
Log.w(TAG, "Needs unverified approval!");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isNonBlockingApprovalRequired(identityRecord.get())) {
|
||||
Log.w(TAG, "Needs non-blocking approval!");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isNonBlockingApprovalRequired(IdentityRecord identityRecord) {
|
||||
return !identityRecord.isFirstUse() &&
|
||||
System.currentTimeMillis() - identityRecord.getTimestamp() < TimeUnit.SECONDS.toMillis(TIMESTAMP_THRESHOLD_SECONDS) &&
|
||||
!identityRecord.isApprovedNonBlocking();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package org.thoughtcrime.securesms.crypto.storage;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.whispersystems.libsignal.InvalidKeyIdException;
|
||||
import org.whispersystems.libsignal.state.PreKeyRecord;
|
||||
import org.whispersystems.libsignal.state.PreKeyStore;
|
||||
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
|
||||
import org.whispersystems.libsignal.state.SignedPreKeyStore;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class TextSecurePreKeyStore implements PreKeyStore, SignedPreKeyStore {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = TextSecurePreKeyStore.class.getSimpleName();
|
||||
|
||||
private static final Object FILE_LOCK = new Object();
|
||||
|
||||
@NonNull
|
||||
private final Context context;
|
||||
|
||||
public TextSecurePreKeyStore(@NonNull Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException {
|
||||
synchronized (FILE_LOCK) {
|
||||
PreKeyRecord preKeyRecord = DatabaseFactory.getPreKeyDatabase(context).getPreKey(preKeyId);
|
||||
|
||||
if (preKeyRecord == null) throw new InvalidKeyIdException("No such key: " + preKeyId);
|
||||
else return preKeyRecord;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException {
|
||||
synchronized (FILE_LOCK) {
|
||||
SignedPreKeyRecord signedPreKeyRecord = DatabaseFactory.getSignedPreKeyDatabase(context).getSignedPreKey(signedPreKeyId);
|
||||
|
||||
if (signedPreKeyRecord == null) throw new InvalidKeyIdException("No such signed prekey: " + signedPreKeyId);
|
||||
else return signedPreKeyRecord;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SignedPreKeyRecord> loadSignedPreKeys() {
|
||||
synchronized (FILE_LOCK) {
|
||||
return DatabaseFactory.getSignedPreKeyDatabase(context).getAllSignedPreKeys();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storePreKey(int preKeyId, PreKeyRecord record) {
|
||||
synchronized (FILE_LOCK) {
|
||||
DatabaseFactory.getPreKeyDatabase(context).insertPreKey(preKeyId, record);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) {
|
||||
synchronized (FILE_LOCK) {
|
||||
DatabaseFactory.getSignedPreKeyDatabase(context).insertSignedPreKey(signedPreKeyId, record);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsPreKey(int preKeyId) {
|
||||
return DatabaseFactory.getPreKeyDatabase(context).getPreKey(preKeyId) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsSignedPreKey(int signedPreKeyId) {
|
||||
return DatabaseFactory.getSignedPreKeyDatabase(context).getSignedPreKey(signedPreKeyId) != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removePreKey(int preKeyId) {
|
||||
DatabaseFactory.getPreKeyDatabase(context).removePreKey(preKeyId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeSignedPreKey(int signedPreKeyId) {
|
||||
DatabaseFactory.getSignedPreKeyDatabase(context).removeSignedPreKey(signedPreKeyId);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package org.thoughtcrime.securesms.crypto.storage;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.SessionDatabase;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.whispersystems.libsignal.SignalProtocolAddress;
|
||||
import org.whispersystems.libsignal.protocol.CiphertextMessage;
|
||||
import org.whispersystems.libsignal.state.SessionRecord;
|
||||
import org.whispersystems.libsignal.state.SessionStore;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public class TextSecureSessionStore implements SessionStore {
|
||||
|
||||
private static final String TAG = TextSecureSessionStore.class.getSimpleName();
|
||||
|
||||
private static final Object FILE_LOCK = new Object();
|
||||
|
||||
@NonNull private final Context context;
|
||||
|
||||
public TextSecureSessionStore(@NonNull Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SessionRecord loadSession(@NonNull SignalProtocolAddress address) {
|
||||
synchronized (FILE_LOCK) {
|
||||
RecipientId recipientId = Recipient.external(context, address.getName()).getId();
|
||||
SessionRecord sessionRecord = DatabaseFactory.getSessionDatabase(context).load(recipientId, address.getDeviceId());
|
||||
|
||||
if (sessionRecord == null) {
|
||||
Log.w(TAG, "No existing session information found.");
|
||||
return new SessionRecord();
|
||||
}
|
||||
|
||||
return sessionRecord;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storeSession(@NonNull SignalProtocolAddress address, @NonNull SessionRecord record) {
|
||||
synchronized (FILE_LOCK) {
|
||||
RecipientId id = Recipient.external(context, address.getName()).getId();
|
||||
DatabaseFactory.getSessionDatabase(context).store(id, address.getDeviceId(), record);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsSession(SignalProtocolAddress address) {
|
||||
synchronized (FILE_LOCK) {
|
||||
if (DatabaseFactory.getRecipientDatabase(context).containsPhoneOrUuid(address.getName())) {
|
||||
RecipientId recipientId = Recipient.external(context, address.getName()).getId();
|
||||
SessionRecord sessionRecord = DatabaseFactory.getSessionDatabase(context).load(recipientId, address.getDeviceId());
|
||||
|
||||
return sessionRecord != null &&
|
||||
sessionRecord.getSessionState().hasSenderChain() &&
|
||||
sessionRecord.getSessionState().getSessionVersion() == CiphertextMessage.CURRENT_VERSION;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteSession(SignalProtocolAddress address) {
|
||||
synchronized (FILE_LOCK) {
|
||||
if (DatabaseFactory.getRecipientDatabase(context).containsPhoneOrUuid(address.getName())) {
|
||||
RecipientId recipientId = Recipient.external(context, address.getName()).getId();
|
||||
DatabaseFactory.getSessionDatabase(context).delete(recipientId, address.getDeviceId());
|
||||
} else {
|
||||
Log.w(TAG, "Tried to delete session for " + address.toString() + ", but none existed!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteAllSessions(String name) {
|
||||
synchronized (FILE_LOCK) {
|
||||
if (DatabaseFactory.getRecipientDatabase(context).containsPhoneOrUuid(name)) {
|
||||
RecipientId recipientId = Recipient.external(context, name).getId();
|
||||
DatabaseFactory.getSessionDatabase(context).deleteAllFor(recipientId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Integer> getSubDeviceSessions(String name) {
|
||||
synchronized (FILE_LOCK) {
|
||||
if (DatabaseFactory.getRecipientDatabase(context).containsPhoneOrUuid(name)) {
|
||||
RecipientId recipientId = Recipient.external(context, name).getId();
|
||||
return DatabaseFactory.getSessionDatabase(context).getSubDevices(recipientId);
|
||||
} else {
|
||||
Log.w(TAG, "Tried to get sub device sessions for " + name + ", but none existed!");
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void archiveSiblingSessions(@NonNull SignalProtocolAddress address) {
|
||||
synchronized (FILE_LOCK) {
|
||||
if (DatabaseFactory.getRecipientDatabase(context).containsPhoneOrUuid(address.getName())) {
|
||||
RecipientId recipientId = Recipient.external(context, address.getName()).getId();
|
||||
List<SessionDatabase.SessionRow> sessions = DatabaseFactory.getSessionDatabase(context).getAllFor(recipientId);
|
||||
|
||||
for (SessionDatabase.SessionRow row : sessions) {
|
||||
if (row.getDeviceId() != address.getDeviceId()) {
|
||||
row.getRecord().archiveCurrentState();
|
||||
storeSession(new SignalProtocolAddress(Recipient.resolved(row.getRecipientId()).requireServiceId(), row.getDeviceId()), row.getRecord());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Tried to archive sibling sessions for " + address.toString() + ", but none existed!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void archiveAllSessions() {
|
||||
synchronized (FILE_LOCK) {
|
||||
List<SessionDatabase.SessionRow> sessions = DatabaseFactory.getSessionDatabase(context).getAll();
|
||||
|
||||
for (SessionDatabase.SessionRow row : sessions) {
|
||||
row.getRecord().archiveCurrentState();
|
||||
storeSession(new SignalProtocolAddress(Recipient.resolved(row.getRecipientId()).requireServiceId(), row.getDeviceId()), row.getRecord());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user