Skip to content

Commit

Permalink
Implement incremental MAC
Browse files Browse the repository at this point in the history
  • Loading branch information
moiseev-signal authored May 9, 2023
1 parent 0e74a41 commit 2b46ae1
Show file tree
Hide file tree
Showing 20 changed files with 1,259 additions and 1 deletion.
5 changes: 5 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//

package org.signal.libsignal.protocol.incrementalmac;

import junit.framework.TestCase;
import org.signal.libsignal.protocol.util.Hex;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;

public class IncrementalStreamsTest extends TestCase {
private static final byte[] TEST_HMAC_KEY = Hex.fromStringCondensedAssert("a83481457efecc69ad1342e21d9c0297f71debbf5c9304b4c1b2e433c1a78f98");
private static final String TEST_EXPECTED_DIGEST = "84892f70600e549fb72879667a9d96a273f144b698ff9ef5a76062a56061a909884f6d9f42918a9e476ed518c4ac8f714bd33f045152ae049877fd3d1b0db25a";
private static final ChunkSizeChoice SIZE_CHOICE = ChunkSizeChoice.everyNthByte(32);
private static final String[] TEST_INPUT_PARTS = {"this is a test", " input to the incremental ", "mac stream"};

public void testIncrementalDigestCreation() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] actualDigest = fullIncrementalDigest(out, TEST_INPUT_PARTS);
assertEquals(String.join("", TEST_INPUT_PARTS), out.toString());
assertEquals(TEST_EXPECTED_DIGEST, Hex.toStringCondensed(actualDigest));
}

public void testIncrementalValidationSuccess() throws IOException {
byte[] digest = fullIncrementalDigest(new ByteArrayOutputStream(), TEST_INPUT_PARTS);
ByteArrayInputStream in = new ByteArrayInputStream(String.join("", TEST_INPUT_PARTS).getBytes());

try (IncrementalMacInputStream incrementalIn = new IncrementalMacInputStream(in, TEST_HMAC_KEY, SIZE_CHOICE, digest)) {
byte[] buffer = new byte[10]; // intentionally small
while (incrementalIn.read(buffer) != -1) {
}
}
}

public void testIncrementalValidationFailure() throws IOException {
byte[] digest = fullIncrementalDigest(new ByteArrayOutputStream(), TEST_INPUT_PARTS);
byte[] corruptInput = String.join("", TEST_INPUT_PARTS).getBytes();
corruptInput[42] ^= 0xff;
int EXPECTED_SUCCESSFUL_READS = 2;
ByteArrayInputStream in = new ByteArrayInputStream(corruptInput);
try (IncrementalMacInputStream incrementalIn = new IncrementalMacInputStream(in, TEST_HMAC_KEY, SIZE_CHOICE, digest)) {
byte[] buffer = new byte[SIZE_CHOICE.getSizeInBytes()];
for (int i = 0; i < EXPECTED_SUCCESSFUL_READS; i++) {
incrementalIn.read(buffer);
}
try {
incrementalIn.read(buffer);
fail("The read should have failed");
} catch (InvalidMacException _ex) {

}
}
}

public void testSingleByteRead() throws IOException {
byte[] digest = fullIncrementalDigest(new ByteArrayOutputStream(), TEST_INPUT_PARTS);
ByteArrayInputStream in = new ByteArrayInputStream(new byte[]{});
try (IncrementalMacInputStream incrementalIn = new IncrementalMacInputStream(in, TEST_HMAC_KEY, SIZE_CHOICE, digest)) {
// The first read with an empty input should call finalize on incremental mac, and throw an exception
try {
incrementalIn.read();
fail("Validation should have failed");
} catch (IOException ex) {
}
}

}

public void testMultipleFlushesWhileWriting() throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteArrayOutputStream digestStream = new ByteArrayOutputStream();
try (IncrementalMacOutputStream incrementalOut = new IncrementalMacOutputStream(out, TEST_HMAC_KEY, SIZE_CHOICE, digestStream)) {
for (String part : TEST_INPUT_PARTS) {
incrementalOut.write(part.getBytes());
incrementalOut.flush();
}
}
byte[] actualDigest = digestStream.toByteArray();
assertEquals(TEST_EXPECTED_DIGEST, Hex.toStringCondensed(actualDigest));
}

public void testOutputStreamCloseIsIdempotent() throws IOException {
ByteArrayOutputStream digestStream = new ByteArrayOutputStream();
IncrementalMacOutputStream incrementalOut = new IncrementalMacOutputStream(new ByteArrayOutputStream(), TEST_HMAC_KEY, SIZE_CHOICE, digestStream);
for (String part : TEST_INPUT_PARTS) {
incrementalOut.write(part.getBytes());
}
incrementalOut.close();
incrementalOut.close();

assertEquals(TEST_EXPECTED_DIGEST, Hex.toStringCondensed(digestStream.toByteArray()));
}

private byte[] fullIncrementalDigest(OutputStream innerOut, String[] input) throws IOException {
ByteArrayOutputStream digestStream = new ByteArrayOutputStream();
try (IncrementalMacOutputStream incrementalOut = new IncrementalMacOutputStream(innerOut, TEST_HMAC_KEY, SIZE_CHOICE, digestStream)) {
for (String part : input) {
incrementalOut.write(part.getBytes());
}
incrementalOut.flush();
}
return digestStream.toByteArray();
}
}
11 changes: 11 additions & 0 deletions java/shared/java/org/signal/libsignal/internal/Native.java
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,12 @@ private Native() {}

public static native boolean IdentityKey_VerifyAlternateIdentity(long publicKey, long otherIdentity, byte[] signature);

public static native int IncrementalMac_CalculateChunkSize(int dataSize);
public static native void IncrementalMac_Destroy(long handle);
public static native byte[] IncrementalMac_Finalize(long mac);
public static native long IncrementalMac_Initialize(byte[] key, int chunkSize);
public static native byte[] IncrementalMac_Update(long mac, byte[] bytes, int offset, int length);

public static native void Logger_Initialize(int maxLevel, Class loggerClass);
public static native void Logger_SetMaxLevel(int maxLevel);

Expand Down Expand Up @@ -496,4 +502,9 @@ private Native() {}
public static native void Username_Verify(byte[] proof, byte[] hash);

public static native void UuidCiphertext_CheckValidContents(byte[] buffer);

public static native void ValidatingMac_Destroy(long handle);
public static native boolean ValidatingMac_Finalize(long mac);
public static native long ValidatingMac_Initialize(byte[] key, int chunkSize, byte[] digests);
public static native boolean ValidatingMac_Update(long mac, byte[] bytes, int offset, int length);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//

package org.signal.libsignal.protocol.incrementalmac;

import org.signal.libsignal.internal.Native;

public abstract class ChunkSizeChoice {

public abstract int getSizeInBytes();

public static ChunkSizeChoice everyNthByte(int n) {
return new EveryN(n);
}

public static ChunkSizeChoice inferChunkSize(int dataSize) {
return new ChunksOf(dataSize);
}

private static final class EveryN extends ChunkSizeChoice {
private int n;

private EveryN(int n) {
this.n = n;
}

public int getSizeInBytes() {
return this.n;
}
}

private static final class ChunksOf extends ChunkSizeChoice {
private int dataSize;

private ChunksOf(int dataSize) {
this.dataSize = dataSize;
}

public int getSizeInBytes() {
return Native.IncrementalMac_CalculateChunkSize(this.dataSize);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//

package org.signal.libsignal.protocol.incrementalmac;

import org.signal.libsignal.internal.Native;

import java.io.IOException;
import java.io.InputStream;

public final class IncrementalMacInputStream extends InputStream {
private final long validatingMac;

private final InputStream inner;
private boolean closed = false;

public IncrementalMacInputStream(InputStream inner, byte[] key, ChunkSizeChoice sizeChoice, byte[] digest) {
int chunkSize = sizeChoice.getSizeInBytes();
this.validatingMac = Native.ValidatingMac_Initialize(key, chunkSize, digest);
this.inner = inner;
}

@Override
public int read() throws IOException {
int read = this.inner.read();
// Narrowing conversion to byte is expected and intentional
byte[] bytes = {(byte) read};
int bytesLength = (read == -1) ? -1 : 1;
return handleRead(bytes, 0, bytesLength);
}

@Override
public int read(byte[] bytes, int offset, int length) throws IOException {
int read = this.inner.read(bytes, offset, length);
return handleRead(bytes, offset, read);
}

private int handleRead(byte[] bytes, int offset, int read) throws IOException {
boolean isValid = (read == -1) ? Native.ValidatingMac_Finalize(this.validatingMac) : Native.ValidatingMac_Update(this.validatingMac, bytes, offset, read);
if (!isValid) {
throw new InvalidMacException();
}
return read;
}

@Override
public void close() throws IOException {
if (this.closed) {
return;
}
this.inner.close();
Native.ValidatingMac_Destroy(this.validatingMac);
this.closed = true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//

package org.signal.libsignal.protocol.incrementalmac;

import org.signal.libsignal.internal.Native;

import java.io.IOException;
import java.io.OutputStream;

public final class IncrementalMacOutputStream extends OutputStream {
private final long incrementalMac;
private final OutputStream digestStream;
private final OutputStream inner;
private boolean closed = false;

public IncrementalMacOutputStream(OutputStream inner, byte[] key, ChunkSizeChoice sizeChoice, OutputStream digestStream) {
int chunkSize = sizeChoice.getSizeInBytes();
this.incrementalMac = Native.IncrementalMac_Initialize(key, chunkSize);
this.inner = inner;
this.digestStream = digestStream;
}

@Override
public void write(byte[] buffer) throws IOException {
this.inner.write(buffer);
byte[] digestIncrement = Native.IncrementalMac_Update(this.incrementalMac, buffer, 0, buffer.length);
digestStream.write(digestIncrement);
}

@Override
public void write(byte[] buffer, int offset, int length) throws IOException {
this.inner.write(buffer, offset, length);
byte[] digestIncrement = Native.IncrementalMac_Update(this.incrementalMac, buffer, offset, length);
digestStream.write(digestIncrement);
}

@Override
public void write(int b) throws IOException {
// According to the spec the narrowing conversion to byte is expected here
byte[] bytes = {(byte) b};
byte[] digestIncrement = Native.IncrementalMac_Update(this.incrementalMac, bytes, 0, 1);
this.inner.write(b);
this.digestStream.write(digestIncrement);
}

@Override
public void flush() throws IOException {
this.inner.flush();
digestStream.flush();
}

@Override
public void close() throws IOException {
if (this.closed) {
return;
}
try {
flush();
} catch (IOException ignored) {
}
byte[] digestIncrement = Native.IncrementalMac_Finalize(this.incrementalMac);
digestStream.write(digestIncrement);
Native.IncrementalMac_Destroy(this.incrementalMac);
this.inner.close();
this.digestStream.close();
this.closed = true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//
// Copyright 2023 Signal Messenger, LLC.
// SPDX-License-Identifier: AGPL-3.0-only
//
package org.signal.libsignal.protocol.incrementalmac;

import java.io.IOException;

public class InvalidMacException extends IOException {
}
9 changes: 9 additions & 0 deletions node/Native.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ export function IdentityKeyPair_Deserialize(buffer: Buffer): {publicKey:PublicKe
export function IdentityKeyPair_Serialize(publicKey: Wrapper<PublicKey>, privateKey: Wrapper<PrivateKey>): Buffer;
export function IdentityKeyPair_SignAlternateIdentity(publicKey: Wrapper<PublicKey>, privateKey: Wrapper<PrivateKey>, otherIdentity: Wrapper<PublicKey>): Buffer;
export function IdentityKey_VerifyAlternateIdentity(publicKey: Wrapper<PublicKey>, otherIdentity: Wrapper<PublicKey>, signature: Buffer): boolean;
export function IncrementalMac_CalculateChunkSize(dataSize: number): number;
export function IncrementalMac_Finalize(mac: Wrapper<IncrementalMac>): Buffer;
export function IncrementalMac_Initialize(key: Buffer, chunkSize: number): IncrementalMac;
export function IncrementalMac_Update(mac: Wrapper<IncrementalMac>, bytes: Buffer, offset: number, length: number): Buffer;
export function Mp4Sanitizer_Sanitize(input: InputStream, len: Buffer): Promise<SanitizedMetadata>;
export function PlaintextContent_Deserialize(data: Buffer): PlaintextContent;
export function PlaintextContent_FromDecryptionErrorMessage(m: Wrapper<DecryptionErrorMessage>): PlaintextContent;
Expand Down Expand Up @@ -319,6 +323,9 @@ export function Username_Hash(username: string): Buffer;
export function Username_Proof(username: string, randomness: Buffer): Buffer;
export function Username_Verify(proof: Buffer, hash: Buffer): void;
export function UuidCiphertext_CheckValidContents(buffer: Buffer): void;
export function ValidatingMac_Finalize(mac: Wrapper<ValidatingMac>): boolean;
export function ValidatingMac_Initialize(key: Buffer, chunkSize: number, digests: Buffer): ValidatingMac;
export function ValidatingMac_Update(mac: Wrapper<ValidatingMac>, bytes: Buffer, offset: number, length: number): boolean;
export function initLogger(maxLevel: LogLevel, callback: (level: LogLevel, target: string, file: string | null, line: number | null, message: string) => void): void
interface Aes256GcmSiv { readonly __type: unique symbol; }
interface AuthCredential { readonly __type: unique symbol; }
Expand All @@ -334,6 +341,7 @@ interface GroupMasterKey { readonly __type: unique symbol; }
interface GroupPublicParams { readonly __type: unique symbol; }
interface GroupSecretParams { readonly __type: unique symbol; }
interface HsmEnclaveClient { readonly __type: unique symbol; }
interface IncrementalMac { readonly __type: unique symbol; }
interface PlaintextContent { readonly __type: unique symbol; }
interface PreKeyBundle { readonly __type: unique symbol; }
interface PreKeyRecord { readonly __type: unique symbol; }
Expand Down Expand Up @@ -366,3 +374,4 @@ interface SignalMessage { readonly __type: unique symbol; }
interface SignedPreKeyRecord { readonly __type: unique symbol; }
interface UnidentifiedSenderMessageContent { readonly __type: unique symbol; }
interface UuidCiphertext { readonly __type: unique symbol; }
interface ValidatingMac { readonly __type: unique symbol; }
Loading

0 comments on commit 2b46ae1

Please sign in to comment.