reference.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. """Independent fixture generator; needs OpenSSL only when regenerating.
  2. No production code is imported or called. DES/3DES use OpenSSL; SHA-1 uses
  3. hashlib. Packet framing, key derivation, salted MAC and ACR are implemented
  4. here. Public protocol constants and PARVE/ACR definitions are from
  5. InvoxiPlayGames/libxsm3 (LGPL-2.1-or-later), copyright 2013 oct0xor and
  6. 2022-2023 InvoxiPlayGames; see OpenPuck/src/libxsm3/LICENSE.txt.
  7. Prints JSON to stdout; the checked-in fixture is consumed without OpenSSL.
  8. These are synthetic retail-key transcripts, not captures from hardware.
  9. """
  10. import hashlib
  11. import json
  12. import subprocess
  13. KEY_D = bytes.fromhex("e35bfb1ccdad325bf70e07fd623da7c4")
  14. KEY_E = bytes.fromhex("8f2908380b5bfe687c26462a51f2bc19")
  15. ROOT_23 = bytes.fromhex("828078683a523a9810f40c127066dcba")
  16. ROOT_24 = bytes.fromhex("66621a78f8609c8a269a04aed85c1ec8")
  17. SERIAL = b"012345ABCDEF"
  18. RANDOM = bytes(range(0x90, 0xA0))
  19. SBOX = bytes.fromhex(
  20. "b03d9b70f3c78060739f6cc0f13dbb40b3c83714df49dad4482278806ecde700"
  21. "818668e15d7c542c557bef48427b3b68e3dbaac00fa99620950593949af6a364"
  22. "5dcc7600e50819e88d29d74c219117f4bc6ab38083c6d4909bae0efe2e4af200"
  23. "7388d94066c5d40857b18948dc54fc436a2687b8095fce80e40b059c24f3dee2"
  24. "3eec388aa255a4504e4be9587f9f7d80230c4d80054426b8e9d8bce6763a6ea4"
  25. "19dec2d0c4bcc35c59df16463970f4ee2d585aa817866b6029584dd25f287ad8"
  26. "8e79ea8294333181d922d510da92a07d3ddaac1ca25331b83c965200826b56a0"
  27. "d3c240c71b7fdc017270b18c01090936fc97eadee30dae7ee30dae7e33698040"
  28. )
  29. PLAINTEXT = bytes.fromhex(
  30. "d1d2f2806eba0cc0b6c4c9d861751d1a3f9558bed80de2c0d0217920652d9940"
  31. "3c9652001b7fdc01821c13d833698040fc97eade08ea14dceb0f6a186f782cb0"
  32. "d3c240c7826b56a0190936e07270b18ce30dae7e50a52be2c9afc7701c298056"
  33. "24f066fa022b58988fe4d13c6e382affb8fa35b05249c5b466fa47556c8d4008"
  34. )
  35. def crypt(key, data, encrypt=True):
  36. # DES parity bits do not affect OpenSSL's key schedule.
  37. args = ["openssl", "enc", "-des-ede-cbc", "-nopad", "-nosalt",
  38. "-K", key[:16].hex(), "-iv", "0000000000000000"]
  39. if not encrypt:
  40. args.append("-d")
  41. return subprocess.run(args, input=data, capture_output=True,
  42. check=True).stdout
  43. def xor(a, b):
  44. return bytes(x ^ y for x, y in zip(a, b))
  45. def mac(key, data, salt=None):
  46. block = bytes(8)
  47. des_key = key[:8] * 2
  48. if salt is not None:
  49. counter = (int.from_bytes(salt[:8], "big") + 1) % (1 << 64)
  50. salt[:8] = counter.to_bytes(8, "big")
  51. block = crypt(des_key, salt[:8])
  52. for offset in range(0, len(data), 8):
  53. block = crypt(des_key, xor(block, data[offset:offset + 8]))
  54. return crypt(key, xor(block, b"\x80" + bytes(7)))
  55. def parve(key, data):
  56. state = list(data) + [data[0]]
  57. for round_number in range(8, 0, -1):
  58. for index in range(8):
  59. value = SBOX[(key[index] + state[index] + round_number) & 255]
  60. value = (value + state[index + 1]) & 255
  61. state[index + 1] = ((value << 1) | (value >> 7)) & 255
  62. state[0] = state[8]
  63. return bytes(state[:8])
  64. def acr(console, identity, key):
  65. cd = parve(key, identity[:4] + console[:4])
  66. ab = parve(key, identity[16:24])
  67. for offset in range(0, 128, 8):
  68. ab = parve(key, xor(ab, PLAINTEXT[offset:offset + 8]))
  69. modulus = 0x7FFFFFFF
  70. a, b = (int.from_bytes(ab[i:i + 4], "big") % modulus for i in (0, 4))
  71. c, d = (int.from_bytes(cd[i:i + 4], "big") % modulus for i in (0, 4))
  72. accumulator = total = 0
  73. for offset in range(0, 128, 8):
  74. left = int.from_bytes(PLAINTEXT[offset:offset + 4], "big")
  75. right = int.from_bytes(PLAINTEXT[offset + 4:offset + 8], "big")
  76. accumulator = ((accumulator + left * 0xE79A9C1) * a + b) % modulus
  77. total += accumulator
  78. accumulator = ((right + accumulator) * c + d) % modulus
  79. total += accumulator
  80. result = ((accumulator + b) % modulus).to_bytes(4, "big")
  81. result += ((total + d) % modulus).to_bytes(4, "big")
  82. return xor(result, ab)
  83. def packet(payload, magic=b"IL"):
  84. checksum = 0
  85. for value in payload:
  86. checksum ^= value
  87. return magic + b"\0\0" + bytes([len(payload)]) + payload + bytes([checksum])
  88. def transcript(number):
  89. console = bytes(range(0x10 + number * 0x20, 0x18 + number * 0x20))
  90. nonce = bytes(range(number * 0x20, number * 0x20 + 16))
  91. identification = packet(SERIAL + bytes.fromhex("0080025e048e0203000101"), b"IK")
  92. identity = bytearray(32)
  93. identity[:15] = identification[5:20]
  94. identity[16:21] = identification[20:25]
  95. identity[21] = identification[27]
  96. identity[22:24] = identification[25:27]
  97. digest = hashlib.sha1(console).digest()
  98. key1 = crypt(ROOT_23, digest[:16])
  99. key2 = crypt(ROOT_24, digest[4:20])
  100. session1 = crypt(key1, nonce)
  101. session2 = crypt(key2, nonce[8:] + nonce[:8])
  102. encrypted = crypt(KEY_D, nonce + console)
  103. init = packet(encrypted + mac(KEY_E, encrypted)[4:])
  104. encrypted = crypt(session1, RANDOM + nonce)
  105. init_response = packet(encrypted + acr(console, identity,
  106. mac(session2, encrypted)))
  107. # The console recovers the controller nonce and hash from the reply.
  108. recovered = crypt(session1, init_response[5:37], False)
  109. assert recovered[16:] == nonce
  110. controller = recovered[:16]
  111. init_hash = hashlib.sha1(recovered).digest()
  112. salt = bytearray(controller[12:] + nonce[12:] + nonce[8:])
  113. verifies = []
  114. replies = []
  115. for iteration in range(3):
  116. fresh = bytes(range(0x60 + iteration * 8, 0x68 + iteration * 8))
  117. salt[8:] = fresh
  118. encrypted = crypt(controller, fresh)
  119. verifies.append(packet(encrypted + mac(init_hash, encrypted, salt)))
  120. encrypted = crypt(session1, acr(console, identity, fresh))
  121. replies.append(packet(encrypted + mac(session2, encrypted, salt)))
  122. return {"console": console.hex(), "nonce": nonce.hex(),
  123. "identification": identification.hex(), "init": init.hex(),
  124. "init_response": init_response.hex(),
  125. "verify": [v.hex() for v in verifies],
  126. "verify_response": [v.hex() for v in replies]}
  127. def main():
  128. # FIPS DES example, exercised through two-key EDE with identical keys.
  129. key = bytes.fromhex("133457799bbcdff1") * 2
  130. assert crypt(key, bytes.fromhex("0123456789abcdef")).hex() == (
  131. "85e813540f0ab405")
  132. key = bytes(range(16))
  133. salt = bytearray.fromhex("ffffffffffffffff1020304050607080")
  134. salted_mac = mac(key, bytes(range(24)), salt)
  135. fixture = {"consoles": [transcript(0), transcript(1)],
  136. "mac_wrap": salted_mac.hex(), "salt_wrap": salt.hex()}
  137. print(json.dumps(fixture, indent=2))
  138. if __name__ == "__main__":
  139. main()