Skip to content
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
107 changes: 107 additions & 0 deletions src/main/java/com/example/bip39/bit/BitPacker.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package com.example.bip39.bit;

import java.util.Objects;

public final class BitPacker {

private static final int ELEVEN_BIT_GROUP_SIZE = 11;

private BitPacker() {}

public static int readBits(byte[] source, int bitOffset, int bitLength) {
Objects.requireNonNull(source, "source must not be null");
validateBitAccess(source.length, bitOffset, bitLength);
if (bitLength > Integer.SIZE - 1) {
throw new IllegalArgumentException("bitLength must be at most 31");
}

int value = 0;
for (int bitIndex = 0; bitIndex < bitLength; bitIndex++) {
int absoluteBitIndex = bitOffset + bitIndex;
int byteIndex = absoluteBitIndex / Byte.SIZE;
int bitIndexInByte = 7 - (absoluteBitIndex % Byte.SIZE);
int bit = ((source[byteIndex] & 0xff) >> bitIndexInByte) & 0x01;
value = (value << 1) | bit;
}
return value;
}

public static void writeBits(byte[] target, int bitOffset, int bitLength, int value) {
Objects.requireNonNull(target, "target must not be null");
validateBitAccess(target.length, bitOffset, bitLength);
if (bitLength > Integer.SIZE - 1) {
throw new IllegalArgumentException("bitLength must be at most 31");
}
if (bitLength != 0 && value >>> bitLength != 0) {
throw new IllegalArgumentException("value does not fit into the requested bit length");
}

for (int bitIndex = 0; bitIndex < bitLength; bitIndex++) {
int absoluteBitIndex = bitOffset + bitIndex;
int byteIndex = absoluteBitIndex / Byte.SIZE;
int bitIndexInByte = 7 - (absoluteBitIndex % Byte.SIZE);
int bitMask = 1 << bitIndexInByte;
int bit = (value >> (bitLength - 1 - bitIndex)) & 0x01;
int clearedByte = target[byteIndex] & ~bitMask;
target[byteIndex] = (byte) (clearedByte | (bit << bitIndexInByte));
}
}

public static int[] splitTo11BitValues(byte[] source, int totalBitLength) {
Objects.requireNonNull(source, "source must not be null");
validateTotalBitLength(source.length, totalBitLength);
if (totalBitLength % ELEVEN_BIT_GROUP_SIZE != 0) {
throw new IllegalArgumentException("totalBitLength must be divisible by 11");
}

int[] values = new int[totalBitLength / ELEVEN_BIT_GROUP_SIZE];
for (int valueIndex = 0; valueIndex < values.length; valueIndex++) {
values[valueIndex] =
readBits(source, valueIndex * ELEVEN_BIT_GROUP_SIZE, ELEVEN_BIT_GROUP_SIZE);
}
return values;
}

public static byte[] pack11BitValues(int[] values) {
Objects.requireNonNull(values, "values must not be null");
byte[] packed = new byte[(values.length * ELEVEN_BIT_GROUP_SIZE + 7) / Byte.SIZE];
for (int valueIndex = 0; valueIndex < values.length; valueIndex++) {
int value = values[valueIndex];
if (value < 0 || value > 0x7ff) {
throw new IllegalArgumentException("11-bit value out of range: " + value);
}
writeBits(packed, valueIndex * ELEVEN_BIT_GROUP_SIZE, ELEVEN_BIT_GROUP_SIZE, value);
}
return packed;
}

public static byte[] extractBytes(byte[] source, int bitOffset, int bitLength) {
Objects.requireNonNull(source, "source must not be null");
validateBitAccess(source.length, bitOffset, bitLength);
if (bitLength % Byte.SIZE != 0) {
throw new IllegalArgumentException("bitLength must be divisible by 8");
}

byte[] bytes = new byte[bitLength / Byte.SIZE];
for (int byteIndex = 0; byteIndex < bytes.length; byteIndex++) {
bytes[byteIndex] = (byte) readBits(source, bitOffset + byteIndex * Byte.SIZE, Byte.SIZE);
}
return bytes;
}

private static void validateTotalBitLength(int sourceLengthBytes, int totalBitLength) {
validateBitAccess(sourceLengthBytes, 0, totalBitLength);
}

private static void validateBitAccess(int sourceLengthBytes, int bitOffset, int bitLength) {
if (bitOffset < 0) {
throw new IllegalArgumentException("bitOffset must not be negative");
}
if (bitLength < 0) {
throw new IllegalArgumentException("bitLength must not be negative");
}
if (bitOffset + bitLength > sourceLengthBytes * Byte.SIZE) {
throw new IllegalArgumentException("Requested bits exceed source length");
}
}
}
75 changes: 75 additions & 0 deletions src/main/java/com/example/bip39/crypto/Pbkdf2HmacSha512.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.example.bip39.crypto;

import com.example.bip39.error.Bip39ErrorCode;
import com.example.bip39.error.Bip39Exception;
import com.example.bip39.util.Bip39Constants;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Objects;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public final class Pbkdf2HmacSha512 {

private static final String HMAC_SHA512 = "HmacSHA512";

private Pbkdf2HmacSha512() {}

public static byte[] derive(byte[] passwordBytes, byte[] saltBytes) {
Objects.requireNonNull(passwordBytes, "passwordBytes must not be null");
Objects.requireNonNull(saltBytes, "saltBytes must not be null");

try {
Mac mac = Mac.getInstance(HMAC_SHA512);
mac.init(new SecretKeySpec(passwordBytes, HMAC_SHA512));
return deriveKey(
mac, saltBytes, Bip39Constants.PBKDF2_ITERATIONS, Bip39Constants.SEED_LEN_BYTES);
} catch (NoSuchAlgorithmException | InvalidKeyException exception) {
throw new Bip39Exception(
Bip39ErrorCode.ERR_PBKDF2_FAILURE, "PBKDF2-HMAC-SHA512 is unavailable", exception);
}
}

private static byte[] deriveKey(
Mac mac, byte[] saltBytes, int iterations, int derivedKeyLengthBytes) {
int blockLengthBytes = mac.getMacLength();
int blockCount = (derivedKeyLengthBytes + blockLengthBytes - 1) / blockLengthBytes;
byte[] derivedKey = new byte[blockCount * blockLengthBytes];

for (int blockIndex = 1; blockIndex <= blockCount; blockIndex++) {
byte[] block = deriveBlock(mac, saltBytes, iterations, blockIndex);
System.arraycopy(block, 0, derivedKey, (blockIndex - 1) * blockLengthBytes, block.length);
}

return Arrays.copyOf(derivedKey, derivedKeyLengthBytes);
}

private static byte[] deriveBlock(Mac mac, byte[] saltBytes, int iterations, int blockIndex) {
byte[] input = Arrays.copyOf(saltBytes, saltBytes.length + Integer.BYTES);
writeIntBigEndian(input, saltBytes.length, blockIndex);

byte[] iterationResult = mac.doFinal(input);
byte[] block = iterationResult.clone();

for (int iteration = 1; iteration < iterations; iteration++) {
iterationResult = mac.doFinal(iterationResult);
xorInto(block, iterationResult);
}

return block;
}

private static void xorInto(byte[] target, byte[] source) {
for (int index = 0; index < target.length; index++) {
target[index] ^= source[index];
}
}

private static void writeIntBigEndian(byte[] bytes, int offset, int value) {
bytes[offset] = (byte) (value >>> 24);
bytes[offset + 1] = (byte) (value >>> 16);
bytes[offset + 2] = (byte) (value >>> 8);
bytes[offset + 3] = (byte) value;
}
}
21 changes: 21 additions & 0 deletions src/main/java/com/example/bip39/crypto/Sha256Digest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.example.bip39.crypto;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Objects;

public final class Sha256Digest {

private Sha256Digest() {}

public static byte[] digest(byte[] input) {
Objects.requireNonNull(input, "input must not be null");

try {
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
return messageDigest.digest(input);
} catch (NoSuchAlgorithmException exception) {
throw new IllegalStateException("SHA-256 is unavailable", exception);
}
}
}
74 changes: 74 additions & 0 deletions src/test/java/com/example/bip39/bit/BitPackerTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.example.bip39.bit;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import org.junit.jupiter.api.Test;

class BitPackerTest {

@Test
void readsBitsMostSignificantBitFirstFromSignedBytes() {
byte[] source = {(byte) 0x80, (byte) 0xf1};

assertEquals(1, BitPacker.readBits(source, 0, 1));
assertEquals(0, BitPacker.readBits(source, 1, 7));
assertEquals(0xf1, BitPacker.readBits(source, 8, 8));
assertEquals(0x01, BitPacker.readBits(source, 12, 4));
}

@Test
void writesBitsAndExtractsBytesWithoutLosingUnsignedValues() {
byte[] target = new byte[3];

BitPacker.writeBits(target, 0, 8, 0x80);
BitPacker.writeBits(target, 8, 8, 0x01);
BitPacker.writeBits(target, 16, 8, 0xff);

assertArrayEquals(new byte[] {(byte) 0x80, 0x01, (byte) 0xff}, target);
assertArrayEquals(target, BitPacker.extractBytes(target, 0, 24));
}

@Test
void extractsBytesAcrossByteBoundaries() {
byte[] source = {0x12, 0x34, 0x56};

assertArrayEquals(new byte[] {0x23, 0x45}, BitPacker.extractBytes(source, 4, 16));
}

@Test
void splitsKnownBitSequenceIntoElevenBitValues() {
byte[] source = {0x12, 0x34, 0x56};

assertArrayEquals(new int[] {145, 1301}, BitPacker.splitTo11BitValues(source, 22));
}

@Test
void packsElevenBitValuesInMostSignificantBitOrder() {
assertArrayEquals(
new byte[] {0x12, 0x34, 0x54}, BitPacker.pack11BitValues(new int[] {145, 1301}));
}

@Test
void elevenBitPackingAndSplittingRoundTrips() {
int[] values = {0, 1, 1024, 2047, 42};

byte[] packed = BitPacker.pack11BitValues(values);

assertArrayEquals(values, BitPacker.splitTo11BitValues(packed, values.length * 11));
}

@Test
void rejectsInvalidBitAccessAndOutOfRangeValues() {
byte[] source = new byte[2];

assertThrows(IllegalArgumentException.class, () -> BitPacker.readBits(source, -1, 1));
assertThrows(IllegalArgumentException.class, () -> BitPacker.readBits(source, 0, 17));
assertThrows(IllegalArgumentException.class, () -> BitPacker.writeBits(source, 0, 3, 0b1000));
assertThrows(
IllegalArgumentException.class, () -> BitPacker.splitTo11BitValues(source, Byte.SIZE));
assertThrows(IllegalArgumentException.class, () -> BitPacker.pack11BitValues(new int[] {2048}));
assertThrows(IllegalArgumentException.class, () -> BitPacker.extractBytes(source, 0, 7));
}
}
49 changes: 49 additions & 0 deletions src/test/java/com/example/bip39/crypto/Pbkdf2HmacSha512Test.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.example.bip39.crypto;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import com.example.bip39.util.Bip39Constants;
import java.nio.charset.StandardCharsets;
import java.util.HexFormat;
import org.junit.jupiter.api.Test;

class Pbkdf2HmacSha512Test {

@Test
void derivesBip39SeedForFirstOfficialVector() {
byte[] derived =
Pbkdf2HmacSha512.derive(
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
.getBytes(StandardCharsets.UTF_8),
"mnemonicTREZOR".getBytes(StandardCharsets.UTF_8));

assertEquals(Bip39Constants.SEED_LEN_BYTES, derived.length);
assertEquals(
"c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e5349553"
+ "1f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04",
HexFormat.of().formatHex(derived));
}

@Test
void isDeterministicForSameInputs() {
byte[] passwordBytes =
"legal winner thank year wave sausage worth useful legal winner thank yellow"
.getBytes(StandardCharsets.UTF_8);
byte[] saltBytes = "mnemonicTREZOR".getBytes(StandardCharsets.UTF_8);

assertArrayEquals(
Pbkdf2HmacSha512.derive(passwordBytes, saltBytes),
Pbkdf2HmacSha512.derive(passwordBytes, saltBytes));
}

@Test
void rejectsNullInputs() {
byte[] passwordBytes = "password".getBytes(StandardCharsets.UTF_8);
byte[] saltBytes = "salt".getBytes(StandardCharsets.UTF_8);

assertThrows(NullPointerException.class, () -> Pbkdf2HmacSha512.derive(null, saltBytes));
assertThrows(NullPointerException.class, () -> Pbkdf2HmacSha512.derive(passwordBytes, null));
}
}
25 changes: 25 additions & 0 deletions src/test/java/com/example/bip39/crypto/Sha256DigestTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.example.bip39.crypto;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.nio.charset.StandardCharsets;
import java.util.HexFormat;
import org.junit.jupiter.api.Test;

class Sha256DigestTest {

@Test
void hashesKnownInput() {
byte[] digest = Sha256Digest.digest("abc".getBytes(StandardCharsets.UTF_8));

assertEquals(
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
HexFormat.of().formatHex(digest));
}

@Test
void rejectsNullInput() {
assertThrows(NullPointerException.class, () -> Sha256Digest.digest(null));
}
}
Loading