From 192444f09aeca444aa0dc11a2cc52321539f114b Mon Sep 17 00:00:00 2001 From: Sean Gilligan Date: Sat, 20 May 2023 16:59:19 -0400 Subject: [PATCH] ByteArray: immutable byte array wrapper This is used for AesKey, and can be used for more. --- .../org/bitcoinj/base/internal/ByteArray.java | 83 +++++++++++++++++++ .../main/java/org/bitcoinj/crypto/AesKey.java | 16 +--- .../bitcoinj/base/internal/ByteArrayTest.java | 68 +++++++++++++++ 3 files changed, 154 insertions(+), 13 deletions(-) create mode 100644 core/src/main/java/org/bitcoinj/base/internal/ByteArray.java create mode 100644 core/src/test/java/org/bitcoinj/base/internal/ByteArrayTest.java diff --git a/core/src/main/java/org/bitcoinj/base/internal/ByteArray.java b/core/src/main/java/org/bitcoinj/base/internal/ByteArray.java new file mode 100644 index 000000000..5a6c47335 --- /dev/null +++ b/core/src/main/java/org/bitcoinj/base/internal/ByteArray.java @@ -0,0 +1,83 @@ +/* + * Copyright by the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bitcoinj.base.internal; + +import java.util.Arrays; + +/** + * An effectively-immutable byte array. + */ +public class ByteArray implements Comparable { + protected final byte[] bytes; + + /** + * Wrapper for a {@code byte[]} + * @param bytes byte data to wrap + */ + public ByteArray(byte[] bytes) { + // Make defensive copy, so we are effectively immutable + this.bytes = new byte[bytes.length]; + System.arraycopy(bytes, 0, this.bytes, 0, bytes.length); + } + + /** + * @return the key bytes + */ + public byte[] bytes() { + return bytes; + } + + /** + * @return the bytes as a hex-formatted string + */ + public String formatHex() { + return ByteUtils.formatHex(bytes); + } + + /** + * {@inheritDoc} + * @return {@inheritDoc} + */ + @Override + public int hashCode() { + return Arrays.hashCode(bytes); + } + + /** + * {@inheritDoc} + * @param o {@inheritDoc} + * @return {@inheritDoc} + */ + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ByteArray other = (ByteArray) o; + return Arrays.equals(this.bytes, other.bytes); + } + + /** + * {@inheritDoc} + *

For {@link ByteArray} this is a byte-by-byte, unsigned comparison. + * @param o {@inheritDoc} + * @return {@inheritDoc} + */ + @Override + public int compareTo(ByteArray o) { + return ByteUtils.arrayUnsignedComparator().compare(bytes, o.bytes); + } +} diff --git a/core/src/main/java/org/bitcoinj/crypto/AesKey.java b/core/src/main/java/org/bitcoinj/crypto/AesKey.java index cd6868986..2ebe725b7 100644 --- a/core/src/main/java/org/bitcoinj/crypto/AesKey.java +++ b/core/src/main/java/org/bitcoinj/crypto/AesKey.java @@ -16,6 +16,7 @@ package org.bitcoinj.crypto; +import org.bitcoinj.base.internal.ByteArray; import org.bouncycastle.crypto.params.KeyParameter; /** @@ -27,24 +28,13 @@ import org.bouncycastle.crypto.params.KeyParameter; * If for some reason you have code that uses the Bouncy Castle {@link KeyParameter} type and need to convert * to or from {@code AesKey}, you can temporarily use {@link #ofKeyParameter(KeyParameter)} or {@link #toKeyParameter()} */ -public class AesKey { - private final byte[] bytes; - +public class AesKey extends ByteArray { /** * Wrapper for a {@code byte[]} containing an AES Key * @param keyBytes implementation-dependent AES Key bytes */ public AesKey(byte[] keyBytes) { - // Make defensive copy, so AesKey is effectively immutable - this.bytes = new byte[keyBytes.length]; - System.arraycopy(keyBytes, 0, this.bytes, 0, keyBytes.length); - } - - /** - * @return The key bytes - */ - public byte[] bytes() { - return bytes; + super(keyBytes); } /** diff --git a/core/src/test/java/org/bitcoinj/base/internal/ByteArrayTest.java b/core/src/test/java/org/bitcoinj/base/internal/ByteArrayTest.java new file mode 100644 index 000000000..3888434ec --- /dev/null +++ b/core/src/test/java/org/bitcoinj/base/internal/ByteArrayTest.java @@ -0,0 +1,68 @@ +/* + * Copyright by the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bitcoinj.base.internal; + +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; +import nl.jqno.equalsverifier.EqualsVerifier; +import nl.jqno.equalsverifier.Warning; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; + +@RunWith(JUnitParamsRunner.class) +public class ByteArrayTest { + + @Test + public void testImmutability() { + byte[] bytes = new byte[]{0x00}; + ByteArray ba = new ByteArray(bytes); + // Modify original array + bytes[0] = (byte) 0xFF; + + // Verify ByteArray not modified (due to defensive copy) + assertNotEquals(ba.bytes, bytes); + } + + @Test + public void equalsContract() { + EqualsVerifier.forClass(ByteArray.class) + .suppress(Warning.NULL_FIELDS) + .suppress(Warning.TRANSIENT_FIELDS) + .usingGetClass() + .verify(); + } + + @Test + @Parameters(method = "bytesToHexStringVectors") + public void formatHexValid(byte[] bytes, String expectedHexString) { + ByteArray ba = new ByteArray(bytes); + assertEquals("incorrect hex formatted string", expectedHexString, ba.formatHex()); + } + + // Two-way test vectors (can be used to validate mapping in both directions) + private Object[] bytesToHexStringVectors() { + return new Object[]{ + new Object[]{ new byte[] {}, ""}, + new Object[]{ new byte[] {0x00}, "00"}, + new Object[]{ new byte[] {(byte) 0xff}, "ff"}, + new Object[]{ new byte[] {(byte) 0xab, (byte) 0xcd, (byte) 0xef}, "abcdef"} + }; + } +}