mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-05-04 07:25:25 +01:00
Reimplement ProfileCipherInputStream using libsignal-client.
libsignal-client provides an AES-GCM streaming interface that can replace the implementation in AES-GCM-Provider. Using it from ProfileCipherInputStream requires some knowledge about the tag size of AES-GCM, but frees it from the JCE interface. Note that it remains a serious error to not read the *entire* stream, since the authentication tag is at the end!
This commit is contained in:
committed by
Greyson Parrelli
parent
35e9e31a7b
commit
68a2d5ed20
@@ -1,42 +1,33 @@
|
||||
package org.whispersystems.signalservice.api.crypto;
|
||||
|
||||
|
||||
import org.signal.libsignal.crypto.Aes256GcmDecryption;
|
||||
import org.signal.zkgroup.profiles.ProfileKey;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
import org.whispersystems.signalservice.internal.util.Util;
|
||||
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.ShortBufferException;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import static org.signal.libsignal.crypto.Aes256GcmDecryption.TAG_SIZE_IN_BYTES;
|
||||
|
||||
public class ProfileCipherInputStream extends FilterInputStream {
|
||||
|
||||
private final Cipher cipher;
|
||||
private Aes256GcmDecryption aes;
|
||||
|
||||
private boolean finished = false;
|
||||
// The buffer size must match the length of the authentication tag.
|
||||
private byte[] buffer = new byte[TAG_SIZE_IN_BYTES];
|
||||
private byte[] swapBuffer = new byte[TAG_SIZE_IN_BYTES];
|
||||
|
||||
public ProfileCipherInputStream(InputStream in, ProfileKey key) throws IOException {
|
||||
super(in);
|
||||
|
||||
try {
|
||||
this.cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
|
||||
byte[] nonce = new byte[12];
|
||||
Util.readFully(in, nonce);
|
||||
Util.readFully(in, buffer);
|
||||
|
||||
this.cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key.serialize(), "AES"), new GCMParameterSpec(128, nonce));
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException e) {
|
||||
throw new AssertionError(e);
|
||||
this.aes = new Aes256GcmDecryption(key.serialize(), nonce, new byte[] {});
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
@@ -54,31 +45,47 @@ public class ProfileCipherInputStream extends FilterInputStream {
|
||||
|
||||
@Override
|
||||
public int read(byte[] output, int outputOffset, int outputLength) throws IOException {
|
||||
if (finished) return -1;
|
||||
if (aes == null) return -1;
|
||||
|
||||
try {
|
||||
byte[] ciphertext = new byte[outputLength / 2];
|
||||
int read = in.read(ciphertext, 0, ciphertext.length);
|
||||
int read = in.read(output, outputOffset, outputLength);
|
||||
|
||||
if (read == -1) {
|
||||
if (cipher.getOutputSize(0) > outputLength) {
|
||||
throw new AssertionError("Need: " + cipher.getOutputSize(0) + " but only have: " + outputLength);
|
||||
}
|
||||
|
||||
finished = true;
|
||||
return cipher.doFinal(output, outputOffset);
|
||||
} else {
|
||||
if (cipher.getOutputSize(read) > outputLength) {
|
||||
throw new AssertionError("Need: " + cipher.getOutputSize(read) + " but only have: " + outputLength);
|
||||
}
|
||||
|
||||
return cipher.update(ciphertext, 0, read, output, outputOffset);
|
||||
if (read == -1) {
|
||||
// We're done. The buffer has the final tag for authentication.
|
||||
Aes256GcmDecryption aes = this.aes;
|
||||
this.aes = null;
|
||||
if (!aes.verifyTag(this.buffer)) {
|
||||
throw new IOException("authentication of decrypted data failed");
|
||||
}
|
||||
} catch (IllegalBlockSizeException | ShortBufferException e) {
|
||||
throw new AssertionError(e);
|
||||
} catch (BadPaddingException e) {
|
||||
throw new IOException(e);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (read < TAG_SIZE_IN_BYTES) {
|
||||
// swapBuffer = buffer[read..] + output[offset..][..read]
|
||||
// output[offset..][..read] = buffer[..read]
|
||||
System.arraycopy(this.buffer, read, this.swapBuffer, 0, TAG_SIZE_IN_BYTES - read);
|
||||
System.arraycopy(output, outputOffset, this.swapBuffer, TAG_SIZE_IN_BYTES - read, read);
|
||||
System.arraycopy(this.buffer, 0, output, outputOffset, read);
|
||||
} else if (read == TAG_SIZE_IN_BYTES) {
|
||||
// swapBuffer = output[offset..][..read]
|
||||
// output[offset..][..read] = buffer
|
||||
System.arraycopy(output, outputOffset, this.swapBuffer, 0, read);
|
||||
System.arraycopy(this.buffer, 0, output, outputOffset, read);
|
||||
} else {
|
||||
// swapBuffer = output[offset..][(read - TAG_SIZE)..read]
|
||||
// output[offset..][TAG_SIZE..read] = output[offset..][..(read - TAG_SIZE)]
|
||||
// output[offset..][..TAG_SIZE] = buffer
|
||||
System.arraycopy(output, outputOffset + read - TAG_SIZE_IN_BYTES, this.swapBuffer, 0, TAG_SIZE_IN_BYTES);
|
||||
System.arraycopy(output, outputOffset, output, outputOffset + TAG_SIZE_IN_BYTES, read - TAG_SIZE_IN_BYTES);
|
||||
System.arraycopy(this.buffer, 0, output, outputOffset, TAG_SIZE_IN_BYTES);
|
||||
}
|
||||
|
||||
// Now swapBuffer has the buffer for next time.
|
||||
byte[] temp = this.buffer;
|
||||
this.buffer = this.swapBuffer;
|
||||
this.swapBuffer = temp;
|
||||
|
||||
aes.decrypt(output, outputOffset, read);
|
||||
return read;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user