From 6af97321052e83f4074cff2737c6c6fa9aad71b6 Mon Sep 17 00:00:00 2001 From: "Marc R. Hoffmann" Date: Fri, 17 Nov 2023 18:42:16 +0100 Subject: [PATCH] Padded data should be at least 32 bytes --- .../mountainminds/three4j/PaddedBuffer.java | 61 ++++++++++ .../mountainminds/three4j/PlainMessage.java | 21 +--- .../three4j/PaddedBufferTest.java | 105 ++++++++++++++++++ 3 files changed, 172 insertions(+), 15 deletions(-) create mode 100644 src/main/java/com/mountainminds/three4j/PaddedBuffer.java create mode 100644 src/test/java/com/mountainminds/three4j/PaddedBufferTest.java diff --git a/src/main/java/com/mountainminds/three4j/PaddedBuffer.java b/src/main/java/com/mountainminds/three4j/PaddedBuffer.java new file mode 100644 index 0000000..6e4e1f9 --- /dev/null +++ b/src/main/java/com/mountainminds/three4j/PaddedBuffer.java @@ -0,0 +1,61 @@ +/******************************************************************************* + * Copyright (c) 2023 Mountainminds GmbH & Co. KG + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * SPDX-License-Identifier: MIT + *******************************************************************************/ +package com.mountainminds.three4j; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.security.SecureRandom; +import java.util.Random; + +/** + * Internal utility for PKCS#7 padding. + */ +class PaddedBuffer extends DataOutputStream { + + private static ThreadLocal RANDOM = ThreadLocal.withInitial(SecureRandom::new); + + private static final int MSG_LEN_MIN = 32; + private static final int PAD_LEN_MIN = 1; + private static final int PAD_LEN_MAX = 255; + + private final Random random; + + PaddedBuffer(Random random) { + super(new ByteArrayOutputStream()); + this.random = random; + } + + PaddedBuffer() { + this(RANDOM.get()); + } + + byte[] withPadding() throws IOException { + var buffer = (ByteArrayOutputStream) out; + int panLenMin = Math.max(PAD_LEN_MIN, MSG_LEN_MIN - buffer.size()); + int padding = random.nextInt(PAD_LEN_MAX - panLenMin + 1) + panLenMin; + for (int i = 0; i < padding; i++) { + write(padding); + } + return buffer.toByteArray(); + } + + static DataInputStream removePadding(byte[] buffer) { + int len = 0xff & buffer[buffer.length - 1]; + return new DataInputStream(new ByteArrayInputStream(buffer, 0, buffer.length - len)); + } + +} diff --git a/src/main/java/com/mountainminds/three4j/PlainMessage.java b/src/main/java/com/mountainminds/three4j/PlainMessage.java index 2164593..6449c74 100644 --- a/src/main/java/com/mountainminds/three4j/PlainMessage.java +++ b/src/main/java/com/mountainminds/three4j/PlainMessage.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2022 Mountainminds GmbH & Co. KG + * Copyright (c) 2023 Mountainminds GmbH & Co. KG * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, @@ -15,8 +15,6 @@ import static java.nio.charset.StandardCharsets.UTF_8; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; @@ -30,7 +28,6 @@ import com.google.gson.Gson; -import software.pando.crypto.nacl.Bytes; import software.pando.crypto.nacl.CryptoBox; /** @@ -51,15 +48,10 @@ public EncryptedMessage encrypt(PrivateKey privateKey, PublicKey publicKey) { } private final byte[] encode() { - try (var buffer = new ByteArrayOutputStream(); var out = new DataOutputStream(buffer)) { - out.write(getType()); - encode(out); - // add random padding of 1 - 255 bytes (PKCS#7 style) - int padding = Math.max(1, 0xFF & Bytes.secureRandom(1)[0]); - for (int i = 0; i < padding; i++) { - out.write(padding); - } - return buffer.toByteArray(); + try (var buffer = new PaddedBuffer()) { + buffer.write(getType()); + encode(buffer); + return buffer.withPadding(); } catch (IOException e) { // Must not happen with ByteArrayOutputStream throw new RuntimeException("Unexpected IOException", e); @@ -75,8 +67,7 @@ private final byte[] encode() { * @return decoded message of the respective subtype */ public static PlainMessage decode(byte[] bytes) throws IllegalArgumentException { - int padding = 0xFF & bytes[bytes.length - 1]; - try (var in = new DataInputStream(new ByteArrayInputStream(bytes, 0, bytes.length - padding))) { + try (var in = PaddedBuffer.removePadding(bytes)) { int type = in.read(); switch (type) { case Text.TYPE: diff --git a/src/test/java/com/mountainminds/three4j/PaddedBufferTest.java b/src/test/java/com/mountainminds/three4j/PaddedBufferTest.java new file mode 100644 index 0000000..d9723a2 --- /dev/null +++ b/src/test/java/com/mountainminds/three4j/PaddedBufferTest.java @@ -0,0 +1,105 @@ +/******************************************************************************* + * Copyright (c) 2021 Mountainminds GmbH & Co. KG + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + * SPDX-License-Identifier: MIT + *******************************************************************************/ +package com.mountainminds.three4j; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Random; + +import org.junit.jupiter.api.Test; + +public class PaddedBufferTest { + + private final Random minRandom = new Random() { + private static final long serialVersionUID = 1L; + + @Override + public int nextInt(int bound) { + return 0; + } + }; + + private final Random maxRandom = new Random() { + private static final long serialVersionUID = 1L; + + @Override + public int nextInt(int bound) { + return bound - 1; + } + }; + + @Test + void finish_should_expand_message_to_at_least_32_bytes() throws IOException { + try (var buffer = new PaddedBuffer(minRandom)) { + buffer.write(0x12); + buffer.write(0x15); + var msg = buffer.withPadding(); + var expected = bytes(0x12, 0x15).nbytes(30, 30).toByteArray(); + assertArrayEquals(expected, msg); + } + } + + @Test + void finish_should_add_at_least_1_byte() throws IOException { + try (var buffer = new PaddedBuffer(minRandom)) { + buffer.write(bytes().nbytes(64, 42).toByteArray()); + var msg = buffer.withPadding(); + var expected = bytes().nbytes(64, 42).nbytes(1, 1).toByteArray(); + assertArrayEquals(expected, msg); + } + } + + @Test + void finish_should_add_at_most_255_bytes() throws IOException { + try (var buffer = new PaddedBuffer(maxRandom)) { + buffer.write(0x42); + buffer.write(0x43); + var msg = buffer.withPadding(); + var expected = bytes(0x42, 0x43).nbytes(255, 255).toByteArray(); + assertArrayEquals(expected, msg); + } + } + + @Test + void removePadding_should_remove_padding_of_length_1() throws IOException { + var data = PaddedBuffer.removePadding(bytes(1, 2, 3).nbytes(1, 1).toByteArray()); + assertArrayEquals(bytes(1, 2, 3).toByteArray(), data.readAllBytes()); + } + + @Test + void removePadding_should_remove_padding_of_length_255() throws IOException { + var data = PaddedBuffer.removePadding(bytes(1, 2, 3).nbytes(255, 255).toByteArray()); + assertArrayEquals(bytes(1, 2, 3).toByteArray(), data.readAllBytes()); + } + + private static Bytes bytes(int... bytes) { + var out = new Bytes(); + for (int b : bytes) { + out.write(0xff & b); + } + return out; + } + + private static class Bytes extends ByteArrayOutputStream { + Bytes nbytes(int count, int value) { + for (int i = 0; i < count; i++) { + write(0xff & value); + } + return this; + } + } + +}