来自 BIP39 助记符的种子与测试向量不匹配

Seed from BIP39 Mnemonic not matching Test Vectors

我正在编写 Java 比特币 BIP39 的实现。到目前为止,我的代码能够正确生成随机生成的助记词。但是,将 12 字助记词短语转换为 512 位种子时,生成的值与 Ian Coleman's BIP39 Tool.

的结果不匹配

首先,SecureRandom 对象生成一个随机的 512 位熵值 (ENT)。 ENT 值使用 SHA256 进行哈希计算以计算校验和值 (CS),即哈希的前 4 位。校验和连接到 ENT 的末尾以给出 ENT_CS。 ENT_CS被拆分成每段11位,以这11位对应的整数值作为索引号,从Word List中得到一个词。这会生成我的 12 字助记词。到目前为止,到目前为止的所有步骤都符合上述 BIP39 工具的预期结果。

为了创建种子,我将 PBKDF2 与 HmacSHA512 结合使用,将迭代次数设置为 2048,并将密钥大小设置为 512 位(64 字节)。我已经针对这些 Test Vectors, Google's "crypto" package implementation, and NovaCrypto's Java BIP39 implementation. The mnemonic words, excluding separators, is used as input along with a salt of "mnemonic"+password as per the Bitcoin Core BIP39 Specifications.

测试了 PBKDF2 的实现

PBKDF2 函数

  public static byte[] PBKDF2(String mnemonic, String salt) {
    try {
      byte[] fixedSalt = ("mnemonic"+salt).getBytes(StandardCharsets.UTF_8);
      KeySpec spec = new PBEKeySpec(mnemonic.toCharArray(), fixedSalt, 2048, 512);
      SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512");
      return f.generateSecret(spec).getEncoded();
    } catch (NoSuchAlgorithmException | InvalidKeySpecException ex) {
      throw new RuntimeException(ex);
    }
  }

生成助记词功能

public static String[] generateMnemonic() {
  // Generate 128-bit Random Number for Entropy
  byte[] ENT = getEntropy();

  // Hash the Entropy value
  byte[] HASH = SHA256(ENT);

  // Copy first 4 bits of Hash as Checksum
  boolean[] CS = Arrays.copyOfRange(bytesToBits(HASH), 0, 4);

  // Add Checksum to the end of Entropy bits
  boolean[] ENT_CS = Arrays.copyOf(bytesToBits(ENT), bytesToBits(ENT).length + CS.length);
  System.arraycopy(CS, 0, ENT_CS, bytesToBits(ENT).length, CS.length);

  // Split ENT_CS into groups of 11 bits and creates String array for
  // mnemonicWords
  String[] mnemonicWords = new String[12];
  for (int i = 0; i < 12; i++) {
    boolean[] numBits = Arrays.copyOfRange(ENT_CS, i * 11, i * 11 + 11);
    mnemonicWords[i] = wordList.get(bitsToInt(numBits));
  }
  return mnemonicWords;
}

辅助函数

// Returns randomly generated, 16-byte number
  public static byte[] getEntropy() {
    byte[] ent = new byte[16];
    sr.nextBytes(ent);
    return ent;
  }

// Returns bit representation of byte array
  public static boolean[] bytesToBits(byte[] data) {
    boolean[] bits = new boolean[data.length * 8];
    for (int i = 0; i < data.length; ++i)
      for (int j = 0; j < 8; ++j)
        bits[(i * 8) + j] = (data[i] & (1 << (7 - j))) != 0;
    return bits;
  }

// Returns hex string from byte array
  private final static char[] hexArray = "0123456789ABCDEF".toCharArray();
  public static String bytesToHex(byte[] bytes) {
    char[] hexChars = new char[bytes.length * 2];
    for (int j = 0; j < bytes.length; j++) {
      int v = bytes[j] & 0xFF;
      hexChars[j * 2] = hexArray[v >>> 4];
      hexChars[j * 2 + 1] = hexArray[v & 0x0F];
    }
    return new String(hexChars);
  }

// Returns SHA256 hash of input data
  public static byte[] SHA256(byte[] data) {
    try {
      MessageDigest digest = MessageDigest.getInstance("SHA-256");
      System.out.println(Arrays.toString(data));
      return digest.digest(data);
    } catch (NoSuchAlgorithmException ex) {
      throw new RuntimeException(ex);
    }
  }

// Returns int value of a bit array
  public static int bitsToInt(boolean[] bits) {
    int n = 0, l = bits.length;
    for (int i = 0; i < l; ++i) {
      n = (n << 1) + (bits[i] ? 1 : 0);
    }
    return n;
  }

用法

// Generate Mnemonic Words, Mnemonic Phrase, and Seed
    String[] mnemonicWords = generateMnemonic();
    String mnemonicPhrase = "";
    for (String word : mnemonicWords)
      mnemonicPhrase += word;
    byte[] seed = PBKDF2(mnemonicPhrase, "");
    System.out.println("Seed: " + bytesToHex(seed));

示例结果

    My Program Trial
Entropy (hex): 3CCB62D9AF76F1E8DB113E66B2D84656
Checksum bits: 1100
Raw Binary: 00111100110 01011011000 10110110011 01011110111 01101111000 11110100011 01101100010 00100111110 01100110101 10010110110 00010001100 1010110
Mnemonic: devote force reopen galaxy humor virtual hobby chief grit nothing bag pulse
Seed: 013FFA714C57AA26C59DC215880D9C2398A8B38D10D7E41A882CF98C35976F0BF26BCC08B0B196945DE8778C7FD561FB0F20A8B9BAD46B12196C963A85E3B40E

    Expected Results (Derived from same Entropy)
Entropy (hex): 3CCB62D9AF76F1E8DB113E66B2D84656
Checksum bits: 1100
Raw Binary: 00111100110 01011011000 10110110011 01011110111 01101111000 11110100011 01101100010 00100111110 01100110101 10010110110 00010001100 1010110
Mnemonic: devote force reopen galaxy humor virtual hobby chief grit nothing bag pulse
Seed: 0c3c5f9ae724a2a3ed70aeb24919c10506e4962223a5375f70164be8b897d615ec9bf9f3e64a889cff03318cc5d0b3c8378ba0264d198e307c609632016ddd01

看来我能够回答我自己的问题了。 在我的程序中,我使用

连接没有空格的种子词
String mnemonicPhrase = "";
for (String word : mnemonicWords)
  mnemonicPhrase += word;

但这不是正确的格式,因为要包含空格。更改此代码块以添加空格:

String mnemonicPhrase = "";
for(int i=0; i<mnemonicWords.length; i++) {
    mnemonicPhrase += mnemonicWords[i];
    if(i < mnemonicWords.length-1) mnemonicPhrase += " ";
}

使用 "TREZOR".

的密码生成 here 发布的预期测试向量结果