-
-
Notifications
You must be signed in to change notification settings - Fork 433
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
0e74a41
commit 2b46ae1
Showing
20 changed files
with
1,259 additions
and
1 deletion.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
109 changes: 109 additions & 0 deletions
109
...er/src/test/java/org/signal/libsignal/protocol/incrementalmac/IncrementalStreamsTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
45 changes: 45 additions & 0 deletions
45
java/shared/java/org/signal/libsignal/protocol/incrementalmac/ChunkSizeChoice.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
57 changes: 57 additions & 0 deletions
57
java/shared/java/org/signal/libsignal/protocol/incrementalmac/IncrementalMacInputStream.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
71 changes: 71 additions & 0 deletions
71
.../shared/java/org/signal/libsignal/protocol/incrementalmac/IncrementalMacOutputStream.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
10 changes: 10 additions & 0 deletions
10
java/shared/java/org/signal/libsignal/protocol/incrementalmac/InvalidMacException.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 { | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.