Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Padded data should be at least 32 bytes #39

Merged
merged 1 commit into from
Nov 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions src/main/java/com/mountainminds/three4j/PaddedBuffer.java
Original file line number Diff line number Diff line change
@@ -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> 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));
}

}
21 changes: 6 additions & 15 deletions src/main/java/com/mountainminds/three4j/PlainMessage.java
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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;
Expand All @@ -30,7 +28,6 @@

import com.google.gson.Gson;

import software.pando.crypto.nacl.Bytes;
import software.pando.crypto.nacl.CryptoBox;

/**
Expand All @@ -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);
Expand All @@ -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:
Expand Down
105 changes: 105 additions & 0 deletions src/test/java/com/mountainminds/three4j/PaddedBufferTest.java
Original file line number Diff line number Diff line change
@@ -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;
}
}

}