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:
Jordan Rose
2021-06-23 12:59:17 -07:00
committed by Greyson Parrelli
parent 35e9e31a7b
commit 68a2d5ed20
2 changed files with 118 additions and 54 deletions

View File

@@ -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;
}
}