mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-25 11:20:47 +01:00
Move common crypto classes into TextSecureLibrary.
1) Move all the crypto classes from securesms.crypto. 2) Move all the crypto storage from securesms.database.keys 3) Replace the old imported BC code with spongycastle.
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
package org.whispersystems.textsecure.storage;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
public interface CanonicalRecipientAddress {
|
||||
public long getCanonicalAddress(Context context);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecure.storage;
|
||||
|
||||
public class InvalidKeyIdException extends Exception {
|
||||
|
||||
public InvalidKeyIdException() {
|
||||
}
|
||||
|
||||
public InvalidKeyIdException(String detailMessage) {
|
||||
super(detailMessage);
|
||||
}
|
||||
|
||||
public InvalidKeyIdException(Throwable throwable) {
|
||||
super(throwable);
|
||||
}
|
||||
|
||||
public InvalidKeyIdException(String detailMessage, Throwable throwable) {
|
||||
super(detailMessage, throwable);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecure.storage;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.InvalidKeyException;
|
||||
import org.whispersystems.textsecure.crypto.KeyPair;
|
||||
import org.whispersystems.textsecure.crypto.KeyUtil;
|
||||
import org.whispersystems.textsecure.crypto.MasterCipher;
|
||||
import org.whispersystems.textsecure.crypto.MasterSecret;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.channels.FileChannel;
|
||||
|
||||
public class LocalKeyRecord extends Record {
|
||||
|
||||
private static final Object FILE_LOCK = new Object();
|
||||
|
||||
private KeyPair localCurrentKeyPair;
|
||||
private KeyPair localNextKeyPair;
|
||||
|
||||
private final MasterCipher masterCipher;
|
||||
private final MasterSecret masterSecret;
|
||||
|
||||
public LocalKeyRecord(Context context, MasterSecret masterSecret, CanonicalRecipientAddress recipient) {
|
||||
super(context, SESSIONS_DIRECTORY, getFileNameForRecipient(context, recipient));
|
||||
this.masterSecret = masterSecret;
|
||||
this.masterCipher = new MasterCipher(masterSecret);
|
||||
loadData();
|
||||
}
|
||||
|
||||
public static boolean hasRecord(Context context, CanonicalRecipientAddress recipient) {
|
||||
Log.w("LocalKeyRecord", "Checking: " + getFileNameForRecipient(context, recipient));
|
||||
return Record.hasRecord(context, SESSIONS_DIRECTORY, getFileNameForRecipient(context, recipient));
|
||||
}
|
||||
|
||||
public static void delete(Context context, CanonicalRecipientAddress recipient) {
|
||||
Record.delete(context, SESSIONS_DIRECTORY, getFileNameForRecipient(context, recipient));
|
||||
}
|
||||
|
||||
private static String getFileNameForRecipient(Context context, CanonicalRecipientAddress recipient) {
|
||||
return recipient.getCanonicalAddress(context) + "-local";
|
||||
}
|
||||
|
||||
public void advanceKeyIfNecessary(int keyId) {
|
||||
Log.w("LocalKeyRecord", "Remote client acknowledges receiving key id: " + keyId);
|
||||
if (keyId == localNextKeyPair.getId()) {
|
||||
this.localCurrentKeyPair = this.localNextKeyPair;
|
||||
this.localNextKeyPair = new KeyPair(this.localNextKeyPair.getId()+1, KeyUtil.generateKeyPair(), masterSecret);
|
||||
}
|
||||
}
|
||||
|
||||
public void setCurrentKeyPair(KeyPair localCurrentKeyPair) {
|
||||
this.localCurrentKeyPair = localCurrentKeyPair;
|
||||
}
|
||||
|
||||
public void setNextKeyPair(KeyPair localNextKeyPair) {
|
||||
this.localNextKeyPair = localNextKeyPair;
|
||||
}
|
||||
|
||||
public KeyPair getCurrentKeyPair() {
|
||||
return this.localCurrentKeyPair;
|
||||
}
|
||||
|
||||
public KeyPair getNextKeyPair() {
|
||||
return this.localNextKeyPair;
|
||||
}
|
||||
|
||||
public KeyPair getKeyPairForId(int id) throws InvalidKeyIdException {
|
||||
if (this.localCurrentKeyPair.getId() == id) return this.localCurrentKeyPair;
|
||||
else if (this.localNextKeyPair.getId() == id) return this.localNextKeyPair;
|
||||
else throw new InvalidKeyIdException("No local key for ID: " + id);
|
||||
}
|
||||
|
||||
public void save() {
|
||||
synchronized (FILE_LOCK) {
|
||||
try {
|
||||
RandomAccessFile file = openRandomAccessFile();
|
||||
FileChannel out = file.getChannel();
|
||||
out.position(0);
|
||||
|
||||
writeKeyPair(localCurrentKeyPair, out);
|
||||
writeKeyPair(localNextKeyPair, out);
|
||||
|
||||
out.force(true);
|
||||
out.truncate(out.position());
|
||||
out.close();
|
||||
file.close();
|
||||
} catch (IOException ioe) {
|
||||
Log.w("keyrecord", ioe);
|
||||
// XXX
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void loadData() {
|
||||
Log.w("LocalKeyRecord", "Loading local key record...");
|
||||
synchronized (FILE_LOCK) {
|
||||
try {
|
||||
FileInputStream in = this.openInputStream();
|
||||
localCurrentKeyPair = readKeyPair(in, masterCipher);
|
||||
localNextKeyPair = readKeyPair(in, masterCipher);
|
||||
in.close();
|
||||
} catch (FileNotFoundException e) {
|
||||
Log.w("LocalKeyRecord", "No local keypair set found.");
|
||||
} catch (IOException ioe) {
|
||||
Log.w("keyrecord", ioe);
|
||||
// XXX
|
||||
} catch (InvalidKeyException ike) {
|
||||
Log.w("LocalKeyRecord", ike);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void writeKeyPair(KeyPair keyPair, FileChannel out) throws IOException {
|
||||
byte[] keyPairBytes = keyPair.toBytes();
|
||||
writeBlob(keyPairBytes, out);
|
||||
}
|
||||
|
||||
private KeyPair readKeyPair(FileInputStream in, MasterCipher masterCipher)
|
||||
throws IOException, InvalidKeyException
|
||||
{
|
||||
byte[] keyPairBytes = readBlob(in);
|
||||
return new KeyPair(keyPairBytes, masterCipher);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package org.whispersystems.textsecure.storage;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.InvalidKeyException;
|
||||
import org.whispersystems.textsecure.crypto.MasterSecret;
|
||||
import org.whispersystems.textsecure.crypto.PreKeyPair;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.channels.FileChannel;
|
||||
|
||||
public class PreKeyRecord extends Record {
|
||||
|
||||
private static final Object FILE_LOCK = new Object();
|
||||
private static final int CURRENT_VERSION_MARKER = 1;
|
||||
|
||||
private final MasterSecret masterSecret;
|
||||
|
||||
private PreKeyPair keyPair;
|
||||
private long id;
|
||||
|
||||
public PreKeyRecord(Context context, MasterSecret masterSecret, long id)
|
||||
throws InvalidKeyIdException
|
||||
{
|
||||
super(context, PREKEY_DIRECTORY, id+"");
|
||||
|
||||
this.id = id;
|
||||
this.masterSecret = masterSecret;
|
||||
|
||||
loadData();
|
||||
}
|
||||
|
||||
public PreKeyRecord(Context context, MasterSecret masterSecret,
|
||||
long id, PreKeyPair keyPair)
|
||||
{
|
||||
super(context, PREKEY_DIRECTORY, id+"");
|
||||
this.id = id;
|
||||
this.keyPair = keyPair;
|
||||
this.masterSecret = masterSecret;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public PreKeyPair getKeyPair() {
|
||||
return keyPair;
|
||||
}
|
||||
|
||||
public static boolean hasRecord(Context context, long id) {
|
||||
Log.w("PreKeyRecord", "Checking: " + id);
|
||||
return Record.hasRecord(context, PREKEY_DIRECTORY, id+"");
|
||||
}
|
||||
|
||||
public static void delete(Context context, long id) {
|
||||
Record.delete(context, PREKEY_DIRECTORY, id+"");
|
||||
}
|
||||
|
||||
public void save() {
|
||||
synchronized (FILE_LOCK) {
|
||||
try {
|
||||
RandomAccessFile file = openRandomAccessFile();
|
||||
FileChannel out = file.getChannel();
|
||||
out.position(0);
|
||||
|
||||
writeInteger(CURRENT_VERSION_MARKER, out);
|
||||
writeKeyPair(keyPair, out);
|
||||
|
||||
out.force(true);
|
||||
out.truncate(out.position());
|
||||
out.close();
|
||||
file.close();
|
||||
} catch (IOException ioe) {
|
||||
Log.w("PreKeyRecord", ioe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void loadData() throws InvalidKeyIdException {
|
||||
synchronized (FILE_LOCK) {
|
||||
try {
|
||||
FileInputStream in = this.openInputStream();
|
||||
int recordVersion = readInteger(in);
|
||||
|
||||
if (recordVersion != CURRENT_VERSION_MARKER) {
|
||||
Log.w("PreKeyRecord", "Invalid version: " + recordVersion);
|
||||
return;
|
||||
}
|
||||
|
||||
keyPair = readKeyPair(in);
|
||||
in.close();
|
||||
} catch (FileNotFoundException e) {
|
||||
Log.w("PreKeyRecord", e);
|
||||
throw new InvalidKeyIdException(e);
|
||||
} catch (IOException ioe) {
|
||||
Log.w("PreKeyRecord", ioe);
|
||||
throw new InvalidKeyIdException(ioe);
|
||||
} catch (InvalidKeyException ike) {
|
||||
Log.w("LocalKeyRecord", ike);
|
||||
throw new InvalidKeyIdException(ike);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void writeKeyPair(PreKeyPair keyPair, FileChannel out) throws IOException {
|
||||
byte[] serialized = keyPair.serialize();
|
||||
writeBlob(serialized, out);
|
||||
}
|
||||
|
||||
private PreKeyPair readKeyPair(FileInputStream in)
|
||||
throws IOException, InvalidKeyException
|
||||
{
|
||||
byte[] keyPairBytes = readBlob(in);
|
||||
return new PreKeyPair(masterSecret, keyPairBytes);
|
||||
}
|
||||
|
||||
}
|
||||
100
library/src/org/whispersystems/textsecure/storage/Record.java
Normal file
100
library/src/org/whispersystems/textsecure/storage/Record.java
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecure.storage;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.whispersystems.textsecure.util.Conversions;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
|
||||
public abstract class Record {
|
||||
|
||||
protected static final String SESSIONS_DIRECTORY = "sessions";
|
||||
public static final String PREKEY_DIRECTORY = "prekeys";
|
||||
|
||||
protected final String address;
|
||||
protected final String directory;
|
||||
protected final Context context;
|
||||
|
||||
public Record(Context context, String directory, String address) {
|
||||
this.context = context;
|
||||
this.directory = directory;
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
public void delete() {
|
||||
delete(this.context, this.directory, this.address);
|
||||
}
|
||||
|
||||
protected static void delete(Context context, String directory, String address) {
|
||||
getAddressFile(context, directory, address).delete();
|
||||
}
|
||||
|
||||
protected static boolean hasRecord(Context context, String directory, String address) {
|
||||
return getAddressFile(context, directory, address).exists();
|
||||
}
|
||||
|
||||
protected RandomAccessFile openRandomAccessFile() throws FileNotFoundException {
|
||||
return new RandomAccessFile(getAddressFile(), "rw");
|
||||
}
|
||||
|
||||
protected FileInputStream openInputStream() throws FileNotFoundException {
|
||||
return new FileInputStream(getAddressFile().getAbsolutePath());
|
||||
}
|
||||
|
||||
private File getAddressFile() {
|
||||
return getAddressFile(context, directory, address);
|
||||
}
|
||||
|
||||
private static File getAddressFile(Context context, String directory, String address) {
|
||||
return new File(context.getFilesDir().getAbsolutePath() + File.separatorChar + directory, address);
|
||||
}
|
||||
|
||||
protected byte[] readBlob(FileInputStream in) throws IOException {
|
||||
int length = readInteger(in);
|
||||
byte[] blobBytes = new byte[length];
|
||||
|
||||
in.read(blobBytes, 0, blobBytes.length);
|
||||
return blobBytes;
|
||||
}
|
||||
|
||||
protected void writeBlob(byte[] blobBytes, FileChannel out) throws IOException {
|
||||
writeInteger(blobBytes.length, out);
|
||||
ByteBuffer buffer = ByteBuffer.wrap(blobBytes);
|
||||
out.write(buffer);
|
||||
}
|
||||
|
||||
protected int readInteger(FileInputStream in) throws IOException {
|
||||
byte[] integer = new byte[4];
|
||||
in.read(integer, 0, integer.length);
|
||||
return Conversions.byteArrayToInt(integer);
|
||||
}
|
||||
|
||||
protected void writeInteger(int value, FileChannel out) throws IOException {
|
||||
byte[] valueBytes = Conversions.intToByteArray(value);
|
||||
ByteBuffer buffer = ByteBuffer.wrap(valueBytes);
|
||||
out.write(buffer);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecure.storage;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.InvalidKeyException;
|
||||
import org.whispersystems.textsecure.crypto.PublicKey;
|
||||
import org.whispersystems.textsecure.util.Hex;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.channels.FileChannel;
|
||||
|
||||
/**
|
||||
* Represents the current and last public key belonging to the "remote"
|
||||
* endpoint in an encrypted session. These are stored on disk.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class RemoteKeyRecord extends Record {
|
||||
private static final Object FILE_LOCK = new Object();
|
||||
|
||||
private PublicKey remoteKeyCurrent;
|
||||
private PublicKey remoteKeyLast;
|
||||
|
||||
public RemoteKeyRecord(Context context, CanonicalRecipientAddress recipient) {
|
||||
super(context, SESSIONS_DIRECTORY, getFileNameForRecipient(context, recipient));
|
||||
loadData();
|
||||
}
|
||||
|
||||
public static void delete(Context context, CanonicalRecipientAddress recipient) {
|
||||
delete(context, SESSIONS_DIRECTORY, getFileNameForRecipient(context, recipient));
|
||||
}
|
||||
|
||||
public static boolean hasRecord(Context context, CanonicalRecipientAddress recipient) {
|
||||
Log.w("LocalKeyRecord", "Checking: " + getFileNameForRecipient(context, recipient));
|
||||
return hasRecord(context, SESSIONS_DIRECTORY, getFileNameForRecipient(context, recipient));
|
||||
}
|
||||
|
||||
private static String getFileNameForRecipient(Context context, CanonicalRecipientAddress recipient) {
|
||||
return recipient.getCanonicalAddress(context) + "-remote";
|
||||
}
|
||||
|
||||
public void updateCurrentRemoteKey(PublicKey remoteKey) {
|
||||
Log.w("RemoteKeyRecord", "Updating current remote key: " + remoteKey.getId());
|
||||
if (remoteKey.getId() > remoteKeyCurrent.getId()) {
|
||||
this.remoteKeyLast = this.remoteKeyCurrent;
|
||||
this.remoteKeyCurrent = remoteKey;
|
||||
}
|
||||
}
|
||||
|
||||
public void setCurrentRemoteKey(PublicKey remoteKeyCurrent) {
|
||||
this.remoteKeyCurrent = remoteKeyCurrent;
|
||||
}
|
||||
|
||||
public void setLastRemoteKey(PublicKey remoteKeyLast) {
|
||||
this.remoteKeyLast = remoteKeyLast;
|
||||
}
|
||||
|
||||
public PublicKey getCurrentRemoteKey() {
|
||||
return this.remoteKeyCurrent;
|
||||
}
|
||||
|
||||
public PublicKey getLastRemoteKey() {
|
||||
return this.remoteKeyLast;
|
||||
}
|
||||
|
||||
public PublicKey getKeyForId(int id) throws InvalidKeyIdException {
|
||||
if (this.remoteKeyCurrent.getId() == id) return this.remoteKeyCurrent;
|
||||
else if (this.remoteKeyLast.getId() == id) return this.remoteKeyLast;
|
||||
else throw new InvalidKeyIdException("No remote key for ID: " + id);
|
||||
}
|
||||
|
||||
public void save() {
|
||||
Log.w("RemoteKeyRecord", "Saving remote key record for recipient: " + this.address);
|
||||
synchronized (FILE_LOCK) {
|
||||
try {
|
||||
RandomAccessFile file = openRandomAccessFile();
|
||||
FileChannel out = file.getChannel();
|
||||
Log.w("RemoteKeyRecord", "Opened file of size: " + out.size());
|
||||
out.position(0);
|
||||
|
||||
writeKey(remoteKeyCurrent, out);
|
||||
writeKey(remoteKeyLast, out);
|
||||
|
||||
out.truncate(out.position());
|
||||
out.close();
|
||||
file.close();
|
||||
} catch (IOException ioe) {
|
||||
Log.w("keyrecord", ioe);
|
||||
// XXX
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void loadData() {
|
||||
Log.w("RemoteKeyRecord", "Loading remote key record for recipient: " + this.address);
|
||||
synchronized (FILE_LOCK) {
|
||||
try {
|
||||
FileInputStream in = this.openInputStream();
|
||||
remoteKeyCurrent = readKey(in);
|
||||
remoteKeyLast = readKey(in);
|
||||
in.close();
|
||||
} catch (FileNotFoundException e) {
|
||||
Log.w("RemoteKeyRecord", "No remote keys found.");
|
||||
} catch (IOException ioe) {
|
||||
Log.w("keyrecord", ioe);
|
||||
// XXX
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void writeKey(PublicKey key, FileChannel out) throws IOException {
|
||||
byte[] keyBytes = key.serialize();
|
||||
Log.w("RemoteKeyRecord", "Serializing remote key bytes: " + Hex.toString(keyBytes));
|
||||
writeBlob(keyBytes, out);
|
||||
}
|
||||
|
||||
private PublicKey readKey(FileInputStream in) throws IOException {
|
||||
try {
|
||||
byte[] keyBytes = readBlob(in);
|
||||
return new PublicKey(keyBytes);
|
||||
} catch (InvalidKeyException ike) {
|
||||
throw new AssertionError(ike);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecure.storage;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.MasterCipher;
|
||||
import org.whispersystems.textsecure.crypto.MasterSecret;
|
||||
import org.whispersystems.textsecure.crypto.SessionCipher;
|
||||
import org.whispersystems.textsecure.util.Conversions;
|
||||
import org.whispersystems.textsecure.util.Util;
|
||||
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
/**
|
||||
* Represents the currently negotiated session key for a given
|
||||
* local key id and remote key id. This is stored encrypted on
|
||||
* disk.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class SessionKey {
|
||||
|
||||
private int localKeyId;
|
||||
private int remoteKeyId;
|
||||
private SecretKeySpec cipherKey;
|
||||
private SecretKeySpec macKey;
|
||||
private MasterCipher masterCipher;
|
||||
|
||||
public SessionKey(int localKeyId, int remoteKeyId, SecretKeySpec cipherKey, SecretKeySpec macKey, MasterSecret masterSecret) {
|
||||
this.localKeyId = localKeyId;
|
||||
this.remoteKeyId = remoteKeyId;
|
||||
this.cipherKey = cipherKey;
|
||||
this.macKey = macKey;
|
||||
this.masterCipher = new MasterCipher(masterSecret);
|
||||
}
|
||||
|
||||
public SessionKey(byte[] bytes, MasterSecret masterSecret) {
|
||||
this.masterCipher = new MasterCipher(masterSecret);
|
||||
deserialize(bytes);
|
||||
}
|
||||
|
||||
public byte[] serialize() {
|
||||
byte[] localKeyIdBytes = Conversions.mediumToByteArray(localKeyId);
|
||||
byte[] remoteKeyIdBytes = Conversions.mediumToByteArray(remoteKeyId);
|
||||
byte[] cipherKeyBytes = cipherKey.getEncoded();
|
||||
byte[] macKeyBytes = macKey.getEncoded();
|
||||
byte[] combined = Util.combine(localKeyIdBytes, remoteKeyIdBytes, cipherKeyBytes, macKeyBytes);
|
||||
|
||||
return masterCipher.encryptBytes(combined);
|
||||
}
|
||||
|
||||
private void deserialize(byte[] bytes) {
|
||||
byte[] decrypted = masterCipher.encryptBytes(bytes);
|
||||
this.localKeyId = Conversions.byteArrayToMedium(decrypted, 0);
|
||||
this.remoteKeyId = Conversions.byteArrayToMedium(decrypted, 3);
|
||||
|
||||
byte[] keyBytes = new byte[SessionCipher.CIPHER_KEY_LENGTH];
|
||||
System.arraycopy(decrypted, 6, keyBytes, 0, keyBytes.length);
|
||||
|
||||
byte[] macBytes = new byte[SessionCipher.MAC_KEY_LENGTH];
|
||||
System.arraycopy(decrypted, 6 + keyBytes.length, macBytes, 0, macBytes.length);
|
||||
|
||||
this.cipherKey = new SecretKeySpec(keyBytes, "AES");
|
||||
this.macKey = new SecretKeySpec(macBytes, "HmacSHA1");
|
||||
}
|
||||
|
||||
public int getLocalKeyId() {
|
||||
return this.localKeyId;
|
||||
}
|
||||
|
||||
public int getRemoteKeyId() {
|
||||
return this.remoteKeyId;
|
||||
}
|
||||
|
||||
public SecretKeySpec getCipherKey() {
|
||||
return this.cipherKey;
|
||||
}
|
||||
|
||||
public SecretKeySpec getMacKey() {
|
||||
return this.macKey;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
/**
|
||||
* Copyright (C) 2011 Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.whispersystems.textsecure.storage;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import org.whispersystems.textsecure.crypto.IdentityKey;
|
||||
import org.whispersystems.textsecure.crypto.InvalidKeyException;
|
||||
import org.whispersystems.textsecure.crypto.MasterSecret;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.channels.FileChannel;
|
||||
|
||||
/**
|
||||
* A disk record representing a current session.
|
||||
*
|
||||
* @author Moxie Marlinspike
|
||||
*/
|
||||
|
||||
public class SessionRecord extends Record {
|
||||
private static final int CURRENT_VERSION_MARKER = 0X55555556;
|
||||
private static final int[] VALID_VERSION_MARKERS = {CURRENT_VERSION_MARKER, 0X55555555};
|
||||
private static final Object FILE_LOCK = new Object();
|
||||
|
||||
private int counter;
|
||||
private byte[] localFingerprint;
|
||||
private byte[] remoteFingerprint;
|
||||
private int sessionVersion;
|
||||
|
||||
private IdentityKey identityKey;
|
||||
private SessionKey sessionKeyRecord;
|
||||
private boolean verifiedSessionKey;
|
||||
|
||||
private final MasterSecret masterSecret;
|
||||
|
||||
public SessionRecord(Context context, MasterSecret masterSecret, CanonicalRecipientAddress recipient) {
|
||||
this(context, masterSecret, getRecipientId(context, recipient));
|
||||
}
|
||||
|
||||
public SessionRecord(Context context, MasterSecret masterSecret, long recipientId) {
|
||||
super(context, SESSIONS_DIRECTORY, recipientId+"");
|
||||
this.masterSecret = masterSecret;
|
||||
this.sessionVersion = 31337;
|
||||
loadData();
|
||||
}
|
||||
|
||||
public static void delete(Context context, CanonicalRecipientAddress recipient) {
|
||||
delete(context, SESSIONS_DIRECTORY, getRecipientId(context, recipient)+"");
|
||||
}
|
||||
|
||||
public static boolean hasSession(Context context, CanonicalRecipientAddress recipient) {
|
||||
Log.w("LocalKeyRecord", "Checking: " + getRecipientId(context, recipient));
|
||||
return hasRecord(context, SESSIONS_DIRECTORY, getRecipientId(context, recipient)+"");
|
||||
}
|
||||
|
||||
private static long getRecipientId(Context context, CanonicalRecipientAddress recipient) {
|
||||
return recipient.getCanonicalAddress(context);
|
||||
}
|
||||
|
||||
public void setSessionKey(SessionKey sessionKeyRecord) {
|
||||
this.sessionKeyRecord = sessionKeyRecord;
|
||||
}
|
||||
|
||||
public void setSessionId(byte[] localFingerprint, byte[] remoteFingerprint) {
|
||||
this.localFingerprint = localFingerprint;
|
||||
this.remoteFingerprint = remoteFingerprint;
|
||||
}
|
||||
|
||||
public void setIdentityKey(IdentityKey identityKey) {
|
||||
this.identityKey = identityKey;
|
||||
}
|
||||
|
||||
public int getSessionVersion() {
|
||||
return (sessionVersion == 31337 ? 0 : sessionVersion);
|
||||
}
|
||||
|
||||
public void setSessionVersion(int sessionVersion) {
|
||||
this.sessionVersion = sessionVersion;
|
||||
}
|
||||
|
||||
public int getCounter() {
|
||||
return this.counter;
|
||||
}
|
||||
|
||||
public void incrementCounter() {
|
||||
this.counter++;
|
||||
}
|
||||
|
||||
public byte[] getLocalFingerprint() {
|
||||
return this.localFingerprint;
|
||||
}
|
||||
|
||||
public byte[] getRemoteFingerprint() {
|
||||
return this.remoteFingerprint;
|
||||
}
|
||||
|
||||
public IdentityKey getIdentityKey() {
|
||||
return this.identityKey;
|
||||
}
|
||||
|
||||
// public void setVerifiedSessionKey(boolean verifiedSessionKey) {
|
||||
// this.verifiedSessionKey = verifiedSessionKey;
|
||||
// }
|
||||
|
||||
public boolean isVerifiedSession() {
|
||||
return this.verifiedSessionKey;
|
||||
}
|
||||
|
||||
private void writeIdentityKey(FileChannel out) throws IOException {
|
||||
if (identityKey == null) writeBlob(new byte[0], out);
|
||||
else writeBlob(identityKey.serialize(), out);
|
||||
}
|
||||
|
||||
private boolean isValidVersionMarker(int versionMarker) {
|
||||
for (int VALID_VERSION_MARKER : VALID_VERSION_MARKERS)
|
||||
if (versionMarker == VALID_VERSION_MARKER)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void readIdentityKey(FileInputStream in) throws IOException {
|
||||
try {
|
||||
byte[] blob = readBlob(in);
|
||||
|
||||
if (blob.length == 0) this.identityKey = null;
|
||||
else this.identityKey = new IdentityKey(blob, 0);
|
||||
} catch (InvalidKeyException ike) {
|
||||
throw new AssertionError(ike);
|
||||
}
|
||||
}
|
||||
|
||||
public void save() {
|
||||
synchronized (FILE_LOCK) {
|
||||
try {
|
||||
RandomAccessFile file = openRandomAccessFile();
|
||||
FileChannel out = file.getChannel();
|
||||
out.position(0);
|
||||
|
||||
writeInteger(CURRENT_VERSION_MARKER, out);
|
||||
writeInteger(counter, out);
|
||||
writeBlob(localFingerprint, out);
|
||||
writeBlob(remoteFingerprint, out);
|
||||
writeInteger(sessionVersion, out);
|
||||
writeIdentityKey(out);
|
||||
writeInteger(verifiedSessionKey ? 1 : 0, out);
|
||||
|
||||
if (sessionKeyRecord != null)
|
||||
writeBlob(sessionKeyRecord.serialize(), out);
|
||||
|
||||
out.truncate(out.position());
|
||||
file.close();
|
||||
} catch (IOException ioe) {
|
||||
throw new IllegalArgumentException(ioe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void loadData() {
|
||||
synchronized (FILE_LOCK) {
|
||||
try {
|
||||
FileInputStream in = this.openInputStream();
|
||||
int versionMarker = readInteger(in);
|
||||
|
||||
// Sigh, always put a version number on everything.
|
||||
if (!isValidVersionMarker(versionMarker)) {
|
||||
this.counter = versionMarker;
|
||||
this.localFingerprint = readBlob(in);
|
||||
this.remoteFingerprint = readBlob(in);
|
||||
this.sessionVersion = 31337;
|
||||
|
||||
if (in.available() != 0)
|
||||
this.sessionKeyRecord = new SessionKey(readBlob(in), masterSecret);
|
||||
|
||||
in.close();
|
||||
} else {
|
||||
this.counter = readInteger(in);
|
||||
this.localFingerprint = readBlob(in);
|
||||
this.remoteFingerprint = readBlob(in);
|
||||
this.sessionVersion = readInteger(in);
|
||||
|
||||
if (versionMarker >= 0X55555556) {
|
||||
readIdentityKey(in);
|
||||
this.verifiedSessionKey = (readInteger(in) == 1);
|
||||
}
|
||||
|
||||
if (in.available() != 0)
|
||||
this.sessionKeyRecord = new SessionKey(readBlob(in), masterSecret);
|
||||
|
||||
in.close();
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
Log.w("SessionRecord", "No session information found.");
|
||||
// XXX
|
||||
} catch (IOException ioe) {
|
||||
Log.w("keyrecord", ioe);
|
||||
// XXX
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public SessionKey getSessionKey(int localKeyId, int remoteKeyId) {
|
||||
if (this.sessionKeyRecord == null) return null;
|
||||
|
||||
if ((this.sessionKeyRecord.getLocalKeyId() == localKeyId) &&
|
||||
(this.sessionKeyRecord.getRemoteKeyId() == remoteKeyId))
|
||||
return this.sessionKeyRecord;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user