kopia lustrzana https://github.com/ryukoposting/Signal-Android
162 wiersze
5.0 KiB
Java
162 wiersze
5.0 KiB
Java
package org.thoughtcrime.securesms.backup;
|
|
|
|
import androidx.annotation.NonNull;
|
|
|
|
import org.signal.core.util.Conversions;
|
|
import org.signal.core.util.StreamUtil;
|
|
import org.signal.libsignal.protocol.kdf.HKDF;
|
|
import org.signal.libsignal.protocol.util.ByteUtil;
|
|
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.OutputStream;
|
|
import java.security.InvalidAlgorithmParameterException;
|
|
import java.security.InvalidKeyException;
|
|
import java.security.MessageDigest;
|
|
import java.security.NoSuchAlgorithmException;
|
|
|
|
import javax.crypto.BadPaddingException;
|
|
import javax.crypto.Cipher;
|
|
import javax.crypto.IllegalBlockSizeException;
|
|
import javax.crypto.Mac;
|
|
import javax.crypto.NoSuchPaddingException;
|
|
import javax.crypto.spec.IvParameterSpec;
|
|
import javax.crypto.spec.SecretKeySpec;
|
|
|
|
class BackupRecordInputStream extends FullBackupBase.BackupStream {
|
|
|
|
private final InputStream in;
|
|
private final Cipher cipher;
|
|
private final Mac mac;
|
|
|
|
private final byte[] cipherKey;
|
|
|
|
private final byte[] iv;
|
|
private int counter;
|
|
|
|
BackupRecordInputStream(@NonNull InputStream in, @NonNull String passphrase) throws IOException {
|
|
try {
|
|
this.in = in;
|
|
|
|
byte[] headerLengthBytes = new byte[4];
|
|
StreamUtil.readFully(in, headerLengthBytes);
|
|
|
|
int headerLength = Conversions.byteArrayToInt(headerLengthBytes);
|
|
byte[] headerFrame = new byte[headerLength];
|
|
StreamUtil.readFully(in, headerFrame);
|
|
|
|
BackupProtos.BackupFrame frame = BackupProtos.BackupFrame.parseFrom(headerFrame);
|
|
|
|
if (!frame.hasHeader()) {
|
|
throw new IOException("Backup stream does not start with header!");
|
|
}
|
|
|
|
BackupProtos.Header header = frame.getHeader();
|
|
|
|
this.iv = header.getIv().toByteArray();
|
|
|
|
if (iv.length != 16) {
|
|
throw new IOException("Invalid IV length!");
|
|
}
|
|
|
|
byte[] key = getBackupKey(passphrase, header.hasSalt() ? header.getSalt().toByteArray() : null);
|
|
byte[] derived = HKDF.deriveSecrets(key, "Backup Export".getBytes(), 64);
|
|
byte[][] split = ByteUtil.split(derived, 32, 32);
|
|
|
|
this.cipherKey = split[0];
|
|
byte[] macKey = split[1];
|
|
|
|
this.cipher = Cipher.getInstance("AES/CTR/NoPadding");
|
|
this.mac = Mac.getInstance("HmacSHA256");
|
|
this.mac.init(new SecretKeySpec(macKey, "HmacSHA256"));
|
|
|
|
this.counter = Conversions.byteArrayToInt(iv);
|
|
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e) {
|
|
throw new AssertionError(e);
|
|
}
|
|
}
|
|
|
|
BackupProtos.BackupFrame readFrame() throws IOException {
|
|
return readFrame(in);
|
|
}
|
|
|
|
void readAttachmentTo(OutputStream out, int length) throws IOException {
|
|
try {
|
|
Conversions.intToByteArray(iv, 0, counter++);
|
|
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(cipherKey, "AES"), new IvParameterSpec(iv));
|
|
mac.update(iv);
|
|
|
|
byte[] buffer = new byte[8192];
|
|
|
|
while (length > 0) {
|
|
int read = in.read(buffer, 0, Math.min(buffer.length, length));
|
|
if (read == -1) throw new IOException("File ended early!");
|
|
|
|
mac.update(buffer, 0, read);
|
|
|
|
byte[] plaintext = cipher.update(buffer, 0, read);
|
|
|
|
if (plaintext != null) {
|
|
out.write(plaintext, 0, plaintext.length);
|
|
}
|
|
|
|
length -= read;
|
|
}
|
|
|
|
byte[] plaintext = cipher.doFinal();
|
|
|
|
if (plaintext != null) {
|
|
out.write(plaintext, 0, plaintext.length);
|
|
}
|
|
|
|
out.close();
|
|
|
|
byte[] ourMac = ByteUtil.trim(mac.doFinal(), 10);
|
|
byte[] theirMac = new byte[10];
|
|
|
|
try {
|
|
StreamUtil.readFully(in, theirMac);
|
|
} catch (IOException e) {
|
|
throw new IOException(e);
|
|
}
|
|
|
|
if (!MessageDigest.isEqual(ourMac, theirMac)) {
|
|
throw new BadMacException();
|
|
}
|
|
} catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
|
|
throw new AssertionError(e);
|
|
}
|
|
}
|
|
|
|
private BackupProtos.BackupFrame readFrame(InputStream in) throws IOException {
|
|
try {
|
|
byte[] length = new byte[4];
|
|
StreamUtil.readFully(in, length);
|
|
|
|
byte[] frame = new byte[Conversions.byteArrayToInt(length)];
|
|
StreamUtil.readFully(in, frame);
|
|
|
|
byte[] theirMac = new byte[10];
|
|
System.arraycopy(frame, frame.length - 10, theirMac, 0, theirMac.length);
|
|
|
|
mac.update(frame, 0, frame.length - 10);
|
|
byte[] ourMac = ByteUtil.trim(mac.doFinal(), 10);
|
|
|
|
if (!MessageDigest.isEqual(ourMac, theirMac)) {
|
|
throw new IOException("Bad MAC");
|
|
}
|
|
|
|
Conversions.intToByteArray(iv, 0, counter++);
|
|
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(cipherKey, "AES"), new IvParameterSpec(iv));
|
|
|
|
byte[] plaintext = cipher.doFinal(frame, 0, frame.length - 10);
|
|
|
|
return BackupProtos.BackupFrame.parseFrom(plaintext);
|
|
} catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
|
|
throw new AssertionError(e);
|
|
}
|
|
}
|
|
|
|
static class BadMacException extends IOException {}
|
|
}
|