package org.thoughtcrime.SMP.crypto.storage;
import android.content.Context;
import android.util.Log;
import org.thoughtcrime.SMP.crypto.MasterCipher;
import org.thoughtcrime.SMP.crypto.MasterSecret;
import org.whispersystems.libaxolotl.InvalidKeyIdException;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
import org.whispersystems.libaxolotl.state.SignedPreKeyStore;
import org.whispersystems.libaxolotl.state.PreKeyRecord;
import org.whispersystems.libaxolotl.state.PreKeyStore;
import org.thoughtcrime.SMP.util.Conversions;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.LinkedList;
import java.util.List;
public class TextSecurePreKeyStore implements PreKeyStore, SignedPreKeyStore {
public static final String PREKEY_DIRECTORY = "prekeys";
public static final String SIGNED_PREKEY_DIRECTORY = "signed_prekeys";
private static final int CURRENT_VERSION_MARKER = 1;
private static final Object FILE_LOCK = new Object();
private static final String TAG = TextSecurePreKeyStore.class.getSimpleName();
private final Context context;
private final MasterSecret masterSecret;
public TextSecurePreKeyStore(Context context, MasterSecret masterSecret) {
this.context = context;
this.masterSecret = masterSecret;
}
@Override
public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException {
synchronized (FILE_LOCK) {
try {
return new PreKeyRecord(loadSerializedRecord(getPreKeyFile(preKeyId)));
} catch (IOException | InvalidMessageException e) {
Log.w(TAG, e);
throw new InvalidKeyIdException(e);
}
}
}
@Override
public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException {
synchronized (FILE_LOCK) {
try {
return new SignedPreKeyRecord(loadSerializedRecord(getSignedPreKeyFile(signedPreKeyId)));
} catch (IOException | InvalidMessageException e) {
Log.w(TAG, e);
throw new InvalidKeyIdException(e);
}
}
}
@Override
public List<SignedPreKeyRecord> loadSignedPreKeys() {
synchronized (FILE_LOCK) {
File directory = getSignedPreKeyDirectory();
List<SignedPreKeyRecord> results = new LinkedList<>();
for (File signedPreKeyFile : directory.listFiles()) {
try {
results.add(new SignedPreKeyRecord(loadSerializedRecord(signedPreKeyFile)));
} catch (IOException | InvalidMessageException e) {
Log.w(TAG, e);
}
}
return results;
}
}
@Override
public void storePreKey(int preKeyId, PreKeyRecord record) {
synchronized (FILE_LOCK) {
try {
storeSerializedRecord(getPreKeyFile(preKeyId), record.serialize());
} catch (IOException e) {
throw new AssertionError(e);
}
}
}
@Override
public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) {
synchronized (FILE_LOCK) {
try {
storeSerializedRecord(getSignedPreKeyFile(signedPreKeyId), record.serialize());
} catch (IOException e) {
throw new AssertionError(e);
}
}
}
@Override
public boolean containsPreKey(int preKeyId) {
File record = getPreKeyFile(preKeyId);
return record.exists();
}
@Override
public boolean containsSignedPreKey(int signedPreKeyId) {
File record = getSignedPreKeyFile(signedPreKeyId);
return record.exists();
}
@Override
public void removePreKey(int preKeyId) {
File record = getPreKeyFile(preKeyId);
record.delete();
}
@Override
public void removeSignedPreKey(int signedPreKeyId) {
File record = getSignedPreKeyFile(signedPreKeyId);
record.delete();
}
private byte[] loadSerializedRecord(File recordFile)
throws IOException, InvalidMessageException
{
MasterCipher masterCipher = new MasterCipher(masterSecret);
FileInputStream fin = new FileInputStream(recordFile);
int recordVersion = readInteger(fin);
if (recordVersion != CURRENT_VERSION_MARKER) {
throw new AssertionError("Invalid version: " + recordVersion);
}
return masterCipher.decryptBytes(readBlob(fin));
}
private void storeSerializedRecord(File file, byte[] serialized) throws IOException {
MasterCipher masterCipher = new MasterCipher(masterSecret);
RandomAccessFile recordFile = new RandomAccessFile(file, "rw");
FileChannel out = recordFile.getChannel();
out.position(0);
writeInteger(CURRENT_VERSION_MARKER, out);
writeBlob(masterCipher.encryptBytes(serialized), out);
out.truncate(out.position());
recordFile.close();
}
private File getPreKeyFile(int preKeyId) {
return new File(getPreKeyDirectory(), String.valueOf(preKeyId));
}
private File getSignedPreKeyFile(int signedPreKeyId) {
return new File(getSignedPreKeyDirectory(), String.valueOf(signedPreKeyId));
}
private File getPreKeyDirectory() {
return getRecordsDirectory(PREKEY_DIRECTORY);
}
private File getSignedPreKeyDirectory() {
return getRecordsDirectory(SIGNED_PREKEY_DIRECTORY);
}
private File getRecordsDirectory(String directoryName) {
File directory = new File(context.getFilesDir(), directoryName);
if (!directory.exists()) {
if (!directory.mkdirs()) {
Log.w(TAG, "PreKey directory creation failed!");
}
}
return directory;
}
private byte[] readBlob(FileInputStream in) throws IOException {
int length = readInteger(in);
byte[] blobBytes = new byte[length];
in.read(blobBytes, 0, blobBytes.length);
return blobBytes;
}
private void writeBlob(byte[] blobBytes, FileChannel out) throws IOException {
writeInteger(blobBytes.length, out);
out.write(ByteBuffer.wrap(blobBytes));
}
private int readInteger(FileInputStream in) throws IOException {
byte[] integer = new byte[4];
in.read(integer, 0, integer.length);
return Conversions.byteArrayToInt(integer);
}
private void writeInteger(int value, FileChannel out) throws IOException {
byte[] valueBytes = Conversions.intToByteArray(value);
out.write(ByteBuffer.wrap(valueBytes));
}
}