From 9419605b56480b27c3d098d591871376cf9a42ca Mon Sep 17 00:00:00 2001 From: HashMapsData2Value <83883690+HashMapsData2Value@users.noreply.github.com> Date: Mon, 3 Jun 2024 18:51:56 +0200 Subject: [PATCH] fix: fixes padding error --- .../bip32ed25519/Bip32Ed25519Test.kt | 446 +++-- .../bip32ed25519/Bip32Ed25519Base.kt | 1486 +++++++++-------- .../algorandfoundation/bip32ed25519/utils.kt | 2 +- 3 files changed, 999 insertions(+), 935 deletions(-) diff --git a/jvmModule/src/test/kotlin/com/algorandfoundation/bip32ed25519/Bip32Ed25519Test.kt b/jvmModule/src/test/kotlin/com/algorandfoundation/bip32ed25519/Bip32Ed25519Test.kt index 1e462fa..e1aa931 100644 --- a/jvmModule/src/test/kotlin/com/algorandfoundation/bip32ed25519/Bip32Ed25519Test.kt +++ b/jvmModule/src/test/kotlin/com/algorandfoundation/bip32ed25519/Bip32Ed25519Test.kt @@ -27,6 +27,7 @@ import com.goterl.lazysodium.utils.LibraryLoader import java.util.Base64 import kotlin.collections.component1 import kotlin.test.Test +import kotlin.test.assertContains import kotlin.test.assertNotEquals import net.pwall.json.schema.JSONSchema import org.junit.jupiter.api.BeforeAll @@ -571,126 +572,85 @@ class Bip32Ed25519Test { } @Test - fun derivePubliclyPeikert() { + fun testBIP32DerivationTypeValues() { + val derivationTypes = BIP32DerivationType.values().map { it.name } + assertContains(derivationTypes, "Khovratovich") + assertContains(derivationTypes, "Peikert") + } + + @Test + fun derivePubliclyChildren() { val rootKey = helperStringToByteArray( "168,186,128,2,137,34,217,252,250,5,92,120,174,222,85,181,197,117,188,216,213,165,49,104,237,244,95,54,217,236,143,70,148,89,43,75,200,146,144,117,131,226,38,105,236,223,27,4,9,169,243,189,85,73,242,221,117,27,81,54,9,9,205,5,121,107,146,6,236,48,225,66,233,75,121,10,152,128,91,249,153,4,43,85,4,105,99,23,78,230,206,226,208,55,89,70" ) + val account = 0u + val change = 0u val bip44Path = listOf( Bip32Ed25519Base.harden(44u), Bip32Ed25519Base.harden(283u), - Bip32Ed25519Base.harden(0u), - 0u + Bip32Ed25519Base.harden(account), + 0u, ) - val walletRoot = - c.deriveKey( - rootKey, - bip44Path, - false, - BIP32DerivationType.Peikert - ) - - // should be able to derive all public keys from this root without knowing - // private information - // since these are SOFTLY derived - - val numPublicKeysToDerive = 10 - for (i in 0 until numPublicKeysToDerive) { - // assuming in a third party that only has public information - // I'm provided with the wallet level m'/44'/283'/0'/0 - // root[public,chaincode] - // no private information is shared - // i can SOFTLY derive N public keys / addresses from this root - val derivedKey = - c.deriveChildNodePublic( - walletRoot, - i.toUInt(), - BIP32DerivationType.Peikert - ) - // Deriving from my own wallet where i DO have private information - val myKey = - c.keyGen( - KeyContext.Address, - 0u, - 0u, - i.toUInt(), - BIP32DerivationType.Peikert + for (derivationType in BIP32DerivationType.values()) { + + val walletRoot = + c.deriveKey( + rootKey, + bip44Path, + false, + derivationType ) - // they should match - // derivedKey.subarray(0, 32) == public key (excluding chaincode) + // should be able to derive all public keys from this root without + // knowing + // private information + // since these are SOFTLY derived + + val numPublicKeysToDerive = 10 + for (i in 0 until numPublicKeysToDerive) { + // assuming in a third party that only has public + // information + // I'm provided with the wallet level m'/44'/283'/0'/0 root + // [public, + // chaincode] + // no private information is shared + // i can SOFTLY derive N public keys / addresses from this + // root + val derivedKeyKeyIndex = + c.deriveChildNodePublic( + walletRoot, + i.toUInt(), + derivationType + ) + // Deriving from my own wallet where i DO have private + // information + val myKey = + c.keyGen( + KeyContext.Address, + account, + change, + i.toUInt(), + derivationType + ) - assert(derivedKey.take(32).toByteArray().contentEquals(myKey)) + // they should match + // derivedKey.subarray(0, 32) == public key (excluding + // chaincode) + assert( + derivedKeyKeyIndex + .take(32) + .toByteArray() + .contentEquals(myKey) + ) { + "At index ${i} derivedKey and myKey are not equal for derivation type ${derivationType}" + } + } } } - /* - @Test - fun derivePubliclyKhovratovich() { - - val rootKey = - helperStringToByteArray( - "168,186,128,2,137,34,217,252,250,5,92,120,174,222,85,181,197,117,188,216,213,165,49,104,237,244,95,54,217,236,143,70,148,89,43,75,200,146,144,117,131,226,38,105,236,223,27,4,9,169,243,189,85,73,242,221,117,27,81,54,9,9,205,5,121,107,146,6,236,48,225,66,233,75,121,10,152,128,91,249,153,4,43,85,4,105,99,23,78,230,206,226,208,55,89,70" - ) - val account = 0u - val change = 0u - val bip44Path = - listOf( - Bip32Ed25519Base.harden(44u), - Bip32Ed25519Base.harden(283u), - Bip32Ed25519Base.harden(account), - change - ) - - val walletRoot = - c.deriveKey( - rootKey, - bip44Path, - false, - BIP32DerivationType.Khovratovich - ) - - // should be able to derive all public keys from this root without knowing - // private information - // since these are SOFTLY derived - - val numPublicKeysToDerive = 10 - for (i in 0 until numPublicKeysToDerive) { - // assuming in a third party that only has public information - // I'm provided with the wallet level m'/44'/283'/0'/0 root [public, - // chaincode] - // no private information is shared - // i can SOFTLY derive N public keys / addresses from this root - val derivedKey = - c.deriveChildNodePublic( - walletRoot, - i.toUInt(), - BIP32DerivationType.Khovratovich - ) - // Deriving from my own wallet where i DO have private information - val myKey = - c.keyGen( - KeyContext.Address, - account, - change, - i.toUInt(), - BIP32DerivationType.Khovratovich - ) - - // they should match - // derivedKey.subarray(0, 32) == public key (excluding chaincode) - println("derivedKey: ${derivedKey.contentToString()}") - println("myKey: ${myKey.contentToString()}") - println(derivedKey.take(32).toByteArray().contentEquals(myKey)) - println("At index ${i}") - assert(derivedKey.take(32).toByteArray().contentEquals(myKey)) { - "At index ${i} derivedKey and myKey are not equal" - } - } - } - */ @Test fun fromSeedBip39Test() { @@ -737,13 +697,13 @@ class Bip32Ed25519Test { fun validateNonceDataTest() { val challenge = """ - { - "0": 28, "1": 103, "2": 26, "3": 222, "4": 7, "5": 86, "6": 55, "7": 95, - "8": 197, "9": 179, "10": 249, "11": 252, "12": 232, "13": 252, "14": 176, - "15": 39, "16": 112, "17": 131, "18": 52, "19": 63, "20": 212, "21": 58, - "22": 226, "23": 89, "24": 64, "25": 94, "26": 23, "27": 91, "28": 128, - "29": 143, "30": 123, "31": 27 - }""".trimIndent() + { + "0": 28, "1": 103, "2": 26, "3": 222, "4": 7, "5": 86, "6": 55, "7": 95, + "8": 197, "9": 179, "10": 249, "11": 252, "12": 232, "13": 252, "14": 176, + "15": 39, "16": 112, "17": 131, "18": 52, "19": 63, "20": 212, "21": 58, + "22": 226, "23": 89, "24": 64, "25": 94, "26": 23, "27": 91, "28": 128, + "29": 143, "30": 123, "31": 27 + }""".trimIndent() val authSchema = JSONSchema.parseFile("src/test/resources/auth.request.json") @@ -756,13 +716,13 @@ class Bip32Ed25519Test { fun validateNonceDataBase64Test() { val challenge = """ - { - "0": 28, "1": 103, "2": 26, "3": 222, "4": 7, "5": 86, "6": 55, "7": 95, - "8": 197, "9": 179, "10": 249, "11": 252, "12": 232, "13": 252, "14": 176, - "15": 39, "16": 112, "17": 131, "18": 52, "19": 63, "20": 212, "21": 58, - "22": 226, "23": 89, "24": 64, "25": 94, "26": 23, "27": 91, "28": 128, - "29": 143, "30": 123, "31": 27 - }""".trimIndent() + { + "0": 28, "1": 103, "2": 26, "3": 222, "4": 7, "5": 86, "6": 55, "7": 95, + "8": 197, "9": 179, "10": 249, "11": 252, "12": 232, "13": 252, "14": 176, + "15": 39, "16": 112, "17": 131, "18": 52, "19": 63, "20": 212, "21": 58, + "22": 226, "23": 89, "24": 64, "25": 94, "26": 23, "27": 91, "28": 128, + "29": 143, "30": 123, "31": 27 + }""".trimIndent() val authSchema = JSONSchema.parseFile("src/test/resources/auth.request.json") @@ -814,13 +774,13 @@ class Bip32Ed25519Test { // make one value larger than 255, the max according to the schema val challenge = """ - { - "0": 256, "1": 103, "2": 26, "3": 222, "4": 7, "5": 86, "6": 55, "7": 95, - "8": 197, "9": 179, "10": 249, "11": 252, "12": 232, "13": 252, "14": 176, - "15": 39, "16": 112, "17": 131, "18": 52, "19": 63, "20": 212, "21": 58, - "22": 226, "23": 89, "24": 64, "25": 94, "26": 23, "27": 91, "28": 128, - "29": 143, "30": 123, "31": 27 - }""".trimIndent() + { + "0": 256, "1": 103, "2": 26, "3": 222, "4": 7, "5": 86, "6": 55, "7": 95, + "8": 197, "9": 179, "10": 249, "11": 252, "12": 232, "13": 252, "14": 176, + "15": 39, "16": 112, "17": 131, "18": 52, "19": 63, "20": 212, "21": 58, + "22": 226, "23": 89, "24": 64, "25": 94, "26": 23, "27": 91, "28": 128, + "29": 143, "30": 123, "31": 27 + }""".trimIndent() val authSchema = JSONSchema.parseFile("src/test/resources/auth.request.json") @@ -839,16 +799,16 @@ class Bip32Ed25519Test { val message = """{"text":"Hello, World!"}""" val jsonSchema = """ - { - "type": "object", - "properties": { - "text": { - "type": "string" - } - }, - "required": ["text"] - } - """.trimIndent() + { + "type": "object", + "properties": { + "text": { + "type": "string" + } + }, + "required": ["text"] + } + """.trimIndent() val msgSchema = JSONSchema.parse(jsonSchema) @@ -874,16 +834,16 @@ class Bip32Ed25519Test { val message = """{"sentence":"Hello, World!"}""" val jsonSchema = """ - { - "type": "object", - "properties": { - "text": { - "type": "string" - } - }, - "required": ["text"] - } - """.trimIndent() + { + "type": "object", + "properties": { + "text": { + "type": "string" + } + }, + "required": ["text"] + } + """.trimIndent() val msgSchema = JSONSchema.parse(jsonSchema) @@ -902,19 +862,19 @@ class Bip32Ed25519Test { val message = """{"text":"Hello, World!"}""" val jsonSchema = """ - { - "type": "object", - "properties": { - "text": { - "type": "string" - }, - "i": { - "type": "integer" - } - }, - "required": ["i"] - } - """.trimIndent() + { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "i": { + "type": "integer" + } + }, + "required": ["i"] + } + """.trimIndent() val msgSchema = JSONSchema.parse(jsonSchema) @@ -934,20 +894,20 @@ class Bip32Ed25519Test { """{"text":"Hello World", "i": 10, "extra0": "test", "extra1": "test", "extra2": "test"}""" val jsonSchema = """ - { - "type": "object", - "properties": { - "text": { - "type": "string" - }, - "i": { - "type": "integer" - } - }, - "required": ["text", "i"], - "additionalProperties": false - } - """.trimIndent() + { + "type": "object", + "properties": { + "text": { + "type": "string" + }, + "i": { + "type": "integer" + } + }, + "required": ["text", "i"], + "additionalProperties": false + } + """.trimIndent() val msgSchema = JSONSchema.parse(jsonSchema) @@ -966,16 +926,16 @@ class Bip32Ed25519Test { val message = helperHexStringToByteArray(msgPackData) val jsonSchema = """ - { - "type": "object", - "properties": { - "text": { - "type": "string" - } - }, - "required": ["text"] - } - """.trimIndent() + { + "type": "object", + "properties": { + "text": { + "type": "string" + } + }, + "required": ["text"] + } + """.trimIndent() val msgSchema = JSONSchema.parse(jsonSchema) val metadata = SignMetadata(Encoding.MSGPACK, msgSchema) val valid = Bip32Ed25519Base.validateData(message, metadata) @@ -989,16 +949,16 @@ class Bip32Ed25519Test { val message = helperHexStringToByteArray(msgPackData) val jsonSchema = """ - { - "type": "object", - "properties": { - "num": { - "type": "integer" - } - }, - "required": ["num"] - } - """.trimIndent() + { + "type": "object", + "properties": { + "num": { + "type": "integer" + } + }, + "required": ["num"] + } + """.trimIndent() val msgSchema = JSONSchema.parse(jsonSchema) val metadata = SignMetadata(Encoding.MSGPACK, msgSchema) val valid = Bip32Ed25519Base.validateData(message, metadata) @@ -1013,16 +973,16 @@ class Bip32Ed25519Test { val message = helperHexStringToByteArray(msgPackData) val jsonSchema = """ - { - "type": "object", - "properties": { - "text": { - "type": "integer" - } - }, - "required": ["text"] - } - """.trimIndent() + { + "type": "object", + "properties": { + "text": { + "type": "integer" + } + }, + "required": ["text"] + } + """.trimIndent() val msgSchema = JSONSchema.parse(jsonSchema) val metadata = SignMetadata(Encoding.MSGPACK, msgSchema) val valid = Bip32Ed25519Base.validateData(message, metadata) @@ -1083,16 +1043,16 @@ class Bip32Ed25519Test { val message = """{"text":"Hello, World!"}""".toByteArray() val jsonSchema = """ - { - "type": "object", - "properties": { - "text": { - "type": "string" - } - }, - "required": ["text"] - } - """.trimIndent() + { + "type": "object", + "properties": { + "text": { + "type": "string" + } + }, + "required": ["text"] + } + """.trimIndent() val metadata = SignMetadata(Encoding.NONE, JSONSchema.parse(jsonSchema)) val valid = Bip32Ed25519Base.validateData(message, metadata) assert(valid) { "validation failed, message not in line with schema" } @@ -1187,13 +1147,13 @@ class Bip32Ed25519Test { val data = """ - { - "0": 255, "1": 103, "2": 26, "3": 222, "4": 7, "5": 86, "6": 55, "7": 95, - "8": 197, "9": 179, "10": 249, "11": 252, "12": 232, "13": 252, "14": 176, - "15": 39, "16": 112, "17": 131, "18": 52, "19": 63, "20": 212, "21": 58, - "22": 226, "23": 89, "24": 64, "25": 94, "26": 23, "27": 91, "28": 128, - "29": 143, "30": 123, "31": 27 - }""" + { + "0": 255, "1": 103, "2": 26, "3": 222, "4": 7, "5": 86, "6": 55, "7": 95, + "8": 197, "9": 179, "10": 249, "11": 252, "12": 232, "13": 252, "14": 176, + "15": 39, "16": 112, "17": 131, "18": 52, "19": 63, "20": 212, "21": 58, + "22": 226, "23": 89, "24": 64, "25": 94, "26": 23, "27": 91, "28": 128, + "29": 143, "30": 123, "31": 27 + }""" .trimIndent() .toByteArray() @@ -1243,13 +1203,13 @@ class Bip32Ed25519Test { val dataRaw = """ - { - "0": 255, "1": 103, "2": 26, "3": 222, "4": 7, "5": 86, "6": 55, "7": 95, - "8": 197, "9": 179, "10": 249, "11": 252, "12": 232, "13": 252, "14": 176, - "15": 39, "16": 112, "17": 131, "18": 52, "19": 63, "20": 212, "21": 58, - "22": 226, "23": 89, "24": 64, "25": 94, "26": 23, "27": 91, "28": 128, - "29": 143, "30": 123, "31": 27 - }""" + { + "0": 255, "1": 103, "2": 26, "3": 222, "4": 7, "5": 86, "6": 55, "7": 95, + "8": 197, "9": 179, "10": 249, "11": 252, "12": 232, "13": 252, "14": 176, + "15": 39, "16": 112, "17": 131, "18": 52, "19": 63, "20": 212, "21": 58, + "22": 226, "23": 89, "24": 64, "25": 94, "26": 23, "27": 91, "28": 128, + "29": 143, "30": 123, "31": 27 + }""" .trimIndent() .toByteArray() @@ -1300,13 +1260,13 @@ class Bip32Ed25519Test { val dataRaw = """ - { - "0": 255, "1": 103, "2": 26, "3": 222, "4": 7, "5": 86, "6": 55, "7": 95, - "8": 197, "9": 179, "10": 249, "11": 252, "12": 232, "13": 252, "14": 176, - "15": 39, "16": 112, "17": 131, "18": 52, "19": 63, "20": 212, "21": 58, - "22": 226, "23": 89, "24": 64, "25": 94, "26": 23, "27": 91, "28": 128, - "29": 143, "30": 123, "31": 27 - }""" + { + "0": 255, "1": 103, "2": 26, "3": 222, "4": 7, "5": 86, "6": 55, "7": 95, + "8": 197, "9": 179, "10": 249, "11": 252, "12": 232, "13": 252, "14": 176, + "15": 39, "16": 112, "17": 131, "18": 52, "19": 63, "20": 212, "21": 58, + "22": 226, "23": 89, "24": 64, "25": 94, "26": 23, "27": 91, "28": 128, + "29": 143, "30": 123, "31": 27 + }""" .trimIndent() .toByteArray() @@ -1484,16 +1444,16 @@ class Bip32Ed25519Test { val message = """{"text":"Hello, World!"}""".toByteArray() val jsonSchema = """ - { - "type": "object", - "properties": { - "text": { - "type": "string" - } - }, - "required": ["text"] - } - """.trimIndent() + { + "type": "object", + "properties": { + "text": { + "type": "string" + } + }, + "required": ["text"] + } + """.trimIndent() val metadata = SignMetadata(Encoding.NONE, JSONSchema.parse(jsonSchema)) val pk = diff --git a/sharedModule/src/main/kotlin/com/algorandfoundation/bip32ed25519/Bip32Ed25519Base.kt b/sharedModule/src/main/kotlin/com/algorandfoundation/bip32ed25519/Bip32Ed25519Base.kt index f364a85..65b8aea 100644 --- a/sharedModule/src/main/kotlin/com/algorandfoundation/bip32ed25519/Bip32Ed25519Base.kt +++ b/sharedModule/src/main/kotlin/com/algorandfoundation/bip32ed25519/Bip32Ed25519Base.kt @@ -35,737 +35,841 @@ val ED25519_POINT_SIZE = 32 val INDEX_SIZE = 4 abstract class Bip32Ed25519Base(private var seed: ByteArray) { - abstract val lazySodium: LazySodium - companion object { - val prefixes = - listOf( - "appID", - "arc", - "aB", - "aD", - "aO", - "aP", - "aS", - "AS", - "BH", - "B256", - "BR", - "CR", - "GE", - "KP", - "MA", - "MB", - "MX", - "NIC", - "NIR", - "NIV", - "NPR", - "OT1", - "OT2", - "PF", - "PL", - "Program", - "ProgData", - "PS", - "PK", - "SD", - "SpecialAddr", - "STIB", - "spc", - "spm", - "spp", - "sps", - "spv", - "TE", - "TG", - "TL", - "TX", - "VO" - ) + abstract val lazySodium: LazySodium + companion object { + val prefixes = + listOf( + "appID", + "arc", + "aB", + "aD", + "aO", + "aP", + "aS", + "AS", + "BH", + "B256", + "BR", + "CR", + "GE", + "KP", + "MA", + "MB", + "MX", + "NIC", + "NIR", + "NIV", + "NPR", + "OT1", + "OT2", + "PF", + "PL", + "Program", + "ProgData", + "PS", + "PK", + "SD", + "SpecialAddr", + "STIB", + "spc", + "spm", + "spp", + "sps", + "spv", + "TE", + "TG", + "TL", + "TX", + "VO" + ) + + /** + * Harden a number (set the highest bit to 1) Note that the input is UInt and the + * output is also UInt + * + * @param num + * @returns + */ + fun harden(num: UInt): UInt = 0x80000000.toUInt() + num + + /* + * Get the BIP44 path from the context, account and keyIndex + * + * @param context + * @param account + * @param keyIndex + * @returns + */ + + fun getBIP44PathFromContext( + context: KeyContext, + account: UInt, + change: UInt, + keyIndex: UInt + ): List { + return when (context) { + KeyContext.Address -> + listOf( + harden(44u), + harden(283u), + harden(account), + change, + keyIndex + ) + KeyContext.Identity -> + listOf( + harden(44u), + harden(0u), + harden(account), + change, + keyIndex + ) + } + } + + /** + * Implementation how to validate data with encoding and schema, using base64 as an + * example + * + * @param message + * @param metadata + * @returns + */ + fun validateData(message: ByteArray, metadata: SignMetadata): Boolean { + // Check for Algorand tags + if (hasAlgorandTags(message)) { + throw DataValidationException("Data contains Algorand tags") + } + + val decoded: ByteArray = + when (metadata.encoding) { + Encoding.BASE64 -> + Base64.getDecoder().decode(message) + // Encoding.CBOR -> // CBOR is not yet supported + // across all platforms + // ObjectMapper() + // .writeValueAsString( + // + // CBORMapper().readValue(message, Map::class.java) + // ) + // .toByteArray() + Encoding.MSGPACK -> + ObjectMapper().writeValueAsString( + ObjectMapper( + MessagePackFactory() + ) + .readValue( + message, + Map::class.java + ) + ) + .toByteArray() + Encoding.NONE -> message + } + + // Validate with schema + try { + return metadata.schema.validateBasic(String(decoded)).valid + } catch (e: Exception) { + return false + } + } + + fun hasAlgorandTags(message: ByteArray): Boolean { + // Prefixes taken from go-algorand node software code + // https://github.com/algorand/go-algorand/blob/master/protocol/hash.go + + val messageString = String(message) + return prefixes.any { messageString.startsWith(it) } + } + + /** + * Reference of BIP32-Ed25519 Hierarchical Deterministic Keys over a Non-linear + * Keyspace + * + * @see section V. BIP32-Ed25519: Specification; + * + * A) Root keys + * + * @param seed + * - 256 bite seed generated from BIP39 Mnemonic + * @returns + * - Extended root key (kL, kR, c) where kL is the left 32 bytes of the root key, kR + * is the right 32 bytes of the root key, and c is the chain code. Total 96 bytes + */ + fun fromSeed(seed: ByteArray): ByteArray { + // k = H512(seed) + var k = MessageDigest.getInstance("SHA-512").digest(seed) + var kL = k.sliceArray(0 until ED25519_SCALAR_SIZE) + var kR = k.sliceArray(ED25519_SCALAR_SIZE until 2 * ED25519_SCALAR_SIZE) + + // While the third highest bit of the last byte of kL is not zero + while (kL[31].toInt() and 0b00100000 != 0) { + val hmac = Mac.getInstance("HmacSHA512") + hmac.init(SecretKeySpec(kL, "HmacSHA512")) + k = hmac.doFinal(kR) + kL = k.sliceArray(0 until ED25519_SCALAR_SIZE) + kR = k.sliceArray(ED25519_SCALAR_SIZE until 2 * ED25519_SCALAR_SIZE) + } + + // clamp + // Set the bits in kL as follows: + // little Endianess + kL[0] = + (kL[0].toInt() and 0b11111000) + .toByte() // the lowest 3 bits of the first + // byte of kL are cleared + kL[31] = + (kL[31].toInt() and 0b01111111) + .toByte() // the highest bit of the last + // byte is cleared + kL[31] = + (kL[31].toInt() or 0b01000000) + .toByte() // the second highest bit of the + // last byte is set + + // chain root code + // SHA256(0x01||k) + val c = + MessageDigest.getInstance("SHA-256") + .digest(byteArrayOf(0x01) + seed) + return kL + kR + c + } + } /** - * Harden a number (set the highest bit to 1) Note that the input is UInt and the output is - * also UInt * - * @param num + * @see section V. BIP32-Ed25519: Specification + * + * @param kl + * - The scalar + * @param cc + * - chain code + * @param index + * - non-hardened ( < 2^31 ) index * @returns + * - (z, c) where z is the 64-byte child key and c is the chain code */ - fun harden(num: UInt): UInt = 0x80000000.toUInt() + num + internal fun deriveNonHardened( + kl: ByteArray, + cc: ByteArray, + index: UInt + ): Pair { + val data = ByteBuffer.allocate(1 + ED25519_SCALAR_SIZE + INDEX_SIZE) + data.put(1 + ED25519_SCALAR_SIZE, index.toByte()) + + val pk = lazySodium.cryptoScalarMultEd25519BaseNoclamp(kl).toBytes() + data.position(1) + data.put(pk) + + data.put(0, 0x02) + val hmac = Mac.getInstance("HmacSHA512") + hmac.init(SecretKeySpec(cc, "HmacSHA512")) + val z = hmac.doFinal(data.array()) + + data.put(0, 0x03) + hmac.init(SecretKeySpec(cc, "HmacSHA512")) + val fullChildChainCode = hmac.doFinal(data.array()) + val childChainCode = + fullChildChainCode.sliceArray( + CHAIN_CODE_SIZE until 2 * CHAIN_CODE_SIZE + ) + + return Pair(z, childChainCode) + } - /* - * Get the BIP44 path from the context, account and keyIndex + /** * - * @param context - * @param account - * @param keyIndex + * @see section V. BIP32-Ed25519: Specification + * + * @param kl + * - The scalar (a.k.a private key) + * @param kr + * - the right 32 bytes of the root key + * @param cc + * - chain code + * @param index + * - hardened ( >= 2^31 ) index * @returns + * - (z, c) where z is the 64-byte child key and c is the chain code */ - - fun getBIP44PathFromContext( - context: KeyContext, - account: UInt, - change: UInt, - keyIndex: UInt - ): List { - return when (context) { - KeyContext.Address -> - listOf(harden(44u), harden(283u), harden(account), change, keyIndex) - KeyContext.Identity -> - listOf(harden(44u), harden(0u), harden(account), change, keyIndex) - } + internal fun deriveHardened( + kl: ByteArray, + kr: ByteArray, + cc: ByteArray, + index: UInt + ): Pair { + val indexLEBytes = ByteArray(4) { i -> ((index shr (8 * i)) and 0xFFu).toByte() } + val data = ByteBuffer.allocate(1 + 2 * ED25519_SCALAR_SIZE + INDEX_SIZE) + data.position(1 + 2 * ED25519_SCALAR_SIZE) + data.put(indexLEBytes) + data.position(1) + data.put(kl) + data.put(kr) + + data.put(0, 0x00) + val hmac = Mac.getInstance("HmacSHA512") + hmac.init(SecretKeySpec(cc, "HmacSHA512")) + val z = hmac.doFinal(data.array()) + + data.put(0, 0x01) + hmac.init(SecretKeySpec(cc, "HmacSHA512")) + val fullChildChainCode = hmac.doFinal(data.array()) + val childChainCode = + fullChildChainCode.sliceArray( + CHAIN_CODE_SIZE until 2 * CHAIN_CODE_SIZE + ) + + return Pair(z, childChainCode) } /** - * Implementation how to validate data with encoding and schema, using base64 as an example + * @see section V. BIP32-Ed25519: Specification; * - * @param message - * @param metadata + * subsections: + * + * B) Child Keys and C) Private Child Key Derivation + * + * @param extendedKey + * - extended key (kL, kR, c) where kL is the left 32 bytes of the root key the scalar + * (pvtKey). kR is the right 32 bytes of the root key, and c is the chain code. Total 96 + * bytes + * @param index + * - index of the child key * @returns + * - (kL, kR, c) where kL is the left 32 bytes of the child key (the new scalar), kR is the + * right 32 bytes of the child key, and c is the chain code. Total 96 bytes */ - fun validateData(message: ByteArray, metadata: SignMetadata): Boolean { - // Check for Algorand tags - if (hasAlgorandTags(message)) { - throw DataValidationException("Data contains Algorand tags") - } - - val decoded: ByteArray = - when (metadata.encoding) { - Encoding.BASE64 -> Base64.getDecoder().decode(message) - // Encoding.CBOR -> // CBOR is not yet supported across all platforms - // ObjectMapper() - // .writeValueAsString( - // CBORMapper().readValue(message, Map::class.java) - // ) - // .toByteArray() - Encoding.MSGPACK -> - ObjectMapper() - .writeValueAsString( - ObjectMapper(MessagePackFactory()) - .readValue(message, Map::class.java) - ) - .toByteArray() - Encoding.NONE -> message - } - - // Validate with schema - try { - return metadata.schema.validateBasic(String(decoded)).valid - } catch (e: Exception) { - return false - } - } - - fun hasAlgorandTags(message: ByteArray): Boolean { - // Prefixes taken from go-algorand node software code - // https://github.com/algorand/go-algorand/blob/master/protocol/hash.go - - val messageString = String(message) - return prefixes.any { messageString.startsWith(it) } + public fun deriveChildNodePrivate( + extendedKey: ByteArray, + index: UInt, + derivationType: BIP32DerivationType = BIP32DerivationType.Peikert + ): ByteArray { + val kl = extendedKey.sliceArray(0 until ED25519_SCALAR_SIZE) + val kr = extendedKey.sliceArray(ED25519_SCALAR_SIZE until 2 * ED25519_SCALAR_SIZE) + val cc = + extendedKey.sliceArray( + 2 * ED25519_SCALAR_SIZE until + 2 * ED25519_SCALAR_SIZE + + CHAIN_CODE_SIZE + ) + + val (z, chainCode) = + if (index < 0x80000000.toUInt()) deriveNonHardened(kl, cc, index) + else deriveHardened(kl, kr, cc, index) + + val zl = z.sliceArray(0 until ED25519_SCALAR_SIZE) + val zr = z.sliceArray(ED25519_SCALAR_SIZE until 2 * ED25519_SCALAR_SIZE) + + // left = kl + 8 * trunc28(zl) + // right = zr + kr + val left = + (BigInteger(1, kl.reversedArray()) + + BigInteger( + 1, + trunc256MinusGBits( + zl.clone(), + derivationType.value + ) + .reversedArray() + ) * BigInteger.valueOf(8L)) + .toByteArray() + .reversedArray() + .let { bytes -> + ByteArray( + ED25519_SCALAR_SIZE - + bytes.size + ) + bytes + } // Pad to 32 bytes + + var right = + (BigInteger(1, kr.reversedArray()) + + BigInteger(1, zr.reversedArray())) + .toByteArray() + .reversedArray() + .let { bytes -> + bytes.sliceArray( + 0 until + minOf( + bytes.size, + ED25519_SCALAR_SIZE + ) + ) + } // Slice to 32 bytes + + right = right + ByteArray(ED25519_SCALAR_SIZE - right.size) + + return ByteBuffer.allocate( + ED25519_SCALAR_SIZE + + ED25519_SCALAR_SIZE + + CHAIN_CODE_SIZE + ) + .put(left) + .put(right) + .put(chainCode) + .array() } /** - * Reference of BIP32-Ed25519 Hierarchical Deterministic Keys over a Non-linear Keyspace + * * @see section V. BIP32-Ed25519: Specification; * - * @see section V. BIP32-Ed25519: Specification; + * subsections: * - * A) Root keys + * D) Public Child key * - * @param seed - * - 256 bite seed generated from BIP39 Mnemonic + * @param extendedKey + * - extend public key (p, c) where p is the public key and c is the chain code. Total 64 + * bytes + * @param index + * - unharden index (i < 2^31) of the child key + * @param g + * - Defines how many bits to zero in the left 32 bytes of the child key. Standard + * BIP32-ed25519 derivations use 32 bits. * @returns - * - Extended root key (kL, kR, c) where kL is the left 32 bytes of the root key, kR is the - * right 32 bytes of the root key, and c is the chain code. Total 96 bytes + * - 64 bytes, being the 32 bytes of the child key (the new public key) followed by the 32 + * bytes of the chain code */ - fun fromSeed(seed: ByteArray): ByteArray { - // k = H512(seed) - var k = MessageDigest.getInstance("SHA-512").digest(seed) - var kL = k.sliceArray(0 until ED25519_SCALAR_SIZE) - var kR = k.sliceArray(ED25519_SCALAR_SIZE until 2 * ED25519_SCALAR_SIZE) - - // While the third highest bit of the last byte of kL is not zero - while (kL[31].toInt() and 0b00100000 != 0) { + public fun deriveChildNodePublic( + extendedKey: ByteArray, + index: UInt, + derivationType: BIP32DerivationType = BIP32DerivationType.Peikert + ): ByteArray { + if (index > 0x80000000u) + throw IllegalArgumentException( + "Cannot derive public key with hardened index" + ) + + println("derivationType: $derivationType") + println("Extended Key: ${extendedKey.contentToString()}") + println("Index: $index") + val pk = extendedKey.sliceArray(0 until ED25519_POINT_SIZE) + val cc = + extendedKey.sliceArray( + ED25519_POINT_SIZE until + ED25519_POINT_SIZE + CHAIN_CODE_SIZE + ) + println("Public Key: ${pk.contentToString()}") + println("Chain Code: ${cc.contentToString()}") + // Step 1: Compute Z + val data = ByteBuffer.allocate(1 + ED25519_SCALAR_SIZE + 4) + data.put(1 + ED25519_SCALAR_SIZE, index.toByte()) + + data.position(1) + data.put(pk) + + data.put(0, 0x02) val hmac = Mac.getInstance("HmacSHA512") - hmac.init(SecretKeySpec(kL, "HmacSHA512")) - k = hmac.doFinal(kR) - kL = k.sliceArray(0 until ED25519_SCALAR_SIZE) - kR = k.sliceArray(ED25519_SCALAR_SIZE until 2 * ED25519_SCALAR_SIZE) - } - - // clamp - // Set the bits in kL as follows: - // little Endianess - kL[0] = - (kL[0].toInt() and 0b11111000) - .toByte() // the lowest 3 bits of the first byte of kL are cleared - kL[31] = - (kL[31].toInt() and 0b01111111) - .toByte() // the highest bit of the last byte is cleared - kL[31] = - (kL[31].toInt() or 0b01000000) - .toByte() // the second highest bit of the last byte is set - - // chain root code - // SHA256(0x01||k) - val c = MessageDigest.getInstance("SHA-256").digest(byteArrayOf(0x01) + seed) - return kL + kR + c - } - } - - /** - * - * @see section V. BIP32-Ed25519: Specification - * - * @param kl - * - The scalar - * @param cc - * - chain code - * @param index - * - non-hardened ( < 2^31 ) index - * @returns - * - (z, c) where z is the 64-byte child key and c is the chain code - */ - internal fun deriveNonHardened( - kl: ByteArray, - cc: ByteArray, - index: UInt - ): Pair { - val data = ByteBuffer.allocate(1 + ED25519_SCALAR_SIZE + INDEX_SIZE) - data.put(1 + ED25519_SCALAR_SIZE, index.toByte()) - - val pk = lazySodium.cryptoScalarMultEd25519BaseNoclamp(kl).toBytes() - data.position(1) - data.put(pk) - - data.put(0, 0x02) - val hmac = Mac.getInstance("HmacSHA512") - hmac.init(SecretKeySpec(cc, "HmacSHA512")) - val z = hmac.doFinal(data.array()) - - data.put(0, 0x03) - hmac.init(SecretKeySpec(cc, "HmacSHA512")) - val fullChildChainCode = hmac.doFinal(data.array()) - val childChainCode = - fullChildChainCode.sliceArray(CHAIN_CODE_SIZE until 2 * CHAIN_CODE_SIZE) - - return Pair(z, childChainCode) - } - - /** - * - * @see section V. BIP32-Ed25519: Specification - * - * @param kl - * - The scalar (a.k.a private key) - * @param kr - * - the right 32 bytes of the root key - * @param cc - * - chain code - * @param index - * - hardened ( >= 2^31 ) index - * @returns - * - (z, c) where z is the 64-byte child key and c is the chain code - */ - internal fun deriveHardened( - kl: ByteArray, - kr: ByteArray, - cc: ByteArray, - index: UInt - ): Pair { - val indexLEBytes = ByteArray(4) { i -> ((index shr (8 * i)) and 0xFFu).toByte() } - val data = ByteBuffer.allocate(1 + 2 * ED25519_SCALAR_SIZE + INDEX_SIZE) - data.position(1 + 2 * ED25519_SCALAR_SIZE) - data.put(indexLEBytes) - data.position(1) - data.put(kl) - data.put(kr) - - data.put(0, 0x00) - val hmac = Mac.getInstance("HmacSHA512") - hmac.init(SecretKeySpec(cc, "HmacSHA512")) - val z = hmac.doFinal(data.array()) - - data.put(0, 0x01) - hmac.init(SecretKeySpec(cc, "HmacSHA512")) - val fullChildChainCode = hmac.doFinal(data.array()) - val childChainCode = - fullChildChainCode.sliceArray(CHAIN_CODE_SIZE until 2 * CHAIN_CODE_SIZE) - - return Pair(z, childChainCode) - } - - /** - * @see section V. BIP32-Ed25519: Specification; - * - * subsections: - * - * B) Child Keys and C) Private Child Key Derivation - * - * @param extendedKey - * - extended key (kL, kR, c) where kL is the left 32 bytes of the root key the scalar (pvtKey). - * kR is the right 32 bytes of the root key, and c is the chain code. Total 96 bytes - * @param index - * - index of the child key - * @returns - * - (kL, kR, c) where kL is the left 32 bytes of the child key (the new scalar), kR is the - * right 32 bytes of the child key, and c is the chain code. Total 96 bytes - */ - public fun deriveChildNodePrivate( - extendedKey: ByteArray, - index: UInt, - derivationType: BIP32DerivationType = BIP32DerivationType.Peikert - ): ByteArray { - val kl = extendedKey.sliceArray(0 until ED25519_SCALAR_SIZE) - val kr = extendedKey.sliceArray(ED25519_SCALAR_SIZE until 2 * ED25519_SCALAR_SIZE) - val cc = - extendedKey.sliceArray( - 2 * ED25519_SCALAR_SIZE until 2 * ED25519_SCALAR_SIZE + CHAIN_CODE_SIZE - ) - - val (z, chainCode) = - if (index < 0x80000000.toUInt()) deriveNonHardened(kl, cc, index) - else deriveHardened(kl, kr, cc, index) - - val zl = z.sliceArray(0 until ED25519_SCALAR_SIZE) - val zr = z.sliceArray(ED25519_SCALAR_SIZE until 2 * ED25519_SCALAR_SIZE) - - // left = kl + 8 * trunc28(zl) - // right = zr + kr - val left = - (BigInteger(1, kl.reversedArray()) + - BigInteger( - 1, - trunc256MinusGBits(zl.clone(), derivationType.value) + hmac.init(SecretKeySpec(cc, "HmacSHA512")) + val z = hmac.doFinal(data.array()) + val zl = + trunc256MinusGBits( + z.sliceArray(0 until ED25519_SCALAR_SIZE), + derivationType.value + ) + println("Z: ${z.contentToString()}") + println("ZL: ${zl.contentToString()}") + + // Step 2: Compute child PK + + /* + ###################################### + Standard BIP32-ed25519 derivation + ####################################### + zL = 8 * 28bytesOf(z_left_hand_side) + + ###################################### + Chris Peikert's ammendment to BIP32-ed25519 derivation + ####################################### + zL = 8 * trunc_256_minus_g_bits (z_left_hand_side, g) + */ + + val left = + (BigInteger(1, zl.reversedArray()) * BigInteger.valueOf(8L)) + .toByteArray() .reversedArray() - ) * BigInteger.valueOf(8L)) - .toByteArray() - .reversedArray() - .let { bytes -> - ByteArray(ED25519_SCALAR_SIZE - bytes.size) + bytes - } // Pad to 32 bytes - - var right = - (BigInteger(1, kr.reversedArray()) + BigInteger(1, zr.reversedArray())) - .toByteArray() - .reversedArray() - .let { bytes -> - bytes.sliceArray(0 until minOf(bytes.size, ED25519_SCALAR_SIZE)) - } // Slice to 32 bytes - - right = right + ByteArray(ED25519_SCALAR_SIZE - right.size) - - return ByteBuffer.allocate(ED25519_SCALAR_SIZE + ED25519_SCALAR_SIZE + CHAIN_CODE_SIZE) - .put(left) - .put(right) - .put(chainCode) - .array() - } - - /** - * * @see section V. BIP32-Ed25519: Specification; - * - * subsections: - * - * D) Public Child key - * - * @param extendedKey - * - extend public key (p, c) where p is the public key and c is the chain code. Total 64 bytes - * @param index - * - unharden index (i < 2^31) of the child key - * @param g - * - Defines how many bits to zero in the left 32 bytes of the child key. Standard BIP32-ed25519 - * derivations use 32 bits. - * @returns - * - 64 bytes, being the 32 bytes of the child key (the new public key) followed by the 32 bytes - * of the chain code - */ - public fun deriveChildNodePublic( - extendedKey: ByteArray, - index: UInt, - derivationType: BIP32DerivationType = BIP32DerivationType.Peikert - ): ByteArray { - if (index > 0x80000000u) - throw IllegalArgumentException("Cannot derive public key with hardened index") - - val pk = extendedKey.sliceArray(0 until ED25519_POINT_SIZE) - val cc = - extendedKey.sliceArray( - ED25519_POINT_SIZE until ED25519_POINT_SIZE + CHAIN_CODE_SIZE - ) - - // Step 1: Compute Z - val data = ByteBuffer.allocate(1 + ED25519_SCALAR_SIZE + 4) - data.put(1 + ED25519_SCALAR_SIZE, index.toByte()) - - data.position(1) - data.put(pk) - - data.put(0, 0x02) - val hmac = Mac.getInstance("HmacSHA512") - hmac.init(SecretKeySpec(cc, "HmacSHA512")) - val z = hmac.doFinal(data.array()) - val zl = z.sliceArray(0 until ED25519_SCALAR_SIZE) - - // Step 2: Compute child PK - - /* - ###################################### - Standard BIP32-ed25519 derivation - ####################################### - zL = 8 * 28bytesOf(z_left_hand_side) - - ###################################### - Chris Peikert's ammendment to BIP32-ed25519 derivation - ####################################### - zL = 8 * trunc_256_minus_g_bits (z_left_hand_side, g) - */ - - val left = - (BigInteger( - 1, - trunc256MinusGBits(zl.clone(), derivationType.value).reversedArray() - ) * BigInteger.valueOf(8L)) - .toByteArray() - .reversedArray() - .let { bytes -> - ByteArray(ED25519_SCALAR_SIZE - bytes.size) + bytes - } // Pad to 32 bytes - - val p = lazySodium.cryptoScalarMultEd25519BaseNoclamp(left).toBytes() - - // Step 3: Compute child chain code - data.put(0, 0x03) - hmac.init(SecretKeySpec(cc, "HmacSHA512")) - val fullChildChainCode = hmac.doFinal(data.array()) - val childChainCode = - fullChildChainCode.sliceArray(CHAIN_CODE_SIZE until 2 * CHAIN_CODE_SIZE) - - val newPK = ByteArray(32) - lazySodium.cryptoCoreEd25519Add(newPK, p, pk) - - return ByteBuffer.allocate(ED25519_POINT_SIZE + CHAIN_CODE_SIZE) - .put(newPK) - .put(childChainCode) - .array() - } - - /** z_L by */ - internal fun trunc256MinusGBits(zl: ByteArray, g: Int): ByteArray { - if (g < 0 || g > 256) { - throw IllegalArgumentException("Number of bits to zero must be between 0 and 256.") + .let { bytes -> + bytes + + ByteArray( + ED25519_SCALAR_SIZE - + bytes.size + ) + } // Pad to 32 bytes + + println("Left: ${left.contentToString()}") + + val p = lazySodium.cryptoScalarMultEd25519BaseNoclamp(left).toBytes() + println("p: ${p.contentToString()}") + + // Step 3: Compute child chain code + data.put(0, 0x03) + hmac.init(SecretKeySpec(cc, "HmacSHA512")) + val fullChildChainCode = hmac.doFinal(data.array()) + val childChainCode = + fullChildChainCode.sliceArray( + CHAIN_CODE_SIZE until 2 * CHAIN_CODE_SIZE + ) + println("Child Chain Code: ${childChainCode.contentToString()}") + + val newPK = ByteArray(32) + lazySodium.cryptoCoreEd25519Add(newPK, p, pk) + println("New PK=p+pk: ${newPK.contentToString()}") + + return ByteBuffer.allocate(ED25519_POINT_SIZE + CHAIN_CODE_SIZE) + .put(newPK) + .put(childChainCode) + .array() } - val truncated = zl - var remainingBits = g - - // start from the last byte and move backwards - for (i in truncated.size - 1 downTo 0) { - if (remainingBits >= 8) { - truncated[i] = 0 - remainingBits -= 8 - } else { - val mask = ((1 shl (8 - remainingBits)) - 1).toByte() - truncated[i] = (truncated[i].toInt() and mask.toInt()).toByte() - break - } + /** z_L by */ + internal fun trunc256MinusGBits(zl: ByteArray, g: Int): ByteArray { + if (g < 0 || g > 256) { + throw IllegalArgumentException( + "Number of bits to zero must be between 0 and 256." + ) + } + + val truncated = zl + var remainingBits = g + + // start from the last byte and move backwards + for (i in truncated.size - 1 downTo 0) { + if (remainingBits >= 8) { + truncated[i] = 0 + remainingBits -= 8 + } else { + val mask = ((1 shl (8 - remainingBits)) - 1).toByte() + truncated[i] = (truncated[i].toInt() and mask.toInt()).toByte() + break + } + } + return truncated } - return truncated - } - - /** - * Derives a child key from the root key based on BIP44 path - * - * @param rootKey - * - root key in extended format (kL, kR, c). It should be 96 bytes long - * @param bip44Path - * - BIP44 path (m / purpose' / coin_type' / account' / change / address_index). The ' indicates - * that the value is hardened - * @param isPrivate - * - returns full 64 bytes privatekey (first 32 bytes scalar), false returns 32 byte public key, - * @returns - * - The public key of 32 bytes. If isPrivate is true, returns the private key instead. - */ - public fun deriveKey( - rootKey: ByteArray, - bip44Path: List, - isPrivate: Boolean, - derivationType: BIP32DerivationType = BIP32DerivationType.Peikert - ): ByteArray { - var derived = rootKey - for (path in bip44Path) { - derived = this.deriveChildNodePrivate(derived, path, derivationType) - } - if (isPrivate) { - return derived - } else { - return lazySodium - .cryptoScalarMultEd25519BaseNoclamp( - derived.sliceArray(0 until ED25519_SCALAR_SIZE) - ) - .toBytes() + - derived.sliceArray( - 2 * ED25519_SCALAR_SIZE until 2 * ED25519_SCALAR_SIZE + CHAIN_CODE_SIZE - ) + /** + * Derives a child key from the root key based on BIP44 path + * + * @param rootKey + * - root key in extended format (kL, kR, c). It should be 96 bytes long + * @param bip44Path + * - BIP44 path (m / purpose' / coin_type' / account' / change / address_index). The ' + * indicates that the value is hardened + * @param isPrivate + * - returns full 64 bytes privatekey (first 32 bytes scalar), false returns 32 byte public + * key, + * @returns + * - The public key of 32 bytes. If isPrivate is true, returns the private key instead. + */ + public fun deriveKey( + rootKey: ByteArray, + bip44Path: List, + isPrivate: Boolean, + derivationType: BIP32DerivationType = BIP32DerivationType.Peikert + ): ByteArray { + var derived = rootKey + for (path in bip44Path) { + derived = this.deriveChildNodePrivate(derived, path, derivationType) + } + if (isPrivate) { + return derived + } else { + return lazySodium.cryptoScalarMultEd25519BaseNoclamp( + derived.sliceArray( + 0 until ED25519_SCALAR_SIZE + ) + ) + .toBytes() + + derived.sliceArray( + 2 * ED25519_SCALAR_SIZE until + 2 * ED25519_SCALAR_SIZE + + CHAIN_CODE_SIZE + ) + } } - } - - /** - * - * @param context - * - context of the key (i.e Address, Identity) - * @param account - * - account number. This value will be hardened as part of BIP44 - * @param keyIndex - * - key index. This value will be a SOFT derivation as part of BIP44. - * @returns - * - public key 32 bytes - */ - fun keyGen( - context: KeyContext, - account: UInt, - change: UInt, - keyIndex: UInt, - derivationType: BIP32DerivationType = BIP32DerivationType.Peikert - ): ByteArray { - val rootKey: ByteArray = fromSeed(this.seed) - val bip44Path: List = getBIP44PathFromContext(context, account, change, keyIndex) - return this.deriveKey(rootKey, bip44Path, false, derivationType) - .take(ED25519_POINT_SIZE) - .toByteArray() - } - - /** - * Sign arbitrary but non-Algorand related data - * @param context - * - context of the key (i.e Address, Identity) - * @param account - * - account number. This value will be hardened as part of BIP44 - * @param keyIndex - * - key index. This value will be a SOFT derivation as part of BIP44. - * @param data - * - data to be signed in raw bytes - * @param metadata - * - metadata object that describes how `data` was encoded and what schema to use to validate - * against - * - * @returns - * - signature holding R and S, totally 64 bytes - */ - fun signData( - context: KeyContext, - account: UInt, - change: UInt, - keyIndex: UInt, - data: ByteArray, - metadata: SignMetadata, - derivationType: BIP32DerivationType = BIP32DerivationType.Peikert - ): ByteArray { - - val valid = validateData(data, metadata) - - if (!valid) { // failed schema validation - throw DataValidationException("Data validation failed") + + /** + * + * @param context + * - context of the key (i.e Address, Identity) + * @param account + * - account number. This value will be hardened as part of BIP44 + * @param keyIndex + * - key index. This value will be a SOFT derivation as part of BIP44. + * @returns + * - public key 32 bytes + */ + fun keyGen( + context: KeyContext, + account: UInt, + change: UInt, + keyIndex: UInt, + derivationType: BIP32DerivationType = BIP32DerivationType.Peikert + ): ByteArray { + val rootKey: ByteArray = fromSeed(this.seed) + val bip44Path: List = + getBIP44PathFromContext(context, account, change, keyIndex) + return this.deriveKey(rootKey, bip44Path, false, derivationType) + .sliceArray(0 until ED25519_POINT_SIZE) } - return rawSign( - getBIP44PathFromContext(context, account, change, keyIndex), - data, - derivationType - ) - } - - /** - * Sign Algorand transaction - * @param context - * - context of the key (i.e Address, Identity) - * @param account - * - account number. This value will be hardened as part of BIP44 - * @param keyIndex - * - key index. This value will be a SOFT derivation as part of BIP44. - * @param tx - * - Transaction object containing parameters to be signed, e.g. sender, receiver, amount, fee, - * - * @returns stx - * - SignedTransaction object - */ - fun signAlgoTransaction( - context: KeyContext, - account: UInt, - change: UInt, - keyIndex: UInt, - prefixEncodedTx: ByteArray, - derivationType: BIP32DerivationType = BIP32DerivationType.Peikert - ): ByteArray { - return rawSign( - getBIP44PathFromContext(context, account, change, keyIndex), - prefixEncodedTx, - derivationType - ) - } - - /** - * Raw Signing function called by signData and signTransaction - * - * Ref: https://datatracker.ietf.org/doc/html/rfc8032#section-5.1.6 - * - * Edwards-Curve Digital Signature Algorithm (EdDSA) - * - * @param bip44Path - * - BIP44 path (m / purpose' / coin_type' / account' / change / address_index) - * @param data - * - data to be signed in raw bytes - * - * @returns - * - signature holding R and S, totally 64 bytes - */ - fun rawSign( - bip44Path: List, - data: ByteArray, - derivationType: BIP32DerivationType = BIP32DerivationType.Peikert - ): ByteArray { - - val rootKey: ByteArray = fromSeed(this.seed) - val raw: ByteArray = deriveKey(rootKey, bip44Path, true, derivationType) - - val scalar = raw.sliceArray(0 until 32) - val c = raw.sliceArray(32 until 64) - - // \(1): pubKey = scalar * G (base point, no clamp) - val publicKey = lazySodium.cryptoScalarMultEd25519BaseNoclamp(scalar).toBytes() - - // \(2): r = hash(c + msg) mod q [LE] - var r = this.safeModQ(MessageDigest.getInstance("SHA-512").digest(c + data)) - - // \(4): R = r * G (base point, no clamp) - val R = lazySodium.cryptoScalarMultEd25519BaseNoclamp(r).toBytes() - - var h = this.safeModQ(MessageDigest.getInstance("SHA-512").digest(R + publicKey + data)) - - // \(5): S = (r + h * k) mod q - var S = - this.safeModQ( - lazySodium.cryptoCoreEd25519ScalarAdd( - r, - lazySodium - .cryptoCoreEd25519ScalarMul(h, scalar) - .toByteArray() - .reversedArray() - ) + /** + * Sign arbitrary but non-Algorand related data + * @param context + * - context of the key (i.e Address, Identity) + * @param account + * - account number. This value will be hardened as part of BIP44 + * @param keyIndex + * - key index. This value will be a SOFT derivation as part of BIP44. + * @param data + * - data to be signed in raw bytes + * @param metadata + * - metadata object that describes how `data` was encoded and what schema to use to + * validate against + * + * @returns + * - signature holding R and S, totally 64 bytes + */ + fun signData( + context: KeyContext, + account: UInt, + change: UInt, + keyIndex: UInt, + data: ByteArray, + metadata: SignMetadata, + derivationType: BIP32DerivationType = BIP32DerivationType.Peikert + ): ByteArray { + + val valid = validateData(data, metadata) + + if (!valid) { // failed schema validation + throw DataValidationException("Data validation failed") + } + + return rawSign( + getBIP44PathFromContext(context, account, change, keyIndex), + data, + derivationType ) - - return R + S - } - - /* - * SafeModQ is a helper function to ensure that the result of a mod q operation is 32 bytes - * It wraps around the cryptoCoreEd25519ScalarReduce function, which can accept either BigInteger or ByteArray - */ - fun safeModQ(input: BigInteger): ByteArray { - var reduced = lazySodium.cryptoCoreEd25519ScalarReduce(input).toByteArray().reversedArray() - if (reduced.size < 32) { - reduced = reduced + ByteArray(32 - reduced.size) } - return reduced - } - fun safeModQ(input: ByteArray): ByteArray { - var reduced = lazySodium.cryptoCoreEd25519ScalarReduce(input).toByteArray().reversedArray() - if (reduced.size < 32) { - reduced = reduced + ByteArray(32 - reduced.size) - } - return reduced - } - - /** - * Wrapper around libsodium basic signature verification - * - * Any lib or system that can verify EdDSA signatures can be used - * - * @param signature - * - raw 64 bytes signature (R, S) - * @param message - * - raw bytes of the message - * @param publicKey - * - raw 32 bytes public key (x,y) - * @returns true if signature is valid, false otherwise - */ - fun verifyWithPublicKey( - signature: ByteArray, - message: ByteArray, - publicKey: ByteArray - ): Boolean { - return lazySodium.cryptoSignVerifyDetached(signature, message, message.size, publicKey) - } - - /** - * Function to perform ECDH against a provided public key - * - * ECDH reference link: https://en.wikipedia.org/wiki/Elliptic-curve_Diffie%E2%80%93Hellman - * - * It creates a shared secret between two parties. Each party only needs to be aware of the - * other's public key. This symmetric secret can be used to derive a symmetric key for - * encryption and decryption. Creating a private channel between the two parties. Note that you - * must specify the order of concatenation for the public keys with otherFirst. - * @param context - * - context of the key (i.e Address, Identity) - * @param account - * - account number. This value will be hardened as part of BIP44 - * @param keyIndex - * - key index. This value will be a SOFT derivation as part of BIP44. - * @param otherPartyPub - * - raw 32 bytes public key of the other party - * @param meFirst - * - decide the order of concatenation of the public keys in the shared secret, true: my public - * key first, false: other party's public key first - * @returns - * - raw 32 bytes shared secret - */ - fun ECDH( - context: KeyContext, - account: UInt, - change: UInt, - keyIndex: UInt, - otherPartyPub: ByteArray, - meFirst: Boolean, - derivationType: BIP32DerivationType = BIP32DerivationType.Peikert - ): ByteArray { - - val rootKey: ByteArray = fromSeed(this.seed) - - val publicKey: ByteArray = this.keyGen(context, account, change, keyIndex, derivationType) - val privateKey: ByteArray = - this.deriveKey( - rootKey, - getBIP44PathFromContext(context, account, change, keyIndex), - true, - derivationType + /** + * Sign Algorand transaction + * @param context + * - context of the key (i.e Address, Identity) + * @param account + * - account number. This value will be hardened as part of BIP44 + * @param keyIndex + * - key index. This value will be a SOFT derivation as part of BIP44. + * @param tx + * - Transaction object containing parameters to be signed, e.g. sender, receiver, amount, + * fee, + * + * @returns stx + * - SignedTransaction object + */ + fun signAlgoTransaction( + context: KeyContext, + account: UInt, + change: UInt, + keyIndex: UInt, + prefixEncodedTx: ByteArray, + derivationType: BIP32DerivationType = BIP32DerivationType.Peikert + ): ByteArray { + return rawSign( + getBIP44PathFromContext(context, account, change, keyIndex), + prefixEncodedTx, + derivationType ) + } - val scalar: ByteArray = privateKey.sliceArray(0 until 32) - - val sharedPoint = ByteArray(32) - val myCurve25519Key = ByteArray(32) - val otherPartyCurve25519Key = ByteArray(32) + /** + * Raw Signing function called by signData and signTransaction + * + * Ref: https://datatracker.ietf.org/doc/html/rfc8032#section-5.1.6 + * + * Edwards-Curve Digital Signature Algorithm (EdDSA) + * + * @param bip44Path + * - BIP44 path (m / purpose' / coin_type' / account' / change / address_index) + * @param data + * - data to be signed in raw bytes + * + * @returns + * - signature holding R and S, totally 64 bytes + */ + fun rawSign( + bip44Path: List, + data: ByteArray, + derivationType: BIP32DerivationType = BIP32DerivationType.Peikert + ): ByteArray { + + val rootKey: ByteArray = fromSeed(this.seed) + val raw: ByteArray = deriveKey(rootKey, bip44Path, true, derivationType) + + val scalar = raw.sliceArray(0 until 32) + val c = raw.sliceArray(32 until 64) + + // \(1): pubKey = scalar * G (base point, no clamp) + val publicKey = lazySodium.cryptoScalarMultEd25519BaseNoclamp(scalar).toBytes() + + // \(2): r = hash(c + msg) mod q [LE] + var r = this.safeModQ(MessageDigest.getInstance("SHA-512").digest(c + data)) + + // \(4): R = r * G (base point, no clamp) + val R = lazySodium.cryptoScalarMultEd25519BaseNoclamp(r).toBytes() + + var h = + this.safeModQ( + MessageDigest.getInstance("SHA-512") + .digest(R + publicKey + data) + ) + + // \(5): S = (r + h * k) mod q + var S = + this.safeModQ( + lazySodium.cryptoCoreEd25519ScalarAdd( + r, + lazySodium.cryptoCoreEd25519ScalarMul( + h, + scalar + ) + .toByteArray() + .reversedArray() + ) + ) + + return R + S + } - lazySodium.convertPublicKeyEd25519ToCurve25519(myCurve25519Key, publicKey) - lazySodium.convertPublicKeyEd25519ToCurve25519(otherPartyCurve25519Key, otherPartyPub) - lazySodium.cryptoScalarMult(sharedPoint, scalar, otherPartyCurve25519Key) + /* + * SafeModQ is a helper function to ensure that the result of a mod q operation is 32 bytes + * It wraps around the cryptoCoreEd25519ScalarReduce function, which can accept either BigInteger or ByteArray + */ + fun safeModQ(input: BigInteger): ByteArray { + var reduced = + lazySodium.cryptoCoreEd25519ScalarReduce(input) + .toByteArray() + .reversedArray() + if (reduced.size < 32) { + reduced = reduced + ByteArray(32 - reduced.size) + } + return reduced + } - val concatenated: ByteArray + fun safeModQ(input: ByteArray): ByteArray { + var reduced = + lazySodium.cryptoCoreEd25519ScalarReduce(input) + .toByteArray() + .reversedArray() + if (reduced.size < 32) { + reduced = reduced + ByteArray(32 - reduced.size) + } + return reduced + } - if (meFirst) { - concatenated = sharedPoint + myCurve25519Key + otherPartyCurve25519Key - } else { - concatenated = sharedPoint + otherPartyCurve25519Key + myCurve25519Key + /** + * Wrapper around libsodium basic signature verification + * + * Any lib or system that can verify EdDSA signatures can be used + * + * @param signature + * - raw 64 bytes signature (R, S) + * @param message + * - raw bytes of the message + * @param publicKey + * - raw 32 bytes public key (x,y) + * @returns true if signature is valid, false otherwise + */ + fun verifyWithPublicKey( + signature: ByteArray, + message: ByteArray, + publicKey: ByteArray + ): Boolean { + return lazySodium.cryptoSignVerifyDetached( + signature, + message, + message.size, + publicKey + ) } - val output = ByteArray(32) - lazySodium.cryptoGenericHash( - output, - 32, - concatenated, - concatenated.size.toLong(), - ) - return output - } + /** + * Function to perform ECDH against a provided public key + * + * ECDH reference link: https://en.wikipedia.org/wiki/Elliptic-curve_Diffie%E2%80%93Hellman + * + * It creates a shared secret between two parties. Each party only needs to be aware of the + * other's public key. This symmetric secret can be used to derive a symmetric key for + * encryption and decryption. Creating a private channel between the two parties. Note that + * you must specify the order of concatenation for the public keys with otherFirst. + * @param context + * - context of the key (i.e Address, Identity) + * @param account + * - account number. This value will be hardened as part of BIP44 + * @param keyIndex + * - key index. This value will be a SOFT derivation as part of BIP44. + * @param otherPartyPub + * - raw 32 bytes public key of the other party + * @param meFirst + * - decide the order of concatenation of the public keys in the shared secret, true: my + * public key first, false: other party's public key first + * @returns + * - raw 32 bytes shared secret + */ + fun ECDH( + context: KeyContext, + account: UInt, + change: UInt, + keyIndex: UInt, + otherPartyPub: ByteArray, + meFirst: Boolean, + derivationType: BIP32DerivationType = BIP32DerivationType.Peikert + ): ByteArray { + + val rootKey: ByteArray = fromSeed(this.seed) + + val publicKey: ByteArray = + this.keyGen(context, account, change, keyIndex, derivationType) + val privateKey: ByteArray = + this.deriveKey( + rootKey, + getBIP44PathFromContext( + context, + account, + change, + keyIndex + ), + true, + derivationType + ) + + val scalar: ByteArray = privateKey.sliceArray(0 until 32) + + val sharedPoint = ByteArray(32) + val myCurve25519Key = ByteArray(32) + val otherPartyCurve25519Key = ByteArray(32) + + lazySodium.convertPublicKeyEd25519ToCurve25519(myCurve25519Key, publicKey) + lazySodium.convertPublicKeyEd25519ToCurve25519( + otherPartyCurve25519Key, + otherPartyPub + ) + lazySodium.cryptoScalarMult(sharedPoint, scalar, otherPartyCurve25519Key) + + val concatenated: ByteArray + + if (meFirst) { + concatenated = sharedPoint + myCurve25519Key + otherPartyCurve25519Key + } else { + concatenated = sharedPoint + otherPartyCurve25519Key + myCurve25519Key + } + + val output = ByteArray(32) + lazySodium.cryptoGenericHash( + output, + 32, + concatenated, + concatenated.size.toLong(), + ) + return output + } } diff --git a/sharedModule/src/main/kotlin/com/algorandfoundation/bip32ed25519/utils.kt b/sharedModule/src/main/kotlin/com/algorandfoundation/bip32ed25519/utils.kt index 3071319..e0ac242 100644 --- a/sharedModule/src/main/kotlin/com/algorandfoundation/bip32ed25519/utils.kt +++ b/sharedModule/src/main/kotlin/com/algorandfoundation/bip32ed25519/utils.kt @@ -11,8 +11,8 @@ enum class KeyContext(val value: Int) { } enum class BIP32DerivationType(val value: Int) { - Khovratovich(32), Peikert(9), + Khovratovich(32) } enum class Encoding {