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,9 +1,8 @@
package org.whispersystems.signalservice.api.crypto;
import junit.framework.TestCase;
import org.conscrypt.Conscrypt;
import org.junit.Test;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.signalservice.internal.util.Util;
@@ -11,14 +10,30 @@ import org.whispersystems.util.Base64;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.Security;
public class ProfileCipherTest extends TestCase {
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.whispersystems.signalservice.testutil.LibSignalLibraryUtil.assumeLibSignalSupportedOnOS;
public class ProfileCipherTest {
private class TestByteArrayInputStream extends ByteArrayInputStream {
TestByteArrayInputStream(byte[] buffer) {
super(buffer);
}
int getPos() {
return this.pos;
}
}
static {
Security.insertProviderAt(Conscrypt.newProvider(), 1);
}
@Test
public void testEncryptDecrypt() throws InvalidCiphertextException, InvalidInputException {
ProfileKey key = new ProfileKey(Util.getSecretBytes(32));
ProfileCipher cipher = new ProfileCipher(key);
@@ -27,6 +42,7 @@ public class ProfileCipherTest extends TestCase {
assertEquals(plaintext, "Clement\0Duval");
}
@Test
public void testEmpty() throws Exception {
ProfileKey key = new ProfileKey(Util.getSecretBytes(32));
ProfileCipher cipher = new ProfileCipher(key);
@@ -36,7 +52,32 @@ public class ProfileCipherTest extends TestCase {
assertEquals(plaintext.length(), 0);
}
private byte[] readStream(byte[] input, ProfileKey key, int bufferSize) throws Exception {
TestByteArrayInputStream bais = new TestByteArrayInputStream(input);
assertEquals(0, bais.getPos());
ProfileCipherInputStream in = new ProfileCipherInputStream(bais, key);
assertEquals(12 + 16, bais.getPos()); // initial read of nonce + tag-sized buffer
ByteArrayOutputStream result = new ByteArrayOutputStream();
byte[] buffer = new byte[bufferSize];
int pos = bais.getPos();
int read;
while ((read = in.read(buffer)) != -1) {
assertEquals(pos + read, bais.getPos());
pos += read;
result.write(buffer, 0, read);
}
assertEquals(pos, input.length);
return result.toByteArray();
}
@Test
public void testStreams() throws Exception {
assumeLibSignalSupportedOnOS();
ProfileKey key = new ProfileKey(Util.getSecretBytes(32));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ProfileCipherOutputStream out = new ProfileCipherOutputStream(baos, key);
@@ -45,21 +86,35 @@ public class ProfileCipherTest extends TestCase {
out.flush();
out.close();
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ProfileCipherInputStream in = new ProfileCipherInputStream(bais, key);
byte[] encrypted = baos.toByteArray();
ByteArrayOutputStream result = new ByteArrayOutputStream();
byte[] buffer = new byte[2048];
int read;
while ((read = in.read(buffer)) != -1) {
result.write(buffer, 0, read);
}
assertEquals(new String(result.toByteArray()), "This is an avatar");
assertEquals(new String(readStream(encrypted, key, 2048)), "This is an avatar");
assertEquals(new String(readStream(encrypted, key, 16 /* == block size */)), "This is an avatar");
assertEquals(new String(readStream(encrypted, key, 5)), "This is an avatar");
}
@Test
public void testStreamBadAuthentication() throws Exception {
assumeLibSignalSupportedOnOS();
ProfileKey key = new ProfileKey(Util.getSecretBytes(32));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ProfileCipherOutputStream out = new ProfileCipherOutputStream(baos, key);
out.write("This is an avatar".getBytes());
out.flush();
out.close();
byte[] encrypted = baos.toByteArray();
encrypted[encrypted.length - 1] ^= 1;
try {
readStream(encrypted, key, 2048);
fail("failed to verify authenticate tag");
} catch (IOException e) {
}
}
@Test
public void testEncryptLengthBucket1() throws InvalidInputException {
ProfileKey key = new ProfileKey(Util.getSecretBytes(32));
ProfileCipher cipher = new ProfileCipher(key);
@@ -70,6 +125,7 @@ public class ProfileCipherTest extends TestCase {
assertEquals(108, encoded.length());
}
@Test
public void testEncryptLengthBucket2() throws InvalidInputException {
ProfileKey key = new ProfileKey(Util.getSecretBytes(32));
ProfileCipher cipher = new ProfileCipher(key);
@@ -80,6 +136,7 @@ public class ProfileCipherTest extends TestCase {
assertEquals(380, encoded.length());
}
@Test
public void testTargetNameLength() {
assertEquals(53, ProfileCipher.getTargetNameLength("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"));
assertEquals(53, ProfileCipher.getTargetNameLength("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1"));