From f533319e3b9bf4d1feb3ba4b625bc9b792d14c7c Mon Sep 17 00:00:00 2001 From: ehanoc Date: Tue, 29 Aug 2023 15:06:25 +0000 Subject: [PATCH 01/47] Contextual Wallet API Signed-off-by: ehanoc --- ARCs/arc-0078.md | 154 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 ARCs/arc-0078.md diff --git a/ARCs/arc-0078.md b/ARCs/arc-0078.md new file mode 100644 index 000000000..e5dd96540 --- /dev/null +++ b/ARCs/arc-0078.md @@ -0,0 +1,154 @@ +--- +arc: 078 +title: Algorand Wallet Contextual KeyGen and Signing +description: +author: Bruno Martins (@ehanoc, bruno.martins@algorand.foundation) +discussions-to: +status: Proposed +type: Standards Track +category: Interface +created: 2023-06-30 +--- + +# Algorand Wallet Contextual KeyGen and Signing + +## Abstract + +This document specifies an expansion of the Algorand Wallet API to support contextual key generation and signing for **non-algorand-transaction** contexts. + +## Motivation + +At it's core, blockchain keys and signatures are just implementations of cryptographic primitives. These primitives can be used for a wide variety of use cases, not just for the purpose of signing transactions. And wallets being applications that contain or serve as Key Management Systems (KMS), should be able to support a multiple of other use cases that are not just related to transactions. This creates the possibility of attaching on-chain behavior with off-chain behavior, allowing for example to concepts of Identity and Authentication. + +The current wallet APIs available only support key derivation for addresses and signing for transactions. In the broader context of Algorand and web3 ecosystems, there is a need for a more flexible API that allows for contextual key generation and signing. An example of this could be identity dedicated keys (i.e DIDs), or passkeys for authentication. + +## Dependency +- [ARC44](https://github.com/algorandfoundation/ARCs/pull/224) support (BIP44) + +## Specification + +The key words "**MUST**", "**MUST NOT**", "**REQUIRED**", "**SHALL**", "**SHALL NOT**", "**SHOULD**", "**SHOULD NOT**", "**RECOMMENDED**", "**MAY**", and "**OPTIONAL**" in this document are to be interpreted as described in RFC-2119. + +> Comments like this are non-normative. + +### Overview + +> This overview section is non-normative. + +At a high level this specification defines a new API to generate keys and signing in other contexts than transactions. + +> Interfaces are defined in TypeScript. All the objects that are defined are valid JSON objects. + + +#### Contexts + +An enum of contexts that are supported by the wallet. This enum is extensible and can be expanded in the future to support more contexts (i.e messaging, ephemeral keys, other chain derivations, etc) + +```ts +enum KeyContext { + Identity = 1, + PASSKEY = 2, + GPG = 3, + ... +} +``` + +#### BIP44 Context Derivation Paths + +There is several reasons why we want to avoid key re-use in different contexts. One of the reasons is that we want to avoid the possibility of **leaking** information about the identity of the user. Another reason is that there is some cryptographic security concers when re-using keys or in case of keys being compromised the impact is **compartmentalized**. + +For this reason is advisable to have different derivation paths for different contexts supporting recoveribility of the keys by using the same mnemonic as before. + +```ts + +| Purpose | Coin Type | Account | Change | Address Index | Context | +| :-----: | :-------: | :-----: | :----: | :-----------: | :-----------------: | +| 44' | 0' | x' | x | x | Identity Keys | +| 67' | 0' | x' | x | x | Passkeys | +| 68' | 0' | x' | x | x | GPG Keys | +``` + +see [BIP44](https://github.com/algorandfoundation/ARCs/pull/224) + +#### Wallet New Interface + +This new interface is for the wallet to implement. It is an extension of the current wallet interface. + +This extension is **OPTIONAL** and separate from the current interface that handles address generation and transaction signing. + +```ts + +interface Wallet { + ... + keyGen(context: KeyContext, name: string): Promise; + sign(context: KeyContext, name: string, message: Uint8Array): Promise; + exportGPGKey(name: string): Promise; // only for gpg keys + ... +} +``` + +##### keyGen + +Wallet API function method that derives a key in a given context. The key should be derived from the wallet's master key. The key derivation should be deterministic, meaning that the same key should be derived from the same context and name. + +In order to derive a key, the wallet should lookup the different derivation paths for the given context. + + +- `keyGen(context: KeyContext, name: string): Promise` + - `context` - The context of the key to be generated + - `name` - The name of the key to be generated + - `returns` - The public key, which is a point on the Ed25519 elliptic curve. The public key is encoded as compressed EC point: the y-coordinate, combined with the lowest bit (the parity) of the x-coordinate, making up 32 bytes. + +###### Example + +```ts +const DID_publicKey: Uint8Array = await wallet.keyGen(KeyContext.Identity, "my-did"); +// share DID + +``` +##### sign + +Signing operation that can be used to perform arbitrary signing operations in a given context. These can be for example: + + - Identity signatures (Documents, DIDs, etc) + - Authentication signatures (JWT, FIDO, etc) + - Passkey signatures (Passwordless authentication) + - GPG signatures (PGP, etc) + +- `sign(context: KeyContext, name: string, message: Uint8Array): Promise` + - `context` - The context of the key to be used for signing + - `name` - The name of the key to be used for signing + - `message` - The message to be signed + - `returns` - The produced digital signature, which is 64 bytes (32 + 32 bytes). It holds a compressed point R + the integer s (confirming that the signer knows the msg and the privKey). + +###### Example + +```ts +// Example -> sign a document + +// first create a identity key +const DID_publicKey: Uint8Array = await wallet.keyGen(KeyContext.Identity, "my-did"); + +// A raw representation of a document +const document: Uint8Array = new Uint8Array([1,2,3,4,5,6,7,8,9,0]); +// hash the document +const documentHash: Uint8Array = await crypto.hash(document); // some hash + +// sign hash +const signature: Uint8Array = await wallet.sign(KeyContext.Identity, "my-did", documentHash); + +``` + +##### exportGPGKey + +GPG keys are very engrained in our digital systems (i.e GIT, SSH, etc) and when these are based in Elliptic Curve cryptography at it's core, these keys are no different from the Ed25519 keys that we use to generate addresses and sign transactions. + +The signature schemes might be slightly different, but the basic cryptographic primitives are the same. + +Being able to derive and manage GPG keys from your wallet; that might already support keys for addresses, identity concepts, authentication, etc; is a very powerful feature as you can always recover your GPG keys from your mnemonic. + +Exporting this key, allows us to import it into already existing ecosystems that rely on GPG keys, such as GIT, SSH, etc. + +- `exportGPGKey(name: string): Promise` + - `name` - The name of the key to be exported + - `returns` - The exported GPG key in cencrypted format \ No newline at end of file From ba9a4c117d84b147730eeac47a974348bcbcd943 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane?= Date: Wed, 30 Aug 2023 09:46:35 +0200 Subject: [PATCH 02/47] Assign number + fix linting --- ARCs/{arc-0078.md => arc-0052.md} | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) rename ARCs/{arc-0078.md => arc-0052.md} (92%) diff --git a/ARCs/arc-0078.md b/ARCs/arc-0052.md similarity index 92% rename from ARCs/arc-0078.md rename to ARCs/arc-0052.md index e5dd96540..244a379b2 100644 --- a/ARCs/arc-0078.md +++ b/ARCs/arc-0052.md @@ -1,13 +1,14 @@ --- -arc: 078 -title: Algorand Wallet Contextual KeyGen and Signing -description: -author: Bruno Martins (@ehanoc, bruno.martins@algorand.foundation) -discussions-to: -status: Proposed +arc: 52 +title: Wallet Contextual KeyGen and Signing +description: Algorand Wallet API for Contextual KeyGen and Signing +author: Bruno Martins (@ehanoc) +discussions-to: https://github.com/algorandfoundation/ARCs/issues/239 +status: Draft type: Standards Track category: Interface created: 2023-06-30 +requires: 44 --- # Algorand Wallet Contextual KeyGen and Signing @@ -22,9 +23,6 @@ At it's core, blockchain keys and signatures are just implementations of cryptog The current wallet APIs available only support key derivation for addresses and signing for transactions. In the broader context of Algorand and web3 ecosystems, there is a need for a more flexible API that allows for contextual key generation and signing. An example of this could be identity dedicated keys (i.e DIDs), or passkeys for authentication. -## Dependency -- [ARC44](https://github.com/algorandfoundation/ARCs/pull/224) support (BIP44) - ## Specification The key words "**MUST**", "**MUST NOT**", "**REQUIRED**", "**SHALL**", "**SHALL NOT**", "**SHOULD**", "**SHOULD NOT**", "**RECOMMENDED**", "**MAY**", and "**OPTIONAL**" in this document are to be interpreted as described in RFC-2119. @@ -68,7 +66,7 @@ For this reason is advisable to have different derivation paths for different co | 68' | 0' | x' | x | x | GPG Keys | ``` -see [BIP44](https://github.com/algorandfoundation/ARCs/pull/224) +see `arc-44` Support (BIP44) #### Wallet New Interface @@ -151,4 +149,11 @@ Exporting this key, allows us to import it into already existing ecosystems that - `exportGPGKey(name: string): Promise` - `name` - The name of the key to be exported - - `returns` - The exported GPG key in cencrypted format \ No newline at end of file + - `returns` - The exported GPG key in cencrypted format + +## Rationale + +## Security Considerations + +## Copyright +Copyright and related rights waived via CCO. \ No newline at end of file From 585d48ac4110e52ca1e3e4d4818873bfff776cf8 Mon Sep 17 00:00:00 2001 From: ehanoc Date: Mon, 25 Sep 2023 15:35:28 +0000 Subject: [PATCH 03/47] Update specification keygen & sign Signed-off-by: ehanoc --- ARCs/arc-0052.md | 80 ++++++++++++++++++------------------------------ 1 file changed, 29 insertions(+), 51 deletions(-) diff --git a/ARCs/arc-0052.md b/ARCs/arc-0052.md index 244a379b2..c6f60225a 100644 --- a/ARCs/arc-0052.md +++ b/ARCs/arc-0052.md @@ -44,9 +44,8 @@ An enum of contexts that are supported by the wallet. This enum is extensible an ```ts enum KeyContext { + Address = 0, // Otherwise known as Account? Identity = 1, - PASSKEY = 2, - GPG = 3, ... } ``` @@ -57,14 +56,10 @@ There is several reasons why we want to avoid key re-use in different contexts. For this reason is advisable to have different derivation paths for different contexts supporting recoveribility of the keys by using the same mnemonic as before. -```ts - | Purpose | Coin Type | Account | Change | Address Index | Context | | :-----: | :-------: | :-----: | :----: | :-----------: | :-----------------: | -| 44' | 0' | x' | x | x | Identity Keys | -| 67' | 0' | x' | x | x | Passkeys | -| 68' | 0' | x' | x | x | GPG Keys | -``` +| 44' | 283' | x' | x | x | Addresses | +| 44' | 0' | x' | x | x | Identity Keys | see `arc-44` Support (BIP44) @@ -80,7 +75,6 @@ interface Wallet { ... keyGen(context: KeyContext, name: string): Promise; sign(context: KeyContext, name: string, message: Uint8Array): Promise; - exportGPGKey(name: string): Promise; // only for gpg keys ... } ``` @@ -91,17 +85,20 @@ Wallet API function method that derives a key in a given context. The key should In order to derive a key, the wallet should lookup the different derivation paths for the given context. - -- `keyGen(context: KeyContext, name: string): Promise` - - `context` - The context of the key to be generated - - `name` - The name of the key to be generated - - `returns` - The public key, which is a point on the Ed25519 elliptic curve. The public key is encoded as compressed EC point: the y-coordinate, combined with the lowest bit (the parity) of the x-coordinate, making up 32 bytes. +- **method name**: `keyGen` +- **parameters**: + - `context`: The context of the key to be generated + - `type` (optional): The type of key to be generated. Possible values are `p-256`, `ed25519` + - Default value: `ed25519` + - `options`(optional): + - `index`: The index of the key to be generated. If not provided, the wallet should derive the next key in the derivation path. + - `key-identifier`(optional): Optional parameter to give the key a rememberable name (i.e "my-did"). Note: this wouldn't be recoverable from the mnemonic. +- **returns**: Byte array cointaing the public key, which is a point on the Ed25519 elliptic curve. The public key is encoded as compressed EC point: the y-coordinate, combined with the lowest bit (the parity) of the x-coordinate, making up 32 bytes. ###### Example ```ts -const DID_publicKey: Uint8Array = await wallet.keyGen(KeyContext.Identity, "my-did"); -// share DID +const identityKey: Uint8Array = await wallet.keyGen(KeyContext.Identity, "ed25519", { index: 0, key-identifier: "myGamingId" }) ``` ##### sign @@ -109,51 +106,32 @@ const DID_publicKey: Uint8Array = await wallet.keyGen(KeyContext.Identity, "my-d Signing operation that can be used to perform arbitrary signing operations in a given context. These can be for example: - Identity signatures (Documents, DIDs, etc) - - Authentication signatures (JWT, FIDO, etc) - - Passkey signatures (Passwordless authentication) - - GPG signatures (PGP, etc) - -- `sign(context: KeyContext, name: string, message: Uint8Array): Promise` - - `context` - The context of the key to be used for signing - - `name` - The name of the key to be used for signing - - `message` - The message to be signed - - `returns` - The produced digital signature, which is 64 bytes (32 + 32 bytes). It holds a compressed point R + the integer s (confirming that the signer knows the msg and the privKey). + - Address signatures (i.e Signing auth challenges) + +- **method name**: `sign` +- **parameters**: + - `data`: The data to be signed + - `context`: The context of the key to be used for signing + - `metadata`: Object to describe the type of data that is being signed. It should specify the encoding used (i.e msgpack, cbor, etc) and a schema so that the wallet can decode it to show the user. + - `encoding`: The encoding used for the data + - `schema`: The schema of the data in the form of a JSON schema + - `type`(optional): The key type to be used for signing. Possible values are `ecdsa-p256`, `ed25519` + - Default value: `ed25519` +- **returns**: Byte array containing the signature. The signature is encoded as a 64-byte array (32 + 32 bytes). It holds a compressed point R + the integer s (confirming that the signer knows the msg and the privKey). ###### Example ```ts -// Example -> sign a document +// Example -> sign auth challenge request with an Address Key -// first create a identity key -const DID_publicKey: Uint8Array = await wallet.keyGen(KeyContext.Identity, "my-did"); - -// A raw representation of a document -const document: Uint8Array = new Uint8Array([1,2,3,4,5,6,7,8,9,0]); -// hash the document -const documentHash: Uint8Array = await crypto.hash(document); // some hash - -// sign hash -const signature: Uint8Array = await wallet.sign(KeyContext.Identity, "my-did", documentHash); +const authChallenge: Uint8Array = await wallet.sign("my-auth-challenge", KeyContext.Address, { encoding: "cbor", schema: { type: "object", properties: { challenge: { type: "string" } } } }, "ed25519", { index: 0 }) ``` -##### exportGPGKey - -GPG keys are very engrained in our digital systems (i.e GIT, SSH, etc) and when these are based in Elliptic Curve cryptography at it's core, these keys are no different from the Ed25519 keys that we use to generate addresses and sign transactions. - -The signature schemes might be slightly different, but the basic cryptographic primitives are the same. - -Being able to derive and manage GPG keys from your wallet; that might already support keys for addresses, identity concepts, authentication, etc; is a very powerful feature as you can always recover your GPG keys from your mnemonic. - -Exporting this key, allows us to import it into already existing ecosystems that rely on GPG keys, such as GIT, SSH, etc. - -- `exportGPGKey(name: string): Promise` - - `name` - The name of the key to be exported - - `returns` - The exported GPG key in cencrypted format - -## Rationale ## Security Considerations +TBC + ## Copyright Copyright and related rights waived via CCO. \ No newline at end of file From 5874b3f87e1c6ee229727c78dcf33e5c5283ae33 Mon Sep 17 00:00:00 2001 From: ehanoc Date: Mon, 25 Sep 2023 15:37:36 +0000 Subject: [PATCH 04/47] Update authors Signed-off-by: ehanoc --- ARCs/arc-0052.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ARCs/arc-0052.md b/ARCs/arc-0052.md index c6f60225a..7241af58c 100644 --- a/ARCs/arc-0052.md +++ b/ARCs/arc-0052.md @@ -2,7 +2,10 @@ arc: 52 title: Wallet Contextual KeyGen and Signing description: Algorand Wallet API for Contextual KeyGen and Signing -author: Bruno Martins (@ehanoc) +authors: + - Bruno Martins (@ehanoc, bruno.martins@algorand.foundation) + - Pattrick (patrick@txnlab.dev) + - Kevin W(kevin@blockshake.io) discussions-to: https://github.com/algorandfoundation/ARCs/issues/239 status: Draft type: Standards Track From 2488fa5ffc5d8e8e40cbc99ec49fa505ff392a46 Mon Sep 17 00:00:00 2001 From: ehanoc Date: Wed, 25 Oct 2023 12:00:13 +0000 Subject: [PATCH 05/47] doc: Update ARC52 API and description Signed-off-by: ehanoc --- ARCs/arc-0052.md | 31 +++++++++---------------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/ARCs/arc-0052.md b/ARCs/arc-0052.md index 7241af58c..e4d9605fe 100644 --- a/ARCs/arc-0052.md +++ b/ARCs/arc-0052.md @@ -76,8 +76,8 @@ This extension is **OPTIONAL** and separate from the current interface that hand interface Wallet { ... - keyGen(context: KeyContext, name: string): Promise; - sign(context: KeyContext, name: string, message: Uint8Array): Promise; + keyGen(context: KeyContext, account:number, keyIndex: number): Promise; + signData(context: KeyContext, account:number, keyIndex: number, message: Uint8Array): Promise; ... } ``` @@ -91,17 +91,15 @@ In order to derive a key, the wallet should lookup the different derivation path - **method name**: `keyGen` - **parameters**: - `context`: The context of the key to be generated - - `type` (optional): The type of key to be generated. Possible values are `p-256`, `ed25519` - - Default value: `ed25519` - - `options`(optional): - - `index`: The index of the key to be generated. If not provided, the wallet should derive the next key in the derivation path. - - `key-identifier`(optional): Optional parameter to give the key a rememberable name (i.e "my-did"). Note: this wouldn't be recoverable from the mnemonic. + - `account`: The account index to be used for key derivation. The value value should be hardened as per BIP44 + - `keyIndex`: The key index to be used for key derivation. + - **returns**: Byte array cointaing the public key, which is a point on the Ed25519 elliptic curve. The public key is encoded as compressed EC point: the y-coordinate, combined with the lowest bit (the parity) of the x-coordinate, making up 32 bytes. ###### Example ```ts -const identityKey: Uint8Array = await wallet.keyGen(KeyContext.Identity, "ed25519", { index: 0, key-identifier: "myGamingId" }) +const identityKey: Uint8Array = await wallet.keyGen(KeyContext.Identity, 0, 0) ``` ##### sign @@ -115,26 +113,15 @@ Signing operation that can be used to perform arbitrary signing operations in a - **parameters**: - `data`: The data to be signed - `context`: The context of the key to be used for signing + - `account`: The account index to be used for key derivation. The value value should be hardened as per BIP44 + - `keyIndex`: The key index to be used for key derivation. - `metadata`: Object to describe the type of data that is being signed. It should specify the encoding used (i.e msgpack, cbor, etc) and a schema so that the wallet can decode it to show the user. - `encoding`: The encoding used for the data - `schema`: The schema of the data in the form of a JSON schema - - `type`(optional): The key type to be used for signing. Possible values are `ecdsa-p256`, `ed25519` - - Default value: `ed25519` - **returns**: Byte array containing the signature. The signature is encoded as a 64-byte array (32 + 32 bytes). It holds a compressed point R + the integer s (confirming that the signer knows the msg and the privKey). + - Throws error if data doesn't match the schema -###### Example - -```ts -// Example -> sign auth challenge request with an Address Key - -const authChallenge: Uint8Array = await wallet.sign("my-auth-challenge", KeyContext.Address, { encoding: "cbor", schema: { type: "object", properties: { challenge: { type: "string" } } } }, "ed25519", { index: 0 }) - -``` - - -## Security Considerations -TBC ## Copyright Copyright and related rights waived via CCO. \ No newline at end of file From 048e50848960c87806282e411270161103a10a4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane?= Date: Wed, 25 Oct 2023 14:50:20 +0200 Subject: [PATCH 06/47] Fix lint + add Rational, Reference Implementation & Security Consideration --- ARCs/arc-0052.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/ARCs/arc-0052.md b/ARCs/arc-0052.md index e4d9605fe..00ea48d57 100644 --- a/ARCs/arc-0052.md +++ b/ARCs/arc-0052.md @@ -2,10 +2,7 @@ arc: 52 title: Wallet Contextual KeyGen and Signing description: Algorand Wallet API for Contextual KeyGen and Signing -authors: - - Bruno Martins (@ehanoc, bruno.martins@algorand.foundation) - - Pattrick (patrick@txnlab.dev) - - Kevin W(kevin@blockshake.io) +author: Bruno Martins (@ehanoc), Patrick Bennett (@pbennett), Kevin Wellenzohn (@k13n) discussions-to: https://github.com/algorandfoundation/ARCs/issues/239 status: Draft type: Standards Track @@ -121,7 +118,14 @@ Signing operation that can be used to perform arbitrary signing operations in a - **returns**: Byte array containing the signature. The signature is encoded as a 64-byte array (32 + 32 bytes). It holds a compressed point R + the integer s (confirming that the signer knows the msg and the privKey). - Throws error if data doesn't match the schema +## Rationale +The rationale fleshes out the specification by describing what motivated the design and why particular design decisions were made. It should describe alternate designs that were considered and related work, e.g., how the feature is supported in other languages. The rationale may also provide evidence of consensus within the community and should discuss significant objections or concerns raised during discussions. +## Reference Implementation +Reference Implementation - An optional section that contains a reference/example implementation that people can use to assist in understanding or implementing this specification. + +## Security Considerations +A section that discusses the security implications/considerations relevant to the proposed change. Include information that might be important for security discussions, surfaces risks, and can be used throughout the life-cycle of the proposal. E.g., include security-relevant design decisions, concerns, essential discussions, implementation-specific guidance and pitfalls, an outline of threats and risks, and how they are being addressed. ## Copyright Copyright and related rights waived via CCO. \ No newline at end of file From 76899c7afba470d3c647d60ba4939ae21930cf42 Mon Sep 17 00:00:00 2001 From: ehanoc Date: Fri, 27 Oct 2023 12:04:35 +0000 Subject: [PATCH 07/47] ECDH section and examples added Signed-off-by: ehanoc --- ARCs/arc-0052.md | 138 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 110 insertions(+), 28 deletions(-) diff --git a/ARCs/arc-0052.md b/ARCs/arc-0052.md index 00ea48d57..9bcebca25 100644 --- a/ARCs/arc-0052.md +++ b/ARCs/arc-0052.md @@ -21,7 +21,7 @@ This document specifies an expansion of the Algorand Wallet API to support conte At it's core, blockchain keys and signatures are just implementations of cryptographic primitives. These primitives can be used for a wide variety of use cases, not just for the purpose of signing transactions. And wallets being applications that contain or serve as Key Management Systems (KMS), should be able to support a multiple of other use cases that are not just related to transactions. This creates the possibility of attaching on-chain behavior with off-chain behavior, allowing for example to concepts of Identity and Authentication. -The current wallet APIs available only support key derivation for addresses and signing for transactions. In the broader context of Algorand and web3 ecosystems, there is a need for a more flexible API that allows for contextual key generation and signing. An example of this could be identity dedicated keys (i.e DIDs), or passkeys for authentication. +The current wallet APIs available only support key derivation for addresses and signing for transactions. In the broader context of Algorand and web3 ecosystems, there is a need for a more flexible API that allows for contextual key generation and signing. An example of this could be identity dedicated keys (i.e DIDs), or passkeys for authentication. ## Specification @@ -33,12 +33,11 @@ The key words "**MUST**", "**MUST NOT**", "**REQUIRED**", "**SHALL**", "**SHALL > This overview section is non-normative. -At a high level this specification defines a new API to generate keys and signing in other contexts than transactions. +At a high level this specification defines a new API to generate keys and signing in other contexts than transactions. > Interfaces are defined in TypeScript. All the objects that are defined are valid JSON objects. - -#### Contexts +#### Contexts An enum of contexts that are supported by the wallet. This enum is extensible and can be expanded in the future to support more contexts (i.e messaging, ephemeral keys, other chain derivations, etc) @@ -52,14 +51,14 @@ enum KeyContext { #### BIP44 Context Derivation Paths -There is several reasons why we want to avoid key re-use in different contexts. One of the reasons is that we want to avoid the possibility of **leaking** information about the identity of the user. Another reason is that there is some cryptographic security concers when re-using keys or in case of keys being compromised the impact is **compartmentalized**. +There is several reasons why we want to avoid key re-use in different contexts. One of the reasons is that we want to avoid the possibility of **leaking** information about the identity of the user. Another reason is that there is some cryptographic security concers when re-using keys or in case of keys being compromised the impact is **compartmentalized**. -For this reason is advisable to have different derivation paths for different contexts supporting recoveribility of the keys by using the same mnemonic as before. +For this reason is advisable to have different derivation paths for different contexts supporting recoveribility of the keys by using the same mnemonic as before. -| Purpose | Coin Type | Account | Change | Address Index | Context | -| :-----: | :-------: | :-----: | :----: | :-----------: | :-----------------: | -| 44' | 283' | x' | x | x | Addresses | -| 44' | 0' | x' | x | x | Identity Keys | +| Purpose | Coin Type | Account | Change | Address Index | Context | +| :-----: | :-------: | :-----: | :----: | :-----------: | :-----------: | +| 44' | 283' | x' | x | x | Addresses | +| 44' | 0' | x' | x | x | Identity Keys | see `arc-44` Support (BIP44) @@ -70,11 +69,11 @@ This new interface is for the wallet to implement. It is an extension of the cur This extension is **OPTIONAL** and separate from the current interface that handles address generation and transaction signing. ```ts - interface Wallet { ... keyGen(context: KeyContext, account:number, keyIndex: number): Promise; signData(context: KeyContext, account:number, keyIndex: number, message: Uint8Array): Promise; + ECDH(context: KeyContext, account: number, keyIndex: number, otherPartyPub: Uint8Array): Promise ... } ``` @@ -87,45 +86,128 @@ In order to derive a key, the wallet should lookup the different derivation path - **method name**: `keyGen` - **parameters**: - - `context`: The context of the key to be generated - - `account`: The account index to be used for key derivation. The value value should be hardened as per BIP44 - - `keyIndex`: The key index to be used for key derivation. -- **returns**: Byte array cointaing the public key, which is a point on the Ed25519 elliptic curve. The public key is encoded as compressed EC point: the y-coordinate, combined with the lowest bit (the parity) of the x-coordinate, making up 32 bytes. + - `context`: The context of the key to be generated + - `account`: The account index to be used for key derivation. The value value should be hardened as per BIP44 + - `keyIndex`: The key index to be used for key derivation. + +- **returns**: Byte array cointaing the public key, which is a point on the Ed25519 elliptic curve. The public key is encoded as compressed EC point: the y-coordinate, combined with the lowest bit (the parity) of the x-coordinate, making up 32 bytes. ###### Example ```ts -const identityKey: Uint8Array = await wallet.keyGen(KeyContext.Identity, 0, 0) - +const identityKey: Uint8Array = await wallet.keyGen(KeyContext.Identity, 0, 0); ``` + ##### sign -Signing operation that can be used to perform arbitrary signing operations in a given context. These can be for example: +Signing operation that can be used to perform arbitrary signing operations in a given context. These can be for example: - Identity signatures (Documents, DIDs, etc) - Address signatures (i.e Signing auth challenges) - **method name**: `sign` - **parameters**: - - `data`: The data to be signed - - `context`: The context of the key to be used for signing - - `account`: The account index to be used for key derivation. The value value should be hardened as per BIP44 - - `keyIndex`: The key index to be used for key derivation. - - `metadata`: Object to describe the type of data that is being signed. It should specify the encoding used (i.e msgpack, cbor, etc) and a schema so that the wallet can decode it to show the user. - - `encoding`: The encoding used for the data - - `schema`: The schema of the data in the form of a JSON schema -- **returns**: Byte array containing the signature. The signature is encoded as a 64-byte array (32 + 32 bytes). It holds a compressed point R + the integer s (confirming that the signer knows the msg and the privKey). - - Throws error if data doesn't match the schema + - `data`: The data to be signed + - `context`: The context of the key to be used for signing + - `account`: The account index to be used for key derivation. The value value should be hardened as per BIP44 + - `keyIndex`: The key index to be used for key derivation. + - `metadata`: Object to describe the type of data that is being signed. It should specify the encoding used (i.e msgpack, cbor, etc) and a schema so that the wallet can decode it to show the user. + - `encoding`: The encoding used for the data + - `schema`: The schema of the data in the form of a JSON schema +- **returns**: Byte array containing the signature. The signature is encoded as a 64-byte array (32 + 32 bytes). It holds a compressed point R + the integer s (confirming that the signer knows the msg and the privKey). + - Throws error if data doesn't match the schema + +###### Example + +```ts +const message = { + letter: "Hello World", +}; + +const encoded: Buffer = Buffer.from(to_base64(JSON.stringify(message))); + +// Schema of what we are signing +const jsonSchema = { + type: "object", + properties: { + letter: { + type: "string", + }, + }, +}; + +const metadata: SignMetadata = { + encoding: Encoding.BASE64, + schema: jsonSchema, +}; + +const signature: Uint8Array = await cryptoService.signData( + KeyContext.Address, + 0, + 0, + encoded, + metadata +); +expect(signature).toHaveLength(64); +``` + +##### ECDH (Elliptic Curve Diffie-Hellman) + +This operation is used to perform a secret key aggrement between two parties. This is useful for example for deriving a shared symmetric secret that is only known between the participants. + +The symmetric secret can be used for encrypting and decrypting messages. + +- **method name**: `ECDH` +- **parameters**: + - `context`: The context of the key to be used for signing + - `account`: The account index to be used for key derivation. The value value should be hardened as per BIP44 + - `keyIndex`: The key index to be used for key derivation. + - `otherPartyPub`: The public key of the other party +- **returns**: Byte array containing the shared secret. The shared secret is a 32-byte array. + +###### Example + +```ts +const aliceKey: Uint8Array = await cryptoService.keyGen( + KeyContext.Address, + 0, + 0 +); + +const bobKey: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 1); + +const aliceSharedSecret: Uint8Array = await cryptoService.ECDH( + KeyContext.Address, + 0, + 0, + bobKey +); + +const bobSharedSecret: Uint8Array = await cryptoService.ECDH( + KeyContext.Address, + 0, + 1, + aliceKey +); + +// Same secret can be used to encrypt by Alice and decrypt by Bob +//and vice-versa +expect(aliceSharedSecret).toEqual(bobSharedSecret); +``` ## Rationale + The rationale fleshes out the specification by describing what motivated the design and why particular design decisions were made. It should describe alternate designs that were considered and related work, e.g., how the feature is supported in other languages. The rationale may also provide evidence of consensus within the community and should discuss significant objections or concerns raised during discussions. ## Reference Implementation + Reference Implementation - An optional section that contains a reference/example implementation that people can use to assist in understanding or implementing this specification. ## Security Considerations + A section that discusses the security implications/considerations relevant to the proposed change. Include information that might be important for security discussions, surfaces risks, and can be used throughout the life-cycle of the proposal. E.g., include security-relevant design decisions, concerns, essential discussions, implementation-specific guidance and pitfalls, an outline of threats and risks, and how they are being addressed. ## Copyright -Copyright and related rights waived via CCO. \ No newline at end of file + +Copyright and related rights waived via CCO. From 3f4fc7120966f26f675df680d481fe8fb204682c Mon Sep 17 00:00:00 2001 From: ehanoc Date: Fri, 27 Oct 2023 12:12:31 +0000 Subject: [PATCH 08/47] Add BIP44 link Signed-off-by: ehanoc --- ARCs/arc-0052.md | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/ARCs/arc-0052.md b/ARCs/arc-0052.md index 9bcebca25..964f3adf9 100644 --- a/ARCs/arc-0052.md +++ b/ARCs/arc-0052.md @@ -17,7 +17,7 @@ requires: 44 This document specifies an expansion of the Algorand Wallet API to support contextual key generation and signing for **non-algorand-transaction** contexts. -## Motivation +## Rationale At it's core, blockchain keys and signatures are just implementations of cryptographic primitives. These primitives can be used for a wide variety of use cases, not just for the purpose of signing transactions. And wallets being applications that contain or serve as Key Management Systems (KMS), should be able to support a multiple of other use cases that are not just related to transactions. This creates the possibility of attaching on-chain behavior with off-chain behavior, allowing for example to concepts of Identity and Authentication. @@ -60,7 +60,7 @@ For this reason is advisable to have different derivation paths for different co | 44' | 283' | x' | x | x | Addresses | | 44' | 0' | x' | x | x | Identity Keys | -see `arc-44` Support (BIP44) +see `BIP-44` #### Wallet New Interface @@ -196,17 +196,13 @@ const bobSharedSecret: Uint8Array = await cryptoService.ECDH( expect(aliceSharedSecret).toEqual(bobSharedSecret); ``` -## Rationale - -The rationale fleshes out the specification by describing what motivated the design and why particular design decisions were made. It should describe alternate designs that were considered and related work, e.g., how the feature is supported in other languages. The rationale may also provide evidence of consensus within the community and should discuss significant objections or concerns raised during discussions. - ## Reference Implementation Reference Implementation - An optional section that contains a reference/example implementation that people can use to assist in understanding or implementing this specification. ## Security Considerations -A section that discusses the security implications/considerations relevant to the proposed change. Include information that might be important for security discussions, surfaces risks, and can be used throughout the life-cycle of the proposal. E.g., include security-relevant design decisions, concerns, essential discussions, implementation-specific guidance and pitfalls, an outline of threats and risks, and how they are being addressed. +As this functionality is based on EdDSA, BIP32 HD derivations and ECDH, the security considerations of those specifications apply. Particularly, the Ed25519 derived keys must be clamped to avoid known attacks ## Copyright From aa256ab315ac3256019eb50255a5d846ffb7565f Mon Sep 17 00:00:00 2001 From: ehanoc Date: Fri, 27 Oct 2023 12:14:15 +0000 Subject: [PATCH 09/47] Rationale section after spec Signed-off-by: ehanoc --- ARCs/arc-0052.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ARCs/arc-0052.md b/ARCs/arc-0052.md index 964f3adf9..c252b10d4 100644 --- a/ARCs/arc-0052.md +++ b/ARCs/arc-0052.md @@ -17,12 +17,6 @@ requires: 44 This document specifies an expansion of the Algorand Wallet API to support contextual key generation and signing for **non-algorand-transaction** contexts. -## Rationale - -At it's core, blockchain keys and signatures are just implementations of cryptographic primitives. These primitives can be used for a wide variety of use cases, not just for the purpose of signing transactions. And wallets being applications that contain or serve as Key Management Systems (KMS), should be able to support a multiple of other use cases that are not just related to transactions. This creates the possibility of attaching on-chain behavior with off-chain behavior, allowing for example to concepts of Identity and Authentication. - -The current wallet APIs available only support key derivation for addresses and signing for transactions. In the broader context of Algorand and web3 ecosystems, there is a need for a more flexible API that allows for contextual key generation and signing. An example of this could be identity dedicated keys (i.e DIDs), or passkeys for authentication. - ## Specification The key words "**MUST**", "**MUST NOT**", "**REQUIRED**", "**SHALL**", "**SHALL NOT**", "**SHOULD**", "**SHOULD NOT**", "**RECOMMENDED**", "**MAY**", and "**OPTIONAL**" in this document are to be interpreted as described in RFC-2119. @@ -196,6 +190,12 @@ const bobSharedSecret: Uint8Array = await cryptoService.ECDH( expect(aliceSharedSecret).toEqual(bobSharedSecret); ``` +## Rationale + +At it's core, blockchain keys and signatures are just implementations of cryptographic primitives. These primitives can be used for a wide variety of use cases, not just for the purpose of signing transactions. And wallets being applications that contain or serve as Key Management Systems (KMS), should be able to support a multiple of other use cases that are not just related to transactions. This creates the possibility of attaching on-chain behavior with off-chain behavior, allowing for example to concepts of Identity and Authentication. + +The current wallet APIs available only support key derivation for addresses and signing for transactions. In the broader context of Algorand and web3 ecosystems, there is a need for a more flexible API that allows for contextual key generation and signing. An example of this could be identity dedicated keys (i.e DIDs), or passkeys for authentication. + ## Reference Implementation Reference Implementation - An optional section that contains a reference/example implementation that people can use to assist in understanding or implementing this specification. From 1fb8d83ec14c8c8c29260aa0295fb735a4dd7b59 Mon Sep 17 00:00:00 2001 From: ehanoc Date: Fri, 27 Oct 2023 12:15:17 +0000 Subject: [PATCH 10/47] Remove dependency on ARC44 Signed-off-by: ehanoc --- ARCs/arc-0052.md | 1 - 1 file changed, 1 deletion(-) diff --git a/ARCs/arc-0052.md b/ARCs/arc-0052.md index c252b10d4..e4076ad68 100644 --- a/ARCs/arc-0052.md +++ b/ARCs/arc-0052.md @@ -8,7 +8,6 @@ status: Draft type: Standards Track category: Interface created: 2023-06-30 -requires: 44 --- # Algorand Wallet Contextual KeyGen and Signing From 0107c5c4c63377b823711f6189a51abd84cf1e21 Mon Sep 17 00:00:00 2001 From: ehanoc Date: Fri, 27 Oct 2023 12:27:36 +0000 Subject: [PATCH 11/47] Add reference implementation Signed-off-by: ehanoc --- assets/arc-0052/.gitignore | 1 + assets/arc-0052/README.md | 56 + assets/arc-0052/contextual.api.crypto.spec.ts | 266 ++ assets/arc-0052/contextual.api.crypto.ts | 278 ++ assets/arc-0052/package.json | 51 + assets/arc-0052/tsconfig.json | 16 + assets/arc-0052/yarn.lock | 2347 +++++++++++++++++ 7 files changed, 3015 insertions(+) create mode 100644 assets/arc-0052/.gitignore create mode 100644 assets/arc-0052/README.md create mode 100644 assets/arc-0052/contextual.api.crypto.spec.ts create mode 100644 assets/arc-0052/contextual.api.crypto.ts create mode 100644 assets/arc-0052/package.json create mode 100644 assets/arc-0052/tsconfig.json create mode 100644 assets/arc-0052/yarn.lock diff --git a/assets/arc-0052/.gitignore b/assets/arc-0052/.gitignore new file mode 100644 index 000000000..c2658d7d1 --- /dev/null +++ b/assets/arc-0052/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/assets/arc-0052/README.md b/assets/arc-0052/README.md new file mode 100644 index 000000000..8d823c1fc --- /dev/null +++ b/assets/arc-0052/README.md @@ -0,0 +1,56 @@ +# ARC52 reference implementation + +This implementation is not meant to be used in production. It is a reference implementation for the ARC52 specification. + +It shows how the ARC52 specification can be implemented on top of a BIP39 / BIP32-ed25519 / BIP44 wallet. + +note: Bip32-ed25519 is meant to be compatible with our Ledger implementation. Meaning that we only do hard-derivation at the level of the `account` as per BIP44 + +## Run + +```shell +$ yarn +$ yarn test +``` + +## Output + +```shell + PASS ./contextual.api.crypto.spec.ts (5.393 s) + Contextual Derivation & Signing + (Derivations) Context + ✓ (OK) ECDH (342 ms) + ✓ (OK) ECDH, Encrypt and Decrypt (365 ms) + ✓ Libsodium example ECDH (4 ms) + Addresses + Soft Derivations + ✓ (OK) Derive m'/44'/283'/0'/0/0 Algorand Address Key (152 ms) + ✓ (OK) Derive m'/44'/283'/0'/0/1 Algorand Address Key (115 ms) + ✓ (OK) Derive m'/44'/283'/0'/0/2 Algorand Address Key (88 ms) + ✓ (OK) Derive m'/44'/283'/3'/0/0 Algorand Address Key (98 ms) + Hard Derivations + ✓ (OK) Derive m'/44'/283'/1'/0/0 Algorand Address Key (94 ms) + ✓ (OK) Derive m'/44'/283'/2'/0/1 Algorand Address Key (96 ms) + Ledger Addresses test Vectors + ✓ (OK) Derive m'/44'/283'/0'/0/0 Algorand Address (94 ms) + ✓ (OK) Derive m'/44'/283'/1'/0/0 Algorand Address (90 ms) + ✓ (OK) Derive m'/44'/283'/2'/0/0 Algorand Address (92 ms) + ✓ (OK) Derive m'/44'/283'/3'/0/0 Algorand Address (91 ms) + ✓ (OK) Derive m'/44'/283'/4'/0/0 Algorand Address (100 ms) + ✓ (OK) Derive m'/44'/283'/5'/0/0 Algorand Address (93 ms) + Identities + Soft Derivations + ✓ (OK) Derive m'/44'/0'/0'/0/0 Identity Key (95 ms) + ✓ (OK) Derive m'/44'/0'/0'/0/1 Identity Key (94 ms) + ✓ (OK) Derive m'/44'/0'/0'/0/2 Identity Key (91 ms) + Hard Derivations + ✓ (OK) Derive m'/44'/0'/1'/0/0 Identity Key (89 ms) + ✓ (OK) Derive m'/44'/0'/2'/0/1 Identity Key (95 ms) + Signing Typed Data + ✓ (OK) Sign Arbitrary Message against Schem (232 ms) + ✓ (FAIL) Signing attempt fails because of invalid data against Schema (38 ms) + +Test Suites: 1 passed, 1 total +Tests: 22 passed, 22 total + +``` \ No newline at end of file diff --git a/assets/arc-0052/contextual.api.crypto.spec.ts b/assets/arc-0052/contextual.api.crypto.spec.ts new file mode 100644 index 000000000..2dd510950 --- /dev/null +++ b/assets/arc-0052/contextual.api.crypto.spec.ts @@ -0,0 +1,266 @@ +import { CryptoKX, KeyPair, crypto_kx_client_session_keys, crypto_kx_server_session_keys, crypto_scalarmult, crypto_scalarmult_ed25519_base_noclamp, crypto_secretbox_NONCEBYTES, crypto_secretbox_easy, crypto_secretbox_open_easy, crypto_sign_ed25519_pk_to_curve25519, crypto_sign_ed25519_sk_to_curve25519, crypto_sign_keypair, crypto_sign_seed_keypair, ready, to_base64 } from "libsodium-wrappers-sumo" +import * as bip39 from "bip39" +import { sha512_256 } from "js-sha512" +import base32 from "hi-base32" +import { randomBytes } from "crypto" +import { ContextualCryptoApi, Encoding, KeyContext, SignMetadata } from "./contextual.api.crypto" + +/** + * @param publicKey + * @returns + */ +function encodeAddress(publicKey: Buffer): string { + const keyHash: string = sha512_256.create().update(publicKey).hex() + + // last 4 bytes of the hash + const checksum: string = keyHash.slice(-8) + + return base32.encode(ConcatArrays(publicKey, Buffer.from(checksum, "hex"))).slice(0, 58) +} + +function ConcatArrays(...arrs: ArrayLike[]) { + const size = arrs.reduce((sum, arr) => sum + arr.length, 0) + const c = new Uint8Array(size) + + let offset = 0 + for (let i = 0; i < arrs.length; i++) { + c.set(arrs[i], offset) + offset += arrs[i].length + } + + return c +} + +describe("Contextual Derivation & Signing", () => { + + let cryptoService: ContextualCryptoApi + let bip39Mnemonic: string = "salon zoo engage submit smile frost later decide wing sight chaos renew lizard rely canal coral scene hobby scare step bus leaf tobacco slice" + let seed: Buffer + + beforeAll(() => { + seed = bip39.mnemonicToSeedSync(bip39Mnemonic, "") + }) + + beforeEach(() => { + cryptoService = new ContextualCryptoApi(seed) + }) + + afterEach(() => {}) + + describe("\(Derivations) Context", () => { + describe("Addresses", () => { + describe("Soft Derivations", () => { + it("\(OK) Derive m'/44'/283'/0'/0/0 Algorand Address Key", async () => { + const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 0) + expect(key).toEqual(new Uint8Array(Buffer.from("9183c50768a724c5b77a4ddf56d7da2f204d56da68ced53626222ace25b25cd8", "hex"))) + }) + + it("\(OK) Derive m'/44'/283'/0'/0/1 Algorand Address Key", async () => { + const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 1) + expect(key).toEqual(new Uint8Array(Buffer.from("15965b321e69bfb6386a1dc31925ee608f8fdc9f272442e4a57e47508390a714", "hex"))) + }) + + it("\(OK) Derive m'/44'/283'/0'/0/2 Algorand Address Key", async () => { + const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 2) + expect(key).toEqual(new Uint8Array(Buffer.from("af1bb9cba94b7a7569c186ecffe881b01bd2cbded23418d2fb51c7ffe675502c", "hex"))) + }) + + it("\(OK) Derive m'/44'/283'/3'/0/0 Algorand Address Key", async () => { + const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 3, 0) + expect(key).toEqual(new Uint8Array(Buffer.from("d81624d97ef08248724eb2432a514efb857d2705783dcb012ede40fcaaeadcd0", "hex"))) + }) + }) + + describe("Hard Derivations", () => { + it("\(OK) Derive m'/44'/283'/1'/0/0 Algorand Address Key", async () => { + const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 1, 0) + expect(key).toEqual(new Uint8Array(Buffer.from("ccca8fd695161983b306bb0756f674d0b437d77b4bdf2b546ab2ecf2890270dd", "hex"))) + }) + + it("\(OK) Derive m'/44'/283'/2'/0/1 Algorand Address Key", async () => { + const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 2, 1) + expect(key).toEqual(new Uint8Array(Buffer.from("d9675a7ce320937ce756617f60a73a72d30de183277bc8ecf8f923787f4f9875", "hex"))) + }) + + describe("Ledger Addresses test Vectors", ()=> { + it("\(OK) Derive m'/44'/283'/0'/0/0 Algorand Address", async () => { + const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 0) + const algorandAddress: string = encodeAddress(Buffer.from(key)) + expect(algorandAddress).toEqual("SGB4KB3IU4SMLN32JXPVNV62F4QE2VW2NDHNKNRGEIVM4JNSLTMJV53P6I") + }) + + it("\(OK) Derive m'/44'/283'/1'/0/0 Algorand Address", async () => { + const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 1, 0) + const algorandAddress: string = encodeAddress(Buffer.from(key)) + expect(algorandAddress).toEqual("ZTFI7VUVCYMYHMYGXMDVN5TU2C2DPV33JPPSWVDKWLWPFCICODO6HWLPLI") + }) + + it("\(OK) Derive m'/44'/283'/2'/0/0 Algorand Address", async () => { + const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 2, 0) + const algorandAddress: string = encodeAddress(Buffer.from(key)) + expect(algorandAddress).toEqual("PSVABV5X6EGQZIHKZWVUUISWIZZKZ6RFEQ6O3IOOHPAJBSVHNNQENRMGQY") + }) + + it("\(OK) Derive m'/44'/283'/3'/0/0 Algorand Address", async () => { + const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 3, 0) + const algorandAddress: string = encodeAddress(Buffer.from(key)) + expect(algorandAddress).toEqual("3ALCJWL66CBEQ4SOWJBSUUKO7OCX2JYFPA64WAJO3ZAPZKXK3TIPC4J3VQ") + }) + + it("\(OK) Derive m'/44'/283'/4'/0/0 Algorand Address", async () => { + const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 4, 0) + const algorandAddress: string = encodeAddress(Buffer.from(key)) + expect(algorandAddress).toEqual("42ASNPEG4P73PI4DYJDILSX6CKOQ275N3IX6GDLVUMDRIGMBURCFNVXNEI") + }) + + it("\(OK) Derive m'/44'/283'/5'/0/0 Algorand Address", async () => { + const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 5, 0) + const algorandAddress: string = encodeAddress(Buffer.from(key)) + expect(algorandAddress).toEqual("XGLSRHKE2RD4VNOKWIHBR4U2SEGX2H2AN75ZLJJ7VLLXWIZPJPCB4MZUFY") + }) + }) + }) + }) + + describe("Identities", () => { + describe("Soft Derivations", () => { + it("\(OK) Derive m'/44'/0'/0'/0/0 Identity Key", async () => { + const key: Uint8Array = await cryptoService.keyGen(KeyContext.Identity, 0, 0) + expect(key).toEqual(new Uint8Array(Buffer.from("19dc07a51e5be0ce009d751c6d5fbd3a14d3dc4d0fc9c3c6f13e3581a08e4322", "hex"))) + }) + + it("\(OK) Derive m'/44'/0'/0'/0/1 Identity Key", async () => { + const key: Uint8Array = await cryptoService.keyGen(KeyContext.Identity, 0, 1) + expect(key).toEqual(new Uint8Array(Buffer.from("270779072e644fd8575925a49084ecbdec3907ec876ebc9081d11278dea24165", "hex"))) + }) + + it("\(OK) Derive m'/44'/0'/0'/0/2 Identity Key", async () => { + const key: Uint8Array = await cryptoService.keyGen(KeyContext.Identity, 0, 2) + expect(key).toEqual(new Uint8Array(Buffer.from("618d1493f75752fe398f3d7a361d3003d5652ae3446804256a63bbea8e11a179", "hex"))) + }) + }) + + describe("Hard Derivations", () => { + it("\(OK) Derive m'/44'/0'/1'/0/0 Identity Key", async () => { + const key: Uint8Array = await cryptoService.keyGen(KeyContext.Identity, 1, 0) + expect(key).toEqual(new Uint8Array(Buffer.from("9afd78d1456265bbdea1b82e2bc1d31ba64589f53325d69f222375fcd09f02c6", "hex"))) + }) + + it("\(OK) Derive m'/44'/0'/2'/0/1 Identity Key", async () => { + const key: Uint8Array = await cryptoService.keyGen(KeyContext.Identity, 2, 1) + expect(key).toEqual(new Uint8Array(Buffer.from("c3364323ba03483057b08a52087c4d6bae2e26c6dbbb3adce6aa8db627eba2b1", "hex"))) + }) + }) + }) + + describe("Signing Typed Data", () => { + it("\(OK) Sign Arbitrary Message against Schem", async () => { + const firstKey: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 0) + + const message = { + letter: "Hello World" + } + + const encoded: Buffer = Buffer.from(to_base64(JSON.stringify(message))) + + // Schema of what we are signing + const jsonSchema = { + type: "object", + properties: { + letter: { + type: "string" + } + } + } + + const metadata: SignMetadata = { encoding: Encoding.BASE64, schema: jsonSchema } + + const signature: Uint8Array = await cryptoService.signData(KeyContext.Address,0, 0, encoded, metadata) + expect(signature).toHaveLength(64) + + const isValid: boolean = await cryptoService.verifyWithPublicKey(signature, encoded, firstKey) + expect(isValid).toBe(true) + }) + + it("\(FAIL) Signing attempt fails because of invalid data against Schema", async () => { + const message = { + letter: "Hello World" + } + + const encoded: Buffer = Buffer.from(to_base64(JSON.stringify(message))) + + // Schema of what we are signing + const jsonSchema = { + type: "string" + } + + const metadata: SignMetadata = { encoding: Encoding.BASE64, schema: jsonSchema } + expect(cryptoService.signData(KeyContext.Identity,0, 0, encoded, metadata)).rejects.toThrowError() + }) + }) + + + it("\(OK) ECDH", async () => { + const aliceKey: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 0) + const bobKey: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 1) + + const aliceSharedSecret: Uint8Array = await cryptoService.ECDH(KeyContext.Address, 0, 0, bobKey) + const bobSharedSecret: Uint8Array = await cryptoService.ECDH(KeyContext.Address, 0, 1, aliceKey) + + expect(aliceSharedSecret).toEqual(bobSharedSecret) + }) + + it("\(OK) ECDH, Encrypt and Decrypt", async () => { + const aliceKey: Uint8Array = await cryptoService.keyGen(KeyContext.Identity, 0, 0) + const bobKey: Uint8Array = await cryptoService.keyGen(KeyContext.Identity, 0, 1) + + const aliceSharedSecret: Uint8Array = await cryptoService.ECDH(KeyContext.Identity, 0, 0, bobKey) + const bobSharedSecret: Uint8Array = await cryptoService.ECDH(KeyContext.Identity, 0, 1, aliceKey) + + expect(aliceSharedSecret).toEqual(bobSharedSecret) + + const message: Uint8Array = new Uint8Array(Buffer.from("Hello World")) + const nonce: Uint8Array = randomBytes(crypto_secretbox_NONCEBYTES) + + // encrypt + const cipherText: Uint8Array = crypto_secretbox_easy(message, nonce, aliceSharedSecret) + + // decrypt + const plainText: Uint8Array = crypto_secretbox_open_easy(cipherText, nonce, bobSharedSecret) + expect(plainText).toEqual(message) + }) + + it("Libsodium example ECDH", async () => { + await ready + // keypair + const alice: KeyPair = crypto_sign_keypair() + + const alicePvtKey: Uint8Array = alice.privateKey + const alicePubKey: Uint8Array = alice.publicKey + + const aliceXPvt: Uint8Array = crypto_sign_ed25519_sk_to_curve25519(alicePvtKey) + const aliceXPub: Uint8Array = crypto_sign_ed25519_pk_to_curve25519(alicePubKey) + + // bob + const bob: KeyPair = crypto_sign_keypair() + + const bobPvtKey: Uint8Array = bob.privateKey + const bobPubKey: Uint8Array = bob.publicKey + + const bobXPvt: Uint8Array = crypto_sign_ed25519_sk_to_curve25519(bobPvtKey) + const bobXPub: Uint8Array = crypto_sign_ed25519_pk_to_curve25519(bobPubKey) + + // shared secret + const aliceSecret: Uint8Array = crypto_scalarmult(aliceXPvt, bobXPub) + const bobSecret: Uint8Array = crypto_scalarmult(bobXPvt, aliceXPub) + expect(aliceSecret).toEqual(bobSecret) + + const aliceSharedSecret: CryptoKX = crypto_kx_client_session_keys(aliceXPub, aliceXPvt, bobXPub) + const bobSharedSecret: CryptoKX = crypto_kx_server_session_keys(bobXPub, bobXPvt, aliceXPub) + + // bilateral encryption channels + expect(aliceSharedSecret.sharedRx).toEqual(bobSharedSecret.sharedTx) + expect(bobSharedSecret.sharedTx).toEqual(aliceSharedSecret.sharedRx) + }) + }) +}) diff --git a/assets/arc-0052/contextual.api.crypto.ts b/assets/arc-0052/contextual.api.crypto.ts new file mode 100644 index 000000000..e992e4077 --- /dev/null +++ b/assets/arc-0052/contextual.api.crypto.ts @@ -0,0 +1,278 @@ +import { createHash, createHmac } from 'crypto'; +import { + crypto_core_ed25519_scalar_add, + crypto_core_ed25519_scalar_mul, + crypto_core_ed25519_scalar_reduce, + crypto_hash_sha512, + crypto_scalarmult_ed25519_base_noclamp, + crypto_sign_verify_detached, + ready, + crypto_sign_ed25519_pk_to_curve25519, + crypto_scalarmult +} from 'libsodium-wrappers-sumo'; + +import Ajv, { JSONSchemaType } from "ajv" +const bip32ed25519 = require("bip32-ed25519"); + + +/** + * + */ +export enum KeyContext { + Address = 0, + Identity = 1, + Cardano = 2, + TESTVECTOR_1 = 3, + TESTVECTOR_2 = 4, + TESTVECTOR_3 = 5 +} + +export interface ChannelKeys { + tx: Uint8Array + rx: Uint8Array +} + +export enum Encoding { + CBOR = "cbor", + MSGPACK = "msgpack", + BASE64 = "base64" +} + +export interface SignMetadata { + encoding: Encoding + schema: Object +} + +export const harden = (num: number): number => 0x80_00_00_00 + num; + +function GetBIP44PathFromContext(context: KeyContext, account:number, key_index: number): number[] { + switch (context) { + case KeyContext.Address: + return [harden(44), harden(283), harden(account), 0, key_index] + case KeyContext.Identity: + return [harden(44), harden(0), harden(account), 0, key_index] + default: + throw new Error("Invalid context") + } +} + +export class ContextualCryptoApi { + + // Only for testing, seed shouldn't be persisted + constructor(private readonly seed: Buffer) { + + } + + /** + * + * Reference of BIP32-Ed25519 Hierarchical Deterministic Keys over a Non-linear Keyspace (https://acrobat.adobe.com/id/urn:aaid:sc:EU:04fe29b0-ea1a-478b-a886-9bb558a5242a) + * + * @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 + */ + private async rootKey(seed: Buffer): Promise { + // SLIP-0010 + // We should have been using [BIP32-Ed25519 HierarchicalDeterministicKeysoveraNon-linear Keyspace] instead. Which would mean SHA512(seed) + // As in the [Section V].A Root keys. + const c: Buffer = createHmac('sha256', "ed25519 seed").update(Buffer.concat([new Uint8Array([0x1]), seed])).digest() + let I: Buffer = createHmac('sha512', "ed25519 seed").update(seed).digest() + + // split into KL and KR. + // (KL, KR) is the extended private key + let kL = I.subarray(0, 32) + let kR = I.subarray(32, 64) + + // Specific to our Algorand app implementation (Taken from Ledger reference implementation: https://github.com/LedgerHQ/orakolo/blob/0b2d5e669ec61df9a824df9fa1a363060116b490/src/python/orakolo/HDEd25519.py#L130) + // Seems to try to find a rootKey in which the last bits are cleared + // Shouldn't be necessary has keys are expected to be clamped as in the next step + while ((kL[31] & 0b00_10_00_00) != 0) { + I = createHmac('sha512', "ed25519 seed").update(I).digest() + kL = I.subarray(0, 32) + kR = I.subarray(32, 64) + } + + // clamping + // This bit is "compliant" with [BIP32-Ed25519 Hierarchical Deterministic Keys over a Non-linear Keyspace] + //Set the bits in kL as follows: + // little Endianess + kL[0] &= 0b11_11_10_00; // the lowest 3 bits of the first byte of kL are cleared + kL[31] &= 0b01_11_11_11; // the highest bit of the last byte is cleared + kL[31] |= 0b01_00_00_00; // the second highest bit of the last byte is set + + return new Uint8Array(Buffer.concat([kL, kR, c])) + } + + + /** + * 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 - if true, return the private key, otherwise return the public key + * @returns + */ + private async deriveKey(rootKey: Uint8Array, bip44Path: number[], isPrivate: boolean = true): Promise { + let derived = bip32ed25519.derivePrivate(Buffer.from(rootKey), bip44Path[0]) + derived = bip32ed25519.derivePrivate(derived, bip44Path[1]) + derived = bip32ed25519.derivePrivate(derived, bip44Path[2]) + derived = bip32ed25519.derivePrivate(derived, bip44Path[3]) + derived = bip32ed25519.derivePrivate(derived, bip44Path[4]) + + const derivedKl = derived.subarray(0, 32) + const xpvt = createHash('sha512').update(derivedKl).digest() + + // Keys clamped again + // This is specific to our algorand app implementation + // Taken from reference code: https://github.com/Zondax/ledger-algorand/blob/test/tests_zemu/tests/key_derivation.ts#L84 + // But not part of Ledger's reference implementation:https://github.com/LedgerHQ/orakolo/blob/0b2d5e669ec61df9a824df9fa1a363060116b490/src/python/orakolo/HDEd25519.py#L156 + // And not part of [BIP32-Ed25519 Hierarchical Deterministic Keys over a Non-linear Keyspace] derivation spec + xpvt[0] &= 0b11_11_10_00 + xpvt[31] &= 0b01_11_11_11 + xpvt[31] |= 0b01_00_00_00 // This bit set is not in the BIP32-Ed25519 derivation sepc + + const scalar: Uint8Array = xpvt.subarray(0, 32) + return isPrivate ? xpvt : crypto_scalarmult_ed25519_base_noclamp(scalar) + } + + /** + * + * + * @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 + */ + async keyGen(context: KeyContext, account:number, keyIndex: number): Promise { + await ready // libsodium + + const rootKey: Uint8Array = await this.rootKey(this.seed) + const bip44Path: number[] = GetBIP44PathFromContext(context, account, keyIndex) + + return await this.deriveKey(rootKey, bip44Path, false) + } + + /** + * Ref: https://datatracker.ietf.org/doc/html/rfc8032#section-5.1.6 + * + * Edwards-Curve Digital Signature Algorithm (EdDSA) + * + * @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 + * */ + async signData(context: KeyContext, account: number, keyIndex: number, data: Uint8Array, metadata: SignMetadata): Promise { + // validate data + if (!this.validateData(data, metadata)) { + throw new Error("Invalid data") + } + + await ready // libsodium + + const rootKey: Uint8Array = await this.rootKey(this.seed) + const bip44Path: number[] = GetBIP44PathFromContext(context, account, keyIndex) + const raw: Uint8Array = await this.deriveKey(rootKey, bip44Path, true) + + const scalar = raw.slice(0, 32); + const c = raw.slice(32, 64); + + // \(1): pubKey = scalar * G (base point, no clamp) + const publicKey = crypto_scalarmult_ed25519_base_noclamp(scalar); + + // \(2): h = hash(c + msg) mod q + const hash: bigint = Buffer.from(crypto_hash_sha512(Buffer.concat([c, data]))).readBigInt64LE() + + // \(3): r = hash(hash(privKey) + msg) mod q + const q: bigint = BigInt(2n ** 252n + 27742317777372353535851937790883648493n); + const rBigInt = hash % q + const rBString = rBigInt.toString(16) // convert to hex string + + // fill 32 bytes of r + // convert to Uint8Array + const r = new Uint8Array(32) + for (let i = 0; i < rBString.length; i += 2) { + r[i / 2] = parseInt(rBString.substring(i, i + 2), 16); + } + + // \(4): R = r * G (base point, no clamp) + const R = crypto_scalarmult_ed25519_base_noclamp(r) + + let h = crypto_hash_sha512(Buffer.concat([R, publicKey, data])); + h = crypto_core_ed25519_scalar_reduce(h); + + // \(5): S = (r + h * k) mod q + const S = crypto_core_ed25519_scalar_add(r, crypto_core_ed25519_scalar_mul(h, scalar)) + + return Buffer.concat([R, S]); + } + + + /** + * SAMPLE IMPLEMENTATION to show how to validate data with encoding and schema, using base64 as an example + * + * @param message + * @param metadata + * @returns + */ + private validateData(message: Uint8Array, metadata: SignMetadata): boolean { + let decoded: Buffer + switch (metadata.encoding) { + case Encoding.BASE64: + decoded = Buffer.from(message.toString(), 'base64') + break + default: + throw new Error("Invalid encoding") + } + + // validate with schema + const ajv = new Ajv() + const validate = ajv.compile(metadata.schema) + return validate(decoded) + } + + + /** + * Wrapper around libsodium basica 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 + */ + async verifyWithPublicKey(signature: Uint8Array, message: Uint8Array, publicKey: Uint8Array): Promise { + return crypto_sign_verify_detached(signature, message, 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. + * + * @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 + * @returns - raw 32 bytes shared secret + */ + async ECDH(context: KeyContext, account: number, keyIndex: number, otherPartyPub: Uint8Array): Promise { + await ready + + const rootKey: Uint8Array = await this.rootKey(this.seed) + + const bip44Path: number[] = GetBIP44PathFromContext(context, account, keyIndex) + const childKey: Uint8Array = await this.deriveKey(rootKey, bip44Path, true) + + const scalar: Uint8Array = childKey.slice(0, 32) + + return crypto_scalarmult(scalar, crypto_sign_ed25519_pk_to_curve25519(otherPartyPub)) + } +} \ No newline at end of file diff --git a/assets/arc-0052/package.json b/assets/arc-0052/package.json new file mode 100644 index 000000000..f649b227e --- /dev/null +++ b/assets/arc-0052/package.json @@ -0,0 +1,51 @@ +{ + "name": "arc52", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "jest --verbose", + "test:cov": "jest --coverage" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@types/jest": "^29.5.5", + "@types/libsodium-wrappers-sumo": "^0.7.7", + "@types/node": "^20.7.1", + "ts-jest": "^29.1.1", + "typescript": "^5.2.2" + }, + "dependencies": { + "ajv": "^8.12.0", + "bip32-ed25519": "^0.0.4", + "bip39": "^3.1.0", + "hi-base32": "^0.5.1", + "jest": "^29.7.0", + "js-sha512": "^0.8.0", + "libsodium-wrappers-sumo": "^0.7.13", + "ts-custom-error": "^3.2.0", + "ts-log": "^2.2.4" + }, + "jest": { + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], + "rootDir": ".", + "testRegex": ".spec.ts$", + "transform": { + ".+\\.(t|j)s$": "ts-jest" + }, + "coverageDirectory": "../test/coverage", + "collectCoverageFrom": [ + "**/!(*.module|*.interface|main|repl|exception.filter|logging.interceptor).{ts,js}" + ], + "coveragePathIgnorePatterns": [ + "/src/migration/" + ], + "testEnvironment": "node" + } +} diff --git a/assets/arc-0052/tsconfig.json b/assets/arc-0052/tsconfig.json new file mode 100644 index 000000000..736eafde0 --- /dev/null +++ b/assets/arc-0052/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "strict": true, + "strictPropertyInitialization": false, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "ignoreDeprecations": "5.0", + "isolatedModules": true, + "resolveJsonModule": true, + "sourceMap": true, + "declaration": true, + "moduleResolution": "node", + "outDir": "./dist", + "target": "ES2022", + } +} diff --git a/assets/arc-0052/yarn.lock b/assets/arc-0052/yarn.lock new file mode 100644 index 000000000..62eb66144 --- /dev/null +++ b/assets/arc-0052/yarn.lock @@ -0,0 +1,2347 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@ampproject/remapping@^2.2.0": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" + integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.22.13": + version "7.22.13" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" + integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w== + dependencies: + "@babel/highlight" "^7.22.13" + chalk "^2.4.2" + +"@babel/compat-data@^7.22.9": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.23.2.tgz#6a12ced93455827037bfb5ed8492820d60fc32cc" + integrity sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ== + +"@babel/core@^7.11.6", "@babel/core@^7.12.3": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.23.2.tgz#ed10df0d580fff67c5f3ee70fd22e2e4c90a9f94" + integrity sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.22.13" + "@babel/generator" "^7.23.0" + "@babel/helper-compilation-targets" "^7.22.15" + "@babel/helper-module-transforms" "^7.23.0" + "@babel/helpers" "^7.23.2" + "@babel/parser" "^7.23.0" + "@babel/template" "^7.22.15" + "@babel/traverse" "^7.23.2" + "@babel/types" "^7.23.0" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/generator@^7.23.0", "@babel/generator@^7.7.2": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.0.tgz#df5c386e2218be505b34837acbcb874d7a983420" + integrity sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g== + dependencies: + "@babel/types" "^7.23.0" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + jsesc "^2.5.1" + +"@babel/helper-compilation-targets@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz#0698fc44551a26cf29f18d4662d5bf545a6cfc52" + integrity sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw== + dependencies: + "@babel/compat-data" "^7.22.9" + "@babel/helper-validator-option" "^7.22.15" + browserslist "^4.21.9" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-environment-visitor@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" + integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== + +"@babel/helper-function-name@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" + integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== + dependencies: + "@babel/template" "^7.22.15" + "@babel/types" "^7.23.0" + +"@babel/helper-hoist-variables@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" + integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-module-imports@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz#16146307acdc40cc00c3b2c647713076464bdbf0" + integrity sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w== + dependencies: + "@babel/types" "^7.22.15" + +"@babel/helper-module-transforms@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz#3ec246457f6c842c0aee62a01f60739906f7047e" + integrity sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw== + dependencies: + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-module-imports" "^7.22.15" + "@babel/helper-simple-access" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/helper-validator-identifier" "^7.22.20" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.8.0": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz#dd7ee3735e8a313b9f7b05a773d892e88e6d7295" + integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg== + +"@babel/helper-simple-access@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz#4938357dc7d782b80ed6dbb03a0fba3d22b1d5de" + integrity sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-split-export-declaration@^7.22.6": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" + integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-string-parser@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" + integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== + +"@babel/helper-validator-identifier@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" + integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== + +"@babel/helper-validator-option@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz#694c30dfa1d09a6534cdfcafbe56789d36aba040" + integrity sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA== + +"@babel/helpers@^7.23.2": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.23.2.tgz#2832549a6e37d484286e15ba36a5330483cac767" + integrity sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ== + dependencies: + "@babel/template" "^7.22.15" + "@babel/traverse" "^7.23.2" + "@babel/types" "^7.23.0" + +"@babel/highlight@^7.22.13": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54" + integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg== + dependencies: + "@babel/helper-validator-identifier" "^7.22.20" + chalk "^2.4.2" + js-tokens "^4.0.0" + +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.22.15", "@babel/parser@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719" + integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw== + +"@babel/plugin-syntax-async-generators@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-bigint@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea" + integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.8.3": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" + integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== + dependencies: + "@babel/helper-plugin-utils" "^7.12.13" + +"@babel/plugin-syntax-import-meta@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" + integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-json-strings@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-jsx@^7.7.2": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz#a6b68e84fb76e759fc3b93e901876ffabbe1d918" + integrity sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-syntax-logical-assignment-operators@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.8.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-object-rest-spread@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-top-level-await@^7.8.3": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" + integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-typescript@^7.7.2": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.22.5.tgz#aac8d383b062c5072c647a31ef990c1d0af90272" + integrity sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/template@^7.22.15", "@babel/template@^7.3.3": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" + integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w== + dependencies: + "@babel/code-frame" "^7.22.13" + "@babel/parser" "^7.22.15" + "@babel/types" "^7.22.15" + +"@babel/traverse@^7.23.2": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8" + integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw== + dependencies: + "@babel/code-frame" "^7.22.13" + "@babel/generator" "^7.23.0" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/parser" "^7.23.0" + "@babel/types" "^7.23.0" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.3.3": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb" + integrity sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg== + dependencies: + "@babel/helper-string-parser" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.20" + to-fast-properties "^2.0.0" + +"@bcoe/v8-coverage@^0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" + integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== + +"@istanbuljs/load-nyc-config@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" + integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== + dependencies: + camelcase "^5.3.1" + find-up "^4.1.0" + get-package-type "^0.1.0" + js-yaml "^3.13.1" + resolve-from "^5.0.0" + +"@istanbuljs/schema@^0.1.2": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" + integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== + +"@jest/console@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.7.0.tgz#cd4822dbdb84529265c5a2bdb529a3c9cc950ffc" + integrity sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg== + dependencies: + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + slash "^3.0.0" + +"@jest/core@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.7.0.tgz#b6cccc239f30ff36609658c5a5e2291757ce448f" + integrity sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg== + dependencies: + "@jest/console" "^29.7.0" + "@jest/reporters" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + ci-info "^3.2.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + jest-changed-files "^29.7.0" + jest-config "^29.7.0" + jest-haste-map "^29.7.0" + jest-message-util "^29.7.0" + jest-regex-util "^29.6.3" + jest-resolve "^29.7.0" + jest-resolve-dependencies "^29.7.0" + jest-runner "^29.7.0" + jest-runtime "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" + jest-validate "^29.7.0" + jest-watcher "^29.7.0" + micromatch "^4.0.4" + pretty-format "^29.7.0" + slash "^3.0.0" + strip-ansi "^6.0.0" + +"@jest/environment@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.7.0.tgz#24d61f54ff1f786f3cd4073b4b94416383baf2a7" + integrity sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw== + dependencies: + "@jest/fake-timers" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + jest-mock "^29.7.0" + +"@jest/expect-utils@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.7.0.tgz#023efe5d26a8a70f21677d0a1afc0f0a44e3a1c6" + integrity sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA== + dependencies: + jest-get-type "^29.6.3" + +"@jest/expect@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.7.0.tgz#76a3edb0cb753b70dfbfe23283510d3d45432bf2" + integrity sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ== + dependencies: + expect "^29.7.0" + jest-snapshot "^29.7.0" + +"@jest/fake-timers@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.7.0.tgz#fd91bf1fffb16d7d0d24a426ab1a47a49881a565" + integrity sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ== + dependencies: + "@jest/types" "^29.6.3" + "@sinonjs/fake-timers" "^10.0.2" + "@types/node" "*" + jest-message-util "^29.7.0" + jest-mock "^29.7.0" + jest-util "^29.7.0" + +"@jest/globals@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.7.0.tgz#8d9290f9ec47ff772607fa864ca1d5a2efae1d4d" + integrity sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/expect" "^29.7.0" + "@jest/types" "^29.6.3" + jest-mock "^29.7.0" + +"@jest/reporters@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.7.0.tgz#04b262ecb3b8faa83b0b3d321623972393e8f4c7" + integrity sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg== + dependencies: + "@bcoe/v8-coverage" "^0.2.3" + "@jest/console" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@jridgewell/trace-mapping" "^0.3.18" + "@types/node" "*" + chalk "^4.0.0" + collect-v8-coverage "^1.0.0" + exit "^0.1.2" + glob "^7.1.3" + graceful-fs "^4.2.9" + istanbul-lib-coverage "^3.0.0" + istanbul-lib-instrument "^6.0.0" + istanbul-lib-report "^3.0.0" + istanbul-lib-source-maps "^4.0.0" + istanbul-reports "^3.1.3" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + jest-worker "^29.7.0" + slash "^3.0.0" + string-length "^4.0.1" + strip-ansi "^6.0.0" + v8-to-istanbul "^9.0.1" + +"@jest/schemas@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" + integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== + dependencies: + "@sinclair/typebox" "^0.27.8" + +"@jest/source-map@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.6.3.tgz#d90ba772095cf37a34a5eb9413f1b562a08554c4" + integrity sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw== + dependencies: + "@jridgewell/trace-mapping" "^0.3.18" + callsites "^3.0.0" + graceful-fs "^4.2.9" + +"@jest/test-result@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.7.0.tgz#8db9a80aa1a097bb2262572686734baed9b1657c" + integrity sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA== + dependencies: + "@jest/console" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/istanbul-lib-coverage" "^2.0.0" + collect-v8-coverage "^1.0.0" + +"@jest/test-sequencer@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz#6cef977ce1d39834a3aea887a1726628a6f072ce" + integrity sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw== + dependencies: + "@jest/test-result" "^29.7.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + slash "^3.0.0" + +"@jest/transform@^29.7.0": + version "29.7.0" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.7.0.tgz#df2dd9c346c7d7768b8a06639994640c642e284c" + integrity sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw== + dependencies: + "@babel/core" "^7.11.6" + "@jest/types" "^29.6.3" + "@jridgewell/trace-mapping" "^0.3.18" + babel-plugin-istanbul "^6.1.1" + chalk "^4.0.0" + convert-source-map "^2.0.0" + fast-json-stable-stringify "^2.1.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + jest-regex-util "^29.6.3" + jest-util "^29.7.0" + micromatch "^4.0.4" + pirates "^4.0.4" + slash "^3.0.0" + write-file-atomic "^4.0.2" + +"@jest/types@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.3.tgz#1131f8cf634e7e84c5e77bab12f052af585fba59" + integrity sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw== + dependencies: + "@jest/schemas" "^29.6.3" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + +"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" + integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" + integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== + +"@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.20" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz#72e45707cf240fa6b081d0366f8265b0cd10197f" + integrity sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@noble/hashes@^1.2.0": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" + integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== + +"@sinclair/typebox@^0.27.8": + version "0.27.8" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" + integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== + +"@sinonjs/commons@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.0.tgz#beb434fe875d965265e04722ccfc21df7f755d72" + integrity sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@^10.0.2": + version "10.3.0" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz#55fdff1ecab9f354019129daf4df0dd4d923ea66" + integrity sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA== + dependencies: + "@sinonjs/commons" "^3.0.0" + +"@types/babel__core@^7.1.14": + version "7.20.3" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.3.tgz#d5625a50b6f18244425a1359a858c73d70340778" + integrity sha512-54fjTSeSHwfan8AyHWrKbfBWiEUrNTZsUwPTDSNaaP1QDQIZbeNUg3a59E9D+375MzUw/x1vx2/0F5LBz+AeYA== + dependencies: + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.6.6" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.6.tgz#676f89f67dc8ddaae923f70ebc5f1fa800c031a8" + integrity sha512-66BXMKb/sUWbMdBNdMvajU7i/44RkrA3z/Yt1c7R5xejt8qh84iU54yUWCtm0QwGJlDcf/gg4zd/x4mpLAlb/w== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.4.3" + resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.3.tgz#db9ac539a2fe05cfe9e168b24f360701bde41f5f" + integrity sha512-ciwyCLeuRfxboZ4isgdNZi/tkt06m8Tw6uGbBSBgWrnnZGNXiEyM27xc/PjXGQLqlZ6ylbgHMnm7ccF9tCkOeQ== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": + version "7.20.3" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.20.3.tgz#a971aa47441b28ef17884ff945d0551265a2d058" + integrity sha512-Lsh766rGEFbaxMIDH7Qa+Yha8cMVI3qAK6CHt3OR0YfxOIn5Z54iHiyDRycHrBqeIiqGa20Kpsv1cavfBKkRSw== + dependencies: + "@babel/types" "^7.20.7" + +"@types/graceful-fs@^4.1.3": + version "4.1.8" + resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.8.tgz#417e461e4dc79d957dc3107f45fe4973b09c2915" + integrity sha512-NhRH7YzWq8WiNKVavKPBmtLYZHxNY19Hh+az28O/phfp68CF45pMFud+ZzJ8ewnxnC5smIdF3dqFeiSUQ5I+pw== + dependencies: + "@types/node" "*" + +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#fdfdd69fa16d530047d9963635bd77c71a08c068" + integrity sha512-zONci81DZYCZjiLe0r6equvZut0b+dBRPBN5kBDjsONnutYNtJMoWQ9uR2RkL1gLG9NMTzvf+29e5RFfPbeKhQ== + +"@types/istanbul-lib-report@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.2.tgz#394798d5f727402eb5ec99eb9618ffcd2b7645a1" + integrity sha512-8toY6FgdltSdONav1XtUHl4LN1yTmLza+EuDazb/fEmRNCwjyqNVIQWs2IfC74IqjHkREs/nQ2FWq5kZU9IC0w== + dependencies: + "@types/istanbul-lib-coverage" "*" + +"@types/istanbul-reports@^3.0.0": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.3.tgz#0313e2608e6d6955d195f55361ddeebd4b74c6e7" + integrity sha512-1nESsePMBlf0RPRffLZi5ujYh7IH1BWL4y9pr+Bn3cJBdxz+RTP8bUFljLz9HvzhhOSWKdyBZ4DIivdL6rvgZg== + dependencies: + "@types/istanbul-lib-report" "*" + +"@types/jest@^29.5.5": + version "29.5.6" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.6.tgz#f4cf7ef1b5b0bfc1aa744e41b24d9cc52533130b" + integrity sha512-/t9NnzkOpXb4Nfvg17ieHE6EeSjDS2SGSpNYfoLbUAeL/EOueU/RSdOWFpfQTXBEM7BguYW1XQ0EbM+6RlIh6w== + dependencies: + expect "^29.0.0" + pretty-format "^29.0.0" + +"@types/libsodium-wrappers-sumo@^0.7.7": + version "0.7.7" + resolved "https://registry.yarnpkg.com/@types/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.7.7.tgz#70e818fefc19f0a9bd4d53e8eddcc9c1d22f5f2a" + integrity sha512-L5KaYOEJqPlMZjP2kUaKjr0vQyv8LRR/QkwAKUazl3JrcEt/VXDdCAi2+Z5mSHOUjan7PEPRSxEPvwsIyXDLDA== + dependencies: + "@types/libsodium-wrappers" "*" + +"@types/libsodium-wrappers@*": + version "0.7.12" + resolved "https://registry.yarnpkg.com/@types/libsodium-wrappers/-/libsodium-wrappers-0.7.12.tgz#0dfa2d66ab8bdc1312c0e2f4e7e1cd5633c6df57" + integrity sha512-NNUV6W5KFMYSazUh7bofvIqjHunu1ia24Q4gygXrhluXvvdPtkV6fXuciidYU7eBc9XTltAc6k727wYlrpo9Jg== + +"@types/node@*", "@types/node@^20.7.1": + version "20.8.8" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.8.tgz#adee050b422061ad5255fc38ff71b2bb96ea2a0e" + integrity sha512-YRsdVxq6OaLfmR9Hy816IMp33xOBjfyOgUd77ehqg96CFywxAPbDbXvAsuN2KVg2HOT8Eh6uAfU+l4WffwPVrQ== + dependencies: + undici-types "~5.25.1" + +"@types/stack-utils@^2.0.0": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.2.tgz#01284dde9ef4e6d8cef6422798d9a3ad18a66f8b" + integrity sha512-g7CK9nHdwjK2n0ymT2CW698FuWJRIx+RP6embAzZ2Qi8/ilIrA1Imt2LVSeHUzKvpoi7BhmmQcXz95eS0f2JXw== + +"@types/yargs-parser@*": + version "21.0.2" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.2.tgz#7bd04c5da378496ef1695a1008bf8f71847a8b8b" + integrity sha512-5qcvofLPbfjmBfKaLfj/+f+Sbd6pN4zl7w7VSVI5uz7m9QZTuB2aZAa2uo1wHFBNN2x6g/SoTkXmd8mQnQF2Cw== + +"@types/yargs@^17.0.8": + version "17.0.29" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.29.tgz#06aabc72497b798c643c812a8b561537fea760cf" + integrity sha512-nacjqA3ee9zRF/++a3FUY1suHTFKZeHba2n8WeDw9cCVdmzmHpIxyzOJBcpHvvEmS8E9KqWlSnWHUkOrkhWcvA== + dependencies: + "@types/yargs-parser" "*" + +ajv@^8.12.0: + version "8.12.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" + integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + +ansi-escapes@^4.2.1: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + +anymatch@^3.0.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +babel-jest@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.7.0.tgz#f4369919225b684c56085998ac63dbd05be020d5" + integrity sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg== + dependencies: + "@jest/transform" "^29.7.0" + "@types/babel__core" "^7.1.14" + babel-plugin-istanbul "^6.1.1" + babel-preset-jest "^29.6.3" + chalk "^4.0.0" + graceful-fs "^4.2.9" + slash "^3.0.0" + +babel-plugin-istanbul@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" + integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@istanbuljs/load-nyc-config" "^1.0.0" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-instrument "^5.0.4" + test-exclude "^6.0.0" + +babel-plugin-jest-hoist@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz#aadbe943464182a8922c3c927c3067ff40d24626" + integrity sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg== + dependencies: + "@babel/template" "^7.3.3" + "@babel/types" "^7.3.3" + "@types/babel__core" "^7.1.14" + "@types/babel__traverse" "^7.0.6" + +babel-preset-current-node-syntax@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b" + integrity sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ== + dependencies: + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-bigint" "^7.8.3" + "@babel/plugin-syntax-class-properties" "^7.8.3" + "@babel/plugin-syntax-import-meta" "^7.8.3" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.8.3" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.8.3" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-top-level-await" "^7.8.3" + +babel-preset-jest@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz#fa05fa510e7d493896d7b0dd2033601c840f171c" + integrity sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA== + dependencies: + babel-plugin-jest-hoist "^29.6.3" + babel-preset-current-node-syntax "^1.0.0" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +bip32-ed25519@^0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/bip32-ed25519/-/bip32-ed25519-0.0.4.tgz#218943e212c2d3152dfd6f3a929305e3fe86534c" + integrity sha512-KfazzGVLwl70WZ1r98dO+8yaJRTGgWHL9ITn4bXHQi2mB4cT3Hjh53tXWUpEWE1zKCln7PbyX8Z337VapAOb5w== + dependencies: + bn.js "^5.1.1" + elliptic "^6.4.1" + hash.js "^1.1.7" + +bip39@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bip39/-/bip39-3.1.0.tgz#c55a418deaf48826a6ceb34ac55b3ee1577e18a3" + integrity sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A== + dependencies: + "@noble/hashes" "^1.2.0" + +bn.js@^4.11.9: + version "4.12.0" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" + integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== + +bn.js@^5.1.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" + integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +brorand@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w== + +browserslist@^4.21.9: + version "4.22.1" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.22.1.tgz#ba91958d1a59b87dab6fed8dfbcb3da5e2e9c619" + integrity sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ== + dependencies: + caniuse-lite "^1.0.30001541" + electron-to-chromium "^1.4.535" + node-releases "^2.0.13" + update-browserslist-db "^1.0.13" + +bs-logger@0.x: + version "0.2.6" + resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" + integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== + dependencies: + fast-json-stable-stringify "2.x" + +bser@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" + integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== + dependencies: + node-int64 "^0.4.0" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +camelcase@^6.2.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +caniuse-lite@^1.0.30001541: + version "1.0.30001554" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001554.tgz#ba80d88dff9acbc0cd4b7535fc30e0191c5e2e2a" + integrity sha512-A2E3U//MBwbJVzebddm1YfNp7Nud5Ip+IPn4BozBmn4KqVX7AvluoIDFWjsv5OkGnKUXQVmMSoMKLa3ScCblcQ== + +chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +char-regex@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" + integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== + +ci-info@^3.2.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" + integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== + +cjs-module-lexer@^1.0.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz#6c370ab19f8a3394e318fe682686ec0ac684d107" + integrity sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ== + +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== + +collect-v8-coverage@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz#c0b29bcd33bcd0779a1344c2136051e6afd3d9e9" + integrity sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q== + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + +create-jest@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/create-jest/-/create-jest-29.7.0.tgz#a355c5b3cb1e1af02ba177fe7afd7feee49a5320" + integrity sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q== + dependencies: + "@jest/types" "^29.6.3" + chalk "^4.0.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + jest-config "^29.7.0" + jest-util "^29.7.0" + prompts "^2.0.1" + +cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +debug@^4.1.0, debug@^4.1.1: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +dedent@^1.0.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.5.1.tgz#4f3fc94c8b711e9bb2800d185cd6ad20f2a90aff" + integrity sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg== + +deepmerge@^4.2.2: + version "4.3.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== + +detect-newline@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" + integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== + +diff-sequences@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" + integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== + +electron-to-chromium@^1.4.535: + version "1.4.566" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.566.tgz#5c5ba1d2dc895f4887043f0cc7e61798c7e5919a" + integrity sha512-mv+fAy27uOmTVlUULy15U3DVJ+jg+8iyKH1bpwboCRhtDC69GKf1PPTZvEIhCyDr81RFqfxZJYrbgp933a1vtg== + +elliptic@^6.4.1: + version "6.5.4" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" + integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== + dependencies: + bn.js "^4.11.9" + brorand "^1.1.0" + hash.js "^1.0.0" + hmac-drbg "^1.0.1" + inherits "^2.0.4" + minimalistic-assert "^1.0.1" + minimalistic-crypto-utils "^1.0.1" + +emittery@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad" + integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + +exit@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== + +expect@^29.0.0, expect@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/expect/-/expect-29.7.0.tgz#578874590dcb3214514084c08115d8aee61e11bc" + integrity sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw== + dependencies: + "@jest/expect-utils" "^29.7.0" + jest-get-type "^29.6.3" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fb-watchman@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c" + integrity sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA== + dependencies: + bser "2.1.1" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +find-up@^4.0.0, find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@^2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-package-type@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" + integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== + +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +glob@^7.1.3, glob@^7.1.4: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +graceful-fs@^4.2.9: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +hash.js@^1.0.0, hash.js@^1.0.3, hash.js@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" + integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.1" + +hasown@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c" + integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== + dependencies: + function-bind "^1.1.2" + +hi-base32@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/hi-base32/-/hi-base32-0.5.1.tgz#1279f2ddae2673219ea5870c2121d2a33132857e" + integrity sha512-EmBBpvdYh/4XxsnUybsPag6VikPYnN30td+vQk+GI3qpahVEG9+gTkG0aXVxTjBqQ5T6ijbWIu77O+C5WFWsnA== + +hmac-drbg@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + integrity sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg== + dependencies: + hash.js "^1.0.3" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.1" + +html-escaper@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" + integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== + +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + +import-local@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" + integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.3, inherits@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + +is-core-module@^2.13.0: + version "2.13.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" + integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== + dependencies: + hasown "^2.0.0" + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-generator-fn@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" + integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" + integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== + +istanbul-lib-instrument@^5.0.4: + version "5.2.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz#d10c8885c2125574e1c231cacadf955675e1ce3d" + integrity sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg== + dependencies: + "@babel/core" "^7.12.3" + "@babel/parser" "^7.14.7" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.2.0" + semver "^6.3.0" + +istanbul-lib-instrument@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.1.tgz#71e87707e8041428732518c6fb5211761753fbdf" + integrity sha512-EAMEJBsYuyyztxMxW3g7ugGPkrZsV57v0Hmv3mm1uQsmB+QnZuepg731CRaIgeUVSdmsTngOkSnauNF8p7FIhA== + dependencies: + "@babel/core" "^7.12.3" + "@babel/parser" "^7.14.7" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.2.0" + semver "^7.5.4" + +istanbul-lib-report@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz#908305bac9a5bd175ac6a74489eafd0fc2445a7d" + integrity sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw== + dependencies: + istanbul-lib-coverage "^3.0.0" + make-dir "^4.0.0" + supports-color "^7.1.0" + +istanbul-lib-source-maps@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#895f3a709fcfba34c6de5a42939022f3e4358551" + integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== + dependencies: + debug "^4.1.1" + istanbul-lib-coverage "^3.0.0" + source-map "^0.6.1" + +istanbul-reports@^3.1.3: + version "3.1.6" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.6.tgz#2544bcab4768154281a2f0870471902704ccaa1a" + integrity sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg== + dependencies: + html-escaper "^2.0.0" + istanbul-lib-report "^3.0.0" + +jest-changed-files@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.7.0.tgz#1c06d07e77c78e1585d020424dedc10d6e17ac3a" + integrity sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w== + dependencies: + execa "^5.0.0" + jest-util "^29.7.0" + p-limit "^3.1.0" + +jest-circus@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.7.0.tgz#b6817a45fcc835d8b16d5962d0c026473ee3668a" + integrity sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/expect" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + co "^4.6.0" + dedent "^1.0.0" + is-generator-fn "^2.0.0" + jest-each "^29.7.0" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-runtime "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" + p-limit "^3.1.0" + pretty-format "^29.7.0" + pure-rand "^6.0.0" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-cli@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.7.0.tgz#5592c940798e0cae677eec169264f2d839a37995" + integrity sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg== + dependencies: + "@jest/core" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" + chalk "^4.0.0" + create-jest "^29.7.0" + exit "^0.1.2" + import-local "^3.0.2" + jest-config "^29.7.0" + jest-util "^29.7.0" + jest-validate "^29.7.0" + yargs "^17.3.1" + +jest-config@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.7.0.tgz#bcbda8806dbcc01b1e316a46bb74085a84b0245f" + integrity sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ== + dependencies: + "@babel/core" "^7.11.6" + "@jest/test-sequencer" "^29.7.0" + "@jest/types" "^29.6.3" + babel-jest "^29.7.0" + chalk "^4.0.0" + ci-info "^3.2.0" + deepmerge "^4.2.2" + glob "^7.1.3" + graceful-fs "^4.2.9" + jest-circus "^29.7.0" + jest-environment-node "^29.7.0" + jest-get-type "^29.6.3" + jest-regex-util "^29.6.3" + jest-resolve "^29.7.0" + jest-runner "^29.7.0" + jest-util "^29.7.0" + jest-validate "^29.7.0" + micromatch "^4.0.4" + parse-json "^5.2.0" + pretty-format "^29.7.0" + slash "^3.0.0" + strip-json-comments "^3.1.1" + +jest-diff@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.7.0.tgz#017934a66ebb7ecf6f205e84699be10afd70458a" + integrity sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw== + dependencies: + chalk "^4.0.0" + diff-sequences "^29.6.3" + jest-get-type "^29.6.3" + pretty-format "^29.7.0" + +jest-docblock@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.7.0.tgz#8fddb6adc3cdc955c93e2a87f61cfd350d5d119a" + integrity sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g== + dependencies: + detect-newline "^3.0.0" + +jest-each@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.7.0.tgz#162a9b3f2328bdd991beaabffbb74745e56577d1" + integrity sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ== + dependencies: + "@jest/types" "^29.6.3" + chalk "^4.0.0" + jest-get-type "^29.6.3" + jest-util "^29.7.0" + pretty-format "^29.7.0" + +jest-environment-node@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.7.0.tgz#0b93e111dda8ec120bc8300e6d1fb9576e164376" + integrity sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/fake-timers" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + jest-mock "^29.7.0" + jest-util "^29.7.0" + +jest-get-type@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.6.3.tgz#36f499fdcea197c1045a127319c0481723908fd1" + integrity sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw== + +jest-haste-map@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.7.0.tgz#3c2396524482f5a0506376e6c858c3bbcc17b104" + integrity sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA== + dependencies: + "@jest/types" "^29.6.3" + "@types/graceful-fs" "^4.1.3" + "@types/node" "*" + anymatch "^3.0.3" + fb-watchman "^2.0.0" + graceful-fs "^4.2.9" + jest-regex-util "^29.6.3" + jest-util "^29.7.0" + jest-worker "^29.7.0" + micromatch "^4.0.4" + walker "^1.0.8" + optionalDependencies: + fsevents "^2.3.2" + +jest-leak-detector@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz#5b7ec0dadfdfec0ca383dc9aa016d36b5ea4c728" + integrity sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw== + dependencies: + jest-get-type "^29.6.3" + pretty-format "^29.7.0" + +jest-matcher-utils@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz#ae8fec79ff249fd592ce80e3ee474e83a6c44f12" + integrity sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g== + dependencies: + chalk "^4.0.0" + jest-diff "^29.7.0" + jest-get-type "^29.6.3" + pretty-format "^29.7.0" + +jest-message-util@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.7.0.tgz#8bc392e204e95dfe7564abbe72a404e28e51f7f3" + integrity sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w== + dependencies: + "@babel/code-frame" "^7.12.13" + "@jest/types" "^29.6.3" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^29.7.0" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-mock@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.7.0.tgz#4e836cf60e99c6fcfabe9f99d017f3fdd50a6347" + integrity sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw== + dependencies: + "@jest/types" "^29.6.3" + "@types/node" "*" + jest-util "^29.7.0" + +jest-pnp-resolver@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz#930b1546164d4ad5937d5540e711d4d38d4cad2e" + integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w== + +jest-regex-util@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.6.3.tgz#4a556d9c776af68e1c5f48194f4d0327d24e8a52" + integrity sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg== + +jest-resolve-dependencies@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz#1b04f2c095f37fc776ff40803dc92921b1e88428" + integrity sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA== + dependencies: + jest-regex-util "^29.6.3" + jest-snapshot "^29.7.0" + +jest-resolve@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.7.0.tgz#64d6a8992dd26f635ab0c01e5eef4399c6bcbc30" + integrity sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA== + dependencies: + chalk "^4.0.0" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + jest-pnp-resolver "^1.2.2" + jest-util "^29.7.0" + jest-validate "^29.7.0" + resolve "^1.20.0" + resolve.exports "^2.0.0" + slash "^3.0.0" + +jest-runner@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.7.0.tgz#809af072d408a53dcfd2e849a4c976d3132f718e" + integrity sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ== + dependencies: + "@jest/console" "^29.7.0" + "@jest/environment" "^29.7.0" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + emittery "^0.13.1" + graceful-fs "^4.2.9" + jest-docblock "^29.7.0" + jest-environment-node "^29.7.0" + jest-haste-map "^29.7.0" + jest-leak-detector "^29.7.0" + jest-message-util "^29.7.0" + jest-resolve "^29.7.0" + jest-runtime "^29.7.0" + jest-util "^29.7.0" + jest-watcher "^29.7.0" + jest-worker "^29.7.0" + p-limit "^3.1.0" + source-map-support "0.5.13" + +jest-runtime@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.7.0.tgz#efecb3141cf7d3767a3a0cc8f7c9990587d3d817" + integrity sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ== + dependencies: + "@jest/environment" "^29.7.0" + "@jest/fake-timers" "^29.7.0" + "@jest/globals" "^29.7.0" + "@jest/source-map" "^29.6.3" + "@jest/test-result" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + cjs-module-lexer "^1.0.0" + collect-v8-coverage "^1.0.0" + glob "^7.1.3" + graceful-fs "^4.2.9" + jest-haste-map "^29.7.0" + jest-message-util "^29.7.0" + jest-mock "^29.7.0" + jest-regex-util "^29.6.3" + jest-resolve "^29.7.0" + jest-snapshot "^29.7.0" + jest-util "^29.7.0" + slash "^3.0.0" + strip-bom "^4.0.0" + +jest-snapshot@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.7.0.tgz#c2c574c3f51865da1bb329036778a69bf88a6be5" + integrity sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw== + dependencies: + "@babel/core" "^7.11.6" + "@babel/generator" "^7.7.2" + "@babel/plugin-syntax-jsx" "^7.7.2" + "@babel/plugin-syntax-typescript" "^7.7.2" + "@babel/types" "^7.3.3" + "@jest/expect-utils" "^29.7.0" + "@jest/transform" "^29.7.0" + "@jest/types" "^29.6.3" + babel-preset-current-node-syntax "^1.0.0" + chalk "^4.0.0" + expect "^29.7.0" + graceful-fs "^4.2.9" + jest-diff "^29.7.0" + jest-get-type "^29.6.3" + jest-matcher-utils "^29.7.0" + jest-message-util "^29.7.0" + jest-util "^29.7.0" + natural-compare "^1.4.0" + pretty-format "^29.7.0" + semver "^7.5.3" + +jest-util@^29.0.0, jest-util@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.7.0.tgz#23c2b62bfb22be82b44de98055802ff3710fc0bc" + integrity sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA== + dependencies: + "@jest/types" "^29.6.3" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + +jest-validate@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.7.0.tgz#7bf705511c64da591d46b15fce41400d52147d9c" + integrity sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw== + dependencies: + "@jest/types" "^29.6.3" + camelcase "^6.2.0" + chalk "^4.0.0" + jest-get-type "^29.6.3" + leven "^3.1.0" + pretty-format "^29.7.0" + +jest-watcher@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.7.0.tgz#7810d30d619c3a62093223ce6bb359ca1b28a2f2" + integrity sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g== + dependencies: + "@jest/test-result" "^29.7.0" + "@jest/types" "^29.6.3" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + emittery "^0.13.1" + jest-util "^29.7.0" + string-length "^4.0.1" + +jest-worker@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.7.0.tgz#acad073acbbaeb7262bd5389e1bcf43e10058d4a" + integrity sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw== + dependencies: + "@types/node" "*" + jest-util "^29.7.0" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +jest@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/jest/-/jest-29.7.0.tgz#994676fc24177f088f1c5e3737f5697204ff2613" + integrity sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw== + dependencies: + "@jest/core" "^29.7.0" + "@jest/types" "^29.6.3" + import-local "^3.0.2" + jest-cli "^29.7.0" + +js-sha512@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/js-sha512/-/js-sha512-0.8.0.tgz#dd22db8d02756faccf19f218e3ed61ec8249f7d4" + integrity sha512-PWsmefG6Jkodqt+ePTvBZCSMFgN7Clckjd0O7su3I0+BW2QWUTJNzjktHsztGLhncP2h8mcF9V9Y2Ha59pAViQ== + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^3.13.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +kleur@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== + +leven@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" + integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== + +libsodium-sumo@^0.7.13: + version "0.7.13" + resolved "https://registry.yarnpkg.com/libsodium-sumo/-/libsodium-sumo-0.7.13.tgz#533b97d2be44b1277e59c1f9f60805978ac5542d" + integrity sha512-zTGdLu4b9zSNLfovImpBCbdAA4xkpkZbMnSQjP8HShyOutnGjRHmSOKlsylh1okao6QhLiz7nG98EGn+04cZjQ== + +libsodium-wrappers-sumo@^0.7.13: + version "0.7.13" + resolved "https://registry.yarnpkg.com/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.7.13.tgz#a33aea845a0bb56db067548f04feba28c730ab8e" + integrity sha512-lz4YdplzDRh6AhnLGF2Dj2IUj94xRN6Bh8T0HLNwzYGwPehQJX6c7iYVrFUPZ3QqxE0bqC+K0IIqqZJYWumwSQ== + dependencies: + libsodium-sumo "^0.7.13" + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +lodash.memoize@4.x: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +make-dir@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e" + integrity sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw== + dependencies: + semver "^7.5.3" + +make-error@1.x: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +makeerror@1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" + integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg== + dependencies: + tmpl "1.0.5" + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +micromatch@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimalistic-crypto-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== + +minimatch@^3.0.4, minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== + +node-releases@^2.0.13: + version "2.0.13" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d" + integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ== + +normalize-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +parse-json@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.0.4, picomatch@^2.2.3, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pirates@^4.0.4: + version "4.0.6" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" + integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== + +pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +pretty-format@^29.0.0, pretty-format@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" + integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ== + dependencies: + "@jest/schemas" "^29.6.3" + ansi-styles "^5.0.0" + react-is "^18.0.0" + +prompts@^2.0.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" + integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.5" + +punycode@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" + integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== + +pure-rand@^6.0.0: + version "6.0.4" + resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.0.4.tgz#50b737f6a925468679bff00ad20eade53f37d5c7" + integrity sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA== + +react-is@^18.0.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" + integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve.exports@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.2.tgz#f8c934b8e6a13f539e38b7098e2e36134f01e800" + integrity sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg== + +resolve@^1.20.0: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +semver@^6.3.0, semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^7.5.3, semver@^7.5.4: + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== + dependencies: + lru-cache "^6.0.0" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +signal-exit@^3.0.3, signal-exit@^3.0.7: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +sisteransi@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" + integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +source-map-support@0.5.13: + version "0.5.13" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" + integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0, source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== + +stack-utils@^2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" + integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ== + dependencies: + escape-string-regexp "^2.0.0" + +string-length@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" + integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== + dependencies: + char-regex "^1.0.2" + strip-ansi "^6.0.0" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-bom@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" + integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +test-exclude@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" + integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== + dependencies: + "@istanbuljs/schema" "^0.1.2" + glob "^7.1.4" + minimatch "^3.0.4" + +tmpl@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" + integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +ts-custom-error@^3.2.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/ts-custom-error/-/ts-custom-error-3.3.1.tgz#8bd3c8fc6b8dc8e1cb329267c45200f1e17a65d1" + integrity sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A== + +ts-jest@^29.1.1: + version "29.1.1" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.1.1.tgz#f58fe62c63caf7bfcc5cc6472082f79180f0815b" + integrity sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA== + dependencies: + bs-logger "0.x" + fast-json-stable-stringify "2.x" + jest-util "^29.0.0" + json5 "^2.2.3" + lodash.memoize "4.x" + make-error "1.x" + semver "^7.5.3" + yargs-parser "^21.0.1" + +ts-log@^2.2.4: + version "2.2.5" + resolved "https://registry.yarnpkg.com/ts-log/-/ts-log-2.2.5.tgz#aef3252f1143d11047e2cb6f7cfaac7408d96623" + integrity sha512-PGcnJoTBnVGy6yYNFxWVNkdcAuAMstvutN9MgDJIV6L0oG8fB+ZNNy1T+wJzah8RPGor1mZuPQkVfXNDpy9eHA== + +type-detect@4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + +typescript@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78" + integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w== + +undici-types@~5.25.1: + version "5.25.3" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.25.3.tgz#e044115914c85f0bcbb229f346ab739f064998c3" + integrity sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA== + +update-browserslist-db@^1.0.13: + version "1.0.13" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4" + integrity sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +v8-to-istanbul@^9.0.1: + version "9.1.3" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.1.3.tgz#ea456604101cd18005ac2cae3cdd1aa058a6306b" + integrity sha512-9lDD+EVI2fjFsMWXc6dy5JJzBsVTcQ2fVkfBvncZ6xJWG9wtBhOldG+mHkSL0+V1K/xgZz0JDO5UT5hFwHUghg== + dependencies: + "@jridgewell/trace-mapping" "^0.3.12" + "@types/istanbul-lib-coverage" "^2.0.1" + convert-source-map "^2.0.0" + +walker@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" + integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== + dependencies: + makeerror "1.0.12" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +write-file-atomic@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-4.0.2.tgz#a9df01ae5b77858a027fd2e80768ee433555fcfd" + integrity sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg== + dependencies: + imurmurhash "^0.1.4" + signal-exit "^3.0.7" + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yargs-parser@^21.0.1, yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + +yargs@^17.3.1: + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== From 326445b0f92dc6e17280583e5cc81ad21963d22c Mon Sep 17 00:00:00 2001 From: ehanoc Date: Sun, 29 Oct 2023 17:02:18 +0000 Subject: [PATCH 12/47] BIP32-Ed25519 by the book impl and test vectors. Reference Paper included Signed-off-by: ehanoc --- ...nistic-Keys-over-a-Non-linear-Keyspace.pdf | Bin 0 -> 227025 bytes assets/arc-0052/bip32-ed25519.ts | 181 ++++++++++++++++++ assets/arc-0052/contextual.api.crypto.spec.ts | 92 ++------- assets/arc-0052/contextual.api.crypto.ts | 91 +++------ assets/arc-0052/package.json | 2 +- assets/arc-0052/yarn.lock | 91 ++------- 6 files changed, 236 insertions(+), 221 deletions(-) create mode 100644 assets/arc-0052/BIP32-ED25519-Hierarchical-Deterministic-Keys-over-a-Non-linear-Keyspace.pdf create mode 100644 assets/arc-0052/bip32-ed25519.ts diff --git a/assets/arc-0052/BIP32-ED25519-Hierarchical-Deterministic-Keys-over-a-Non-linear-Keyspace.pdf b/assets/arc-0052/BIP32-ED25519-Hierarchical-Deterministic-Keys-over-a-Non-linear-Keyspace.pdf new file mode 100644 index 0000000000000000000000000000000000000000..2eee1008779e7c638974ae1d733389e29c9b0328 GIT binary patch literal 227025 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 0x80000000) throw new Error('can not derive public key with harden') + + const pk: Buffer = Buffer.from(extendedKey.subarray(0, 32)) + const cc: Buffer = Buffer.from(extendedKey.subarray(32, 64)) + + const data: Buffer = Buffer.allocUnsafe(1 + 32 + 4); + data.writeUInt32LE(index, 1 + 32); + + pk.copy(data, 1); + data[0] = 0x02; + + const z: Buffer = createHmac("sha512", cc).update(data).digest(); + data[0] = 0x03; + + const i: Buffer = createHmac("sha512", cc).update(data).digest(); + + // truncanted to the 32 bytes on the right hand side + // Section V. BIP32-Ed25519: Specification; subsection D) Public Child Key Derivation + const chainCode: Buffer = i.subarray(32, 64); + const zl: Buffer = z.subarray(0, 32); + + // left = 8 * trunc28(zl) + // const scalar = new BN(zl.subarray(0, 28), 16, 'le').mul(new BN(8)); + const left = new BN(zl.subarray(0, 28), 16, 'le').mul(new BN(8)).toArrayLike(Buffer, 'le', 32); + + const p: Uint8Array = crypto_scalarmult_ed25519_base_noclamp(left); + return Buffer.concat([crypto_core_ed25519_add(p, pk), chainCode]); +} + +/** + * + * @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 + */ +function derivedNonHardened( + kl: Uint8Array, + cc: Uint8Array, + index: number +): { z: Uint8Array; childChainCode: Uint8Array } { + const data: Buffer = Buffer.allocUnsafe(1 + 32 + 4); + data.writeUInt32LE(index, 1 + 32); + + var pk = Buffer.from(crypto_scalarmult_ed25519_base_noclamp(kl)); + pk.copy(data, 1); + + data[0] = 0x02; + const z: Buffer = createHmac("sha512", cc).update(data).digest(); + + data[0] = 0x03; + const childChainCode: Buffer = createHmac("sha512", cc).update(data).digest(); + + return { 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 + */ +function deriveHardened( + kl: Uint8Array, + kr: Uint8Array, + cc: Uint8Array, + index: number +): { z: Uint8Array; childChainCode: Uint8Array } { + const data: Buffer = Buffer.allocUnsafe(1 + 64 + 4); + data.writeUInt32LE(index, 1 + 64); + Buffer.from(kl).copy(data, 1); + Buffer.from(kr).copy(data, 1 + 32); + + data[0] = 0x00; + const z: Buffer = createHmac("sha512", cc).update(data).digest(); + data[0] = 0x01; + const childChainCode: Buffer = createHmac("sha512", cc).update(data).digest(); + + return { z, childChainCode }; +} diff --git a/assets/arc-0052/contextual.api.crypto.spec.ts b/assets/arc-0052/contextual.api.crypto.spec.ts index 2dd510950..4ffc44547 100644 --- a/assets/arc-0052/contextual.api.crypto.spec.ts +++ b/assets/arc-0052/contextual.api.crypto.spec.ts @@ -1,36 +1,8 @@ -import { CryptoKX, KeyPair, crypto_kx_client_session_keys, crypto_kx_server_session_keys, crypto_scalarmult, crypto_scalarmult_ed25519_base_noclamp, crypto_secretbox_NONCEBYTES, crypto_secretbox_easy, crypto_secretbox_open_easy, crypto_sign_ed25519_pk_to_curve25519, crypto_sign_ed25519_sk_to_curve25519, crypto_sign_keypair, crypto_sign_seed_keypair, ready, to_base64 } from "libsodium-wrappers-sumo" +import { CryptoKX, KeyPair, crypto_kx_client_session_keys, crypto_kx_server_session_keys, crypto_scalarmult, crypto_secretbox_NONCEBYTES, crypto_secretbox_easy, crypto_secretbox_open_easy, crypto_sign_ed25519_pk_to_curve25519, crypto_sign_ed25519_sk_to_curve25519, crypto_sign_keypair, ready, to_base64 } from "libsodium-wrappers-sumo" import * as bip39 from "bip39" -import { sha512_256 } from "js-sha512" -import base32 from "hi-base32" import { randomBytes } from "crypto" import { ContextualCryptoApi, Encoding, KeyContext, SignMetadata } from "./contextual.api.crypto" -/** - * @param publicKey - * @returns - */ -function encodeAddress(publicKey: Buffer): string { - const keyHash: string = sha512_256.create().update(publicKey).hex() - - // last 4 bytes of the hash - const checksum: string = keyHash.slice(-8) - - return base32.encode(ConcatArrays(publicKey, Buffer.from(checksum, "hex"))).slice(0, 58) -} - -function ConcatArrays(...arrs: ArrayLike[]) { - const size = arrs.reduce((sum, arr) => sum + arr.length, 0) - const c = new Uint8Array(size) - - let offset = 0 - for (let i = 0; i < arrs.length; i++) { - c.set(arrs[i], offset) - offset += arrs[i].length - } - - return c -} - describe("Contextual Derivation & Signing", () => { let cryptoService: ContextualCryptoApi @@ -52,72 +24,34 @@ describe("Contextual Derivation & Signing", () => { describe("Soft Derivations", () => { it("\(OK) Derive m'/44'/283'/0'/0/0 Algorand Address Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 0) - expect(key).toEqual(new Uint8Array(Buffer.from("9183c50768a724c5b77a4ddf56d7da2f204d56da68ced53626222ace25b25cd8", "hex"))) + expect(key).toEqual(new Uint8Array(Buffer.from("7915e7ecbaad1dc9bc22a9e496686687f1a8cb4895b7ca46f86d64dd56c6cd97", "hex"))) }) it("\(OK) Derive m'/44'/283'/0'/0/1 Algorand Address Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 1) - expect(key).toEqual(new Uint8Array(Buffer.from("15965b321e69bfb6386a1dc31925ee608f8fdc9f272442e4a57e47508390a714", "hex"))) + expect(key).toEqual(new Uint8Array(Buffer.from("054a6881d8809c348a402d67ba2feedcd8e3145f40f21a6bbd0de09c30c78d0a", "hex"))) }) it("\(OK) Derive m'/44'/283'/0'/0/2 Algorand Address Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 2) - expect(key).toEqual(new Uint8Array(Buffer.from("af1bb9cba94b7a7569c186ecffe881b01bd2cbded23418d2fb51c7ffe675502c", "hex"))) + expect(key).toEqual(new Uint8Array(Buffer.from("8cea8052cfa1fd8cec0b4fad6241a91f2edbfe9f072586f243839174e40a25ef", "hex"))) }) it("\(OK) Derive m'/44'/283'/3'/0/0 Algorand Address Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 3, 0) - expect(key).toEqual(new Uint8Array(Buffer.from("d81624d97ef08248724eb2432a514efb857d2705783dcb012ede40fcaaeadcd0", "hex"))) + expect(key).toEqual(new Uint8Array(Buffer.from("cf8d28a3d41bc656acbfeadb64d06054142c97bee6a987c11d934f84853df866", "hex"))) }) }) describe("Hard Derivations", () => { it("\(OK) Derive m'/44'/283'/1'/0/0 Algorand Address Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 1, 0) - expect(key).toEqual(new Uint8Array(Buffer.from("ccca8fd695161983b306bb0756f674d0b437d77b4bdf2b546ab2ecf2890270dd", "hex"))) + expect(key).toEqual(new Uint8Array(Buffer.from("04f3ba279aa781ab4f8f79aaf6cf91e3d7ff75429064dc757001a30e10c628df", "hex"))) }) it("\(OK) Derive m'/44'/283'/2'/0/1 Algorand Address Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 2, 1) - expect(key).toEqual(new Uint8Array(Buffer.from("d9675a7ce320937ce756617f60a73a72d30de183277bc8ecf8f923787f4f9875", "hex"))) - }) - - describe("Ledger Addresses test Vectors", ()=> { - it("\(OK) Derive m'/44'/283'/0'/0/0 Algorand Address", async () => { - const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 0) - const algorandAddress: string = encodeAddress(Buffer.from(key)) - expect(algorandAddress).toEqual("SGB4KB3IU4SMLN32JXPVNV62F4QE2VW2NDHNKNRGEIVM4JNSLTMJV53P6I") - }) - - it("\(OK) Derive m'/44'/283'/1'/0/0 Algorand Address", async () => { - const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 1, 0) - const algorandAddress: string = encodeAddress(Buffer.from(key)) - expect(algorandAddress).toEqual("ZTFI7VUVCYMYHMYGXMDVN5TU2C2DPV33JPPSWVDKWLWPFCICODO6HWLPLI") - }) - - it("\(OK) Derive m'/44'/283'/2'/0/0 Algorand Address", async () => { - const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 2, 0) - const algorandAddress: string = encodeAddress(Buffer.from(key)) - expect(algorandAddress).toEqual("PSVABV5X6EGQZIHKZWVUUISWIZZKZ6RFEQ6O3IOOHPAJBSVHNNQENRMGQY") - }) - - it("\(OK) Derive m'/44'/283'/3'/0/0 Algorand Address", async () => { - const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 3, 0) - const algorandAddress: string = encodeAddress(Buffer.from(key)) - expect(algorandAddress).toEqual("3ALCJWL66CBEQ4SOWJBSUUKO7OCX2JYFPA64WAJO3ZAPZKXK3TIPC4J3VQ") - }) - - it("\(OK) Derive m'/44'/283'/4'/0/0 Algorand Address", async () => { - const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 4, 0) - const algorandAddress: string = encodeAddress(Buffer.from(key)) - expect(algorandAddress).toEqual("42ASNPEG4P73PI4DYJDILSX6CKOQ275N3IX6GDLVUMDRIGMBURCFNVXNEI") - }) - - it("\(OK) Derive m'/44'/283'/5'/0/0 Algorand Address", async () => { - const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 5, 0) - const algorandAddress: string = encodeAddress(Buffer.from(key)) - expect(algorandAddress).toEqual("XGLSRHKE2RD4VNOKWIHBR4U2SEGX2H2AN75ZLJJ7VLLXWIZPJPCB4MZUFY") - }) + expect(key).toEqual(new Uint8Array(Buffer.from("400d78302258dc7b3cb56d1a09f85b018e8100865ced0b5cda474c26bbc07c30", "hex"))) }) }) }) @@ -126,29 +60,29 @@ describe("Contextual Derivation & Signing", () => { describe("Soft Derivations", () => { it("\(OK) Derive m'/44'/0'/0'/0/0 Identity Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Identity, 0, 0) - expect(key).toEqual(new Uint8Array(Buffer.from("19dc07a51e5be0ce009d751c6d5fbd3a14d3dc4d0fc9c3c6f13e3581a08e4322", "hex"))) + expect(key).toEqual(new Uint8Array(Buffer.from("28804f08d8c145e172c998fe75058237b8181846ca763894ae3eefea6ab88352", "hex"))) }) it("\(OK) Derive m'/44'/0'/0'/0/1 Identity Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Identity, 0, 1) - expect(key).toEqual(new Uint8Array(Buffer.from("270779072e644fd8575925a49084ecbdec3907ec876ebc9081d11278dea24165", "hex"))) + expect(key).toEqual(new Uint8Array(Buffer.from("fc8d1c79edd406fa415cb0a76435eb83b6f8af72cd9bd673753471470205057a", "hex"))) }) it("\(OK) Derive m'/44'/0'/0'/0/2 Identity Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Identity, 0, 2) - expect(key).toEqual(new Uint8Array(Buffer.from("618d1493f75752fe398f3d7a361d3003d5652ae3446804256a63bbea8e11a179", "hex"))) + expect(key).toEqual(new Uint8Array(Buffer.from("f7e317899420454886fe79f24e25af0bbb1856c440b14829674015e5fc2ad28a", "hex"))) }) }) describe("Hard Derivations", () => { it("\(OK) Derive m'/44'/0'/1'/0/0 Identity Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Identity, 1, 0) - expect(key).toEqual(new Uint8Array(Buffer.from("9afd78d1456265bbdea1b82e2bc1d31ba64589f53325d69f222375fcd09f02c6", "hex"))) + expect(key).toEqual(new Uint8Array(Buffer.from("dddea98ec1fd24f9045ef0cade9055116079c87d16e51a0a081e94a8fb905435", "hex"))) }) it("\(OK) Derive m'/44'/0'/2'/0/1 Identity Key", async () => { - const key: Uint8Array = await cryptoService.keyGen(KeyContext.Identity, 2, 1) - expect(key).toEqual(new Uint8Array(Buffer.from("c3364323ba03483057b08a52087c4d6bae2e26c6dbbb3adce6aa8db627eba2b1", "hex"))) + const key: Uint8Array = await cryptoService.keyGen(KeyContext.Identity, 2, 1) + expect(key).toEqual(new Uint8Array(Buffer.from("2623ec5aad0ed598fcba654d9abd24c0c3f4804d5ac5f486e618fa1c66da139a", "hex"))) }) }) }) diff --git a/assets/arc-0052/contextual.api.crypto.ts b/assets/arc-0052/contextual.api.crypto.ts index e992e4077..3105cb6f9 100644 --- a/assets/arc-0052/contextual.api.crypto.ts +++ b/assets/arc-0052/contextual.api.crypto.ts @@ -1,4 +1,3 @@ -import { createHash, createHmac } from 'crypto'; import { crypto_core_ed25519_scalar_add, crypto_core_ed25519_scalar_mul, @@ -11,9 +10,8 @@ import { crypto_scalarmult } from 'libsodium-wrappers-sumo'; -import Ajv, { JSONSchemaType } from "ajv" -const bip32ed25519 = require("bip32-ed25519"); - +import Ajv from "ajv" +import { deriveChildNodePrivate, fromSeed } from './bip32-ed25519'; /** * @@ -63,45 +61,6 @@ export class ContextualCryptoApi { } - /** - * - * Reference of BIP32-Ed25519 Hierarchical Deterministic Keys over a Non-linear Keyspace (https://acrobat.adobe.com/id/urn:aaid:sc:EU:04fe29b0-ea1a-478b-a886-9bb558a5242a) - * - * @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 - */ - private async rootKey(seed: Buffer): Promise { - // SLIP-0010 - // We should have been using [BIP32-Ed25519 HierarchicalDeterministicKeysoveraNon-linear Keyspace] instead. Which would mean SHA512(seed) - // As in the [Section V].A Root keys. - const c: Buffer = createHmac('sha256', "ed25519 seed").update(Buffer.concat([new Uint8Array([0x1]), seed])).digest() - let I: Buffer = createHmac('sha512', "ed25519 seed").update(seed).digest() - - // split into KL and KR. - // (KL, KR) is the extended private key - let kL = I.subarray(0, 32) - let kR = I.subarray(32, 64) - - // Specific to our Algorand app implementation (Taken from Ledger reference implementation: https://github.com/LedgerHQ/orakolo/blob/0b2d5e669ec61df9a824df9fa1a363060116b490/src/python/orakolo/HDEd25519.py#L130) - // Seems to try to find a rootKey in which the last bits are cleared - // Shouldn't be necessary has keys are expected to be clamped as in the next step - while ((kL[31] & 0b00_10_00_00) != 0) { - I = createHmac('sha512', "ed25519 seed").update(I).digest() - kL = I.subarray(0, 32) - kR = I.subarray(32, 64) - } - - // clamping - // This bit is "compliant" with [BIP32-Ed25519 Hierarchical Deterministic Keys over a Non-linear Keyspace] - //Set the bits in kL as follows: - // little Endianess - kL[0] &= 0b11_11_10_00; // the lowest 3 bits of the first byte of kL are cleared - kL[31] &= 0b01_11_11_11; // the highest bit of the last byte is cleared - kL[31] |= 0b01_00_00_00; // the second highest bit of the last byte is set - - return new Uint8Array(Buffer.concat([kL, kR, c])) - } - /** * Derives a child key from the root key based on BIP44 path @@ -112,26 +71,26 @@ export class ContextualCryptoApi { * @returns */ private async deriveKey(rootKey: Uint8Array, bip44Path: number[], isPrivate: boolean = true): Promise { - let derived = bip32ed25519.derivePrivate(Buffer.from(rootKey), bip44Path[0]) - derived = bip32ed25519.derivePrivate(derived, bip44Path[1]) - derived = bip32ed25519.derivePrivate(derived, bip44Path[2]) - derived = bip32ed25519.derivePrivate(derived, bip44Path[3]) - derived = bip32ed25519.derivePrivate(derived, bip44Path[4]) - - const derivedKl = derived.subarray(0, 32) - const xpvt = createHash('sha512').update(derivedKl).digest() - - // Keys clamped again - // This is specific to our algorand app implementation - // Taken from reference code: https://github.com/Zondax/ledger-algorand/blob/test/tests_zemu/tests/key_derivation.ts#L84 - // But not part of Ledger's reference implementation:https://github.com/LedgerHQ/orakolo/blob/0b2d5e669ec61df9a824df9fa1a363060116b490/src/python/orakolo/HDEd25519.py#L156 - // And not part of [BIP32-Ed25519 Hierarchical Deterministic Keys over a Non-linear Keyspace] derivation spec - xpvt[0] &= 0b11_11_10_00 - xpvt[31] &= 0b01_11_11_11 - xpvt[31] |= 0b01_00_00_00 // This bit set is not in the BIP32-Ed25519 derivation sepc - - const scalar: Uint8Array = xpvt.subarray(0, 32) - return isPrivate ? xpvt : crypto_scalarmult_ed25519_base_noclamp(scalar) + let derived = deriveChildNodePrivate(Buffer.from(rootKey), bip44Path[0]) + derived = deriveChildNodePrivate(derived, bip44Path[1]) + derived = deriveChildNodePrivate(derived, bip44Path[2]) + derived = deriveChildNodePrivate(derived, bip44Path[3]) + + // Public Key SOFT derivations are possible without using the private key of the parent node + // Could be an implementation choice. + // Example: + // const nodeScalar: Uint8Array = derived.subarray(0, 32) + // const nodePublic: Uint8Array = crypto_scalarmult_ed25519_base_noclamp(nodeScalar) + // const nodeCC: Uint8Array = derived.subarray(64, 96) + + // // [Public][ChainCode] + // const extPub: Uint8Array = new Uint8Array(Buffer.concat([nodePublic, nodeCC])) + // const publicKey: Uint8Array = deriveChildNodePublic(extPub, bip44Path[4]).subarray(0, 32) + + derived = deriveChildNodePrivate(derived, bip44Path[4]) + + const scalar = derived.subarray(0, 32) // scalar == pvtKey + return isPrivate ? scalar : crypto_scalarmult_ed25519_base_noclamp(scalar) } /** @@ -145,7 +104,7 @@ export class ContextualCryptoApi { async keyGen(context: KeyContext, account:number, keyIndex: number): Promise { await ready // libsodium - const rootKey: Uint8Array = await this.rootKey(this.seed) + const rootKey: Uint8Array = fromSeed(this.seed) const bip44Path: number[] = GetBIP44PathFromContext(context, account, keyIndex) return await this.deriveKey(rootKey, bip44Path, false) @@ -172,7 +131,7 @@ export class ContextualCryptoApi { await ready // libsodium - const rootKey: Uint8Array = await this.rootKey(this.seed) + const rootKey: Uint8Array = fromSeed(this.seed) const bip44Path: number[] = GetBIP44PathFromContext(context, account, keyIndex) const raw: Uint8Array = await this.deriveKey(rootKey, bip44Path, true) @@ -266,7 +225,7 @@ export class ContextualCryptoApi { async ECDH(context: KeyContext, account: number, keyIndex: number, otherPartyPub: Uint8Array): Promise { await ready - const rootKey: Uint8Array = await this.rootKey(this.seed) + const rootKey: Uint8Array = fromSeed(this.seed) const bip44Path: number[] = GetBIP44PathFromContext(context, account, keyIndex) const childKey: Uint8Array = await this.deriveKey(rootKey, bip44Path, true) diff --git a/assets/arc-0052/package.json b/assets/arc-0052/package.json index f649b227e..779b1d0a1 100644 --- a/assets/arc-0052/package.json +++ b/assets/arc-0052/package.json @@ -19,8 +19,8 @@ }, "dependencies": { "ajv": "^8.12.0", - "bip32-ed25519": "^0.0.4", "bip39": "^3.1.0", + "bn.js": "^5.2.1", "hi-base32": "^0.5.1", "jest": "^29.7.0", "js-sha512": "^0.8.0", diff --git a/assets/arc-0052/yarn.lock b/assets/arc-0052/yarn.lock index 62eb66144..d3f4f117d 100644 --- a/assets/arc-0052/yarn.lock +++ b/assets/arc-0052/yarn.lock @@ -641,11 +641,11 @@ integrity sha512-NNUV6W5KFMYSazUh7bofvIqjHunu1ia24Q4gygXrhluXvvdPtkV6fXuciidYU7eBc9XTltAc6k727wYlrpo9Jg== "@types/node@*", "@types/node@^20.7.1": - version "20.8.8" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.8.tgz#adee050b422061ad5255fc38ff71b2bb96ea2a0e" - integrity sha512-YRsdVxq6OaLfmR9Hy816IMp33xOBjfyOgUd77ehqg96CFywxAPbDbXvAsuN2KVg2HOT8Eh6uAfU+l4WffwPVrQ== + version "20.8.9" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.9.tgz#646390b4fab269abce59c308fc286dcd818a2b08" + integrity sha512-UzykFsT3FhHb1h7yD4CA4YhBHq545JC0YnEz41xkipN88eKQtL6rSgocL5tbAP6Ola9Izm/Aw4Ora8He4x0BHg== dependencies: - undici-types "~5.25.1" + undici-types "~5.26.4" "@types/stack-utils@^2.0.0": version "2.0.2" @@ -785,15 +785,6 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -bip32-ed25519@^0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/bip32-ed25519/-/bip32-ed25519-0.0.4.tgz#218943e212c2d3152dfd6f3a929305e3fe86534c" - integrity sha512-KfazzGVLwl70WZ1r98dO+8yaJRTGgWHL9ITn4bXHQi2mB4cT3Hjh53tXWUpEWE1zKCln7PbyX8Z337VapAOb5w== - dependencies: - bn.js "^5.1.1" - elliptic "^6.4.1" - hash.js "^1.1.7" - bip39@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/bip39/-/bip39-3.1.0.tgz#c55a418deaf48826a6ceb34ac55b3ee1577e18a3" @@ -801,12 +792,7 @@ bip39@^3.1.0: dependencies: "@noble/hashes" "^1.2.0" -bn.js@^4.11.9: - version "4.12.0" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" - integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== - -bn.js@^5.1.1: +bn.js@^5.2.1: version "5.2.1" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== @@ -826,11 +812,6 @@ braces@^3.0.2: dependencies: fill-range "^7.0.1" -brorand@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" - integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w== - browserslist@^4.21.9: version "4.22.1" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.22.1.tgz#ba91958d1a59b87dab6fed8dfbcb3da5e2e9c619" @@ -876,9 +857,9 @@ camelcase@^6.2.0: integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== caniuse-lite@^1.0.30001541: - version "1.0.30001554" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001554.tgz#ba80d88dff9acbc0cd4b7535fc30e0191c5e2e2a" - integrity sha512-A2E3U//MBwbJVzebddm1YfNp7Nud5Ip+IPn4BozBmn4KqVX7AvluoIDFWjsv5OkGnKUXQVmMSoMKLa3ScCblcQ== + version "1.0.30001557" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001557.tgz#13f762ea1d7f7b009d4d2785fbbd250354d09ad9" + integrity sha512-91oR7hLNUP3gG6MLU+n96em322a8Xzes8wWdBKhLgUoiJsAF5irZnxSUCbc+qUZXNnPCfUwLOi9ZCZpkvjQajw== chalk@^2.4.2: version "2.4.2" @@ -1015,22 +996,9 @@ diff-sequences@^29.6.3: integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== electron-to-chromium@^1.4.535: - version "1.4.566" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.566.tgz#5c5ba1d2dc895f4887043f0cc7e61798c7e5919a" - integrity sha512-mv+fAy27uOmTVlUULy15U3DVJ+jg+8iyKH1bpwboCRhtDC69GKf1PPTZvEIhCyDr81RFqfxZJYrbgp933a1vtg== - -elliptic@^6.4.1: - version "6.5.4" - resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" - integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== - dependencies: - bn.js "^4.11.9" - brorand "^1.1.0" - hash.js "^1.0.0" - hmac-drbg "^1.0.1" - inherits "^2.0.4" - minimalistic-assert "^1.0.1" - minimalistic-crypto-utils "^1.0.1" + version "1.4.569" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.569.tgz#1298b67727187ffbaac005a7425490d157f3ad03" + integrity sha512-LsrJjZ0IbVy12ApW3gpYpcmHS3iRxH4bkKOW98y1/D+3cvDUWGcbzbsFinfUS8knpcZk/PG/2p/RnkMCYN7PVg== emittery@^0.13.1: version "0.13.1" @@ -1199,14 +1167,6 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -hash.js@^1.0.0, hash.js@^1.0.3, hash.js@^1.1.7: - version "1.1.7" - resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" - integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== - dependencies: - inherits "^2.0.3" - minimalistic-assert "^1.0.1" - hasown@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c" @@ -1219,15 +1179,6 @@ hi-base32@^0.5.1: resolved "https://registry.yarnpkg.com/hi-base32/-/hi-base32-0.5.1.tgz#1279f2ddae2673219ea5870c2121d2a33132857e" integrity sha512-EmBBpvdYh/4XxsnUybsPag6VikPYnN30td+vQk+GI3qpahVEG9+gTkG0aXVxTjBqQ5T6ijbWIu77O+C5WFWsnA== -hmac-drbg@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" - integrity sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg== - dependencies: - hash.js "^1.0.3" - minimalistic-assert "^1.0.0" - minimalistic-crypto-utils "^1.0.1" - html-escaper@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" @@ -1259,7 +1210,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.3, inherits@^2.0.4: +inherits@2: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -1840,16 +1791,6 @@ mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== -minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" - integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== - -minimalistic-crypto-utils@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" - integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== - minimatch@^3.0.4, minimatch@^3.1.1: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -2243,10 +2184,10 @@ typescript@^5.2.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78" integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w== -undici-types@~5.25.1: - version "5.25.3" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.25.3.tgz#e044115914c85f0bcbb229f346ab739f064998c3" - integrity sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA== +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== update-browserslist-db@^1.0.13: version "1.0.13" From 8feb165ea3295b6b7151196b7e657e913575db8e Mon Sep 17 00:00:00 2001 From: ehanoc Date: Sun, 29 Oct 2023 17:06:03 +0000 Subject: [PATCH 13/47] Update README with reference impl link Signed-off-by: ehanoc --- ARCs/arc-0052.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ARCs/arc-0052.md b/ARCs/arc-0052.md index e4076ad68..e7df2a6ca 100644 --- a/ARCs/arc-0052.md +++ b/ARCs/arc-0052.md @@ -197,7 +197,7 @@ The current wallet APIs available only support key derivation for addresses and ## Reference Implementation -Reference Implementation - An optional section that contains a reference/example implementation that people can use to assist in understanding or implementing this specification. +Reference Implementation is included in the `assets` folder [here](../assets/arc-0052/README.md) ## Security Considerations From 9ea1f1d07d144a2fae713e93d9342d8fbd89ed6e Mon Sep 17 00:00:00 2001 From: ehanoc Date: Wed, 1 Nov 2023 10:53:59 +0000 Subject: [PATCH 14/47] Update use cases to reject regular transaction signing Signed-off-by: ehanoc --- assets/arc-0052/contextual.api.crypto.spec.ts | 64 ++++++++++++++++++- assets/arc-0052/contextual.api.crypto.ts | 39 ++++++++++- assets/arc-0052/package.json | 1 + assets/arc-0052/yarn.lock | 5 ++ 4 files changed, 106 insertions(+), 3 deletions(-) diff --git a/assets/arc-0052/contextual.api.crypto.spec.ts b/assets/arc-0052/contextual.api.crypto.spec.ts index 4ffc44547..e12ad3c44 100644 --- a/assets/arc-0052/contextual.api.crypto.spec.ts +++ b/assets/arc-0052/contextual.api.crypto.spec.ts @@ -1,7 +1,8 @@ import { CryptoKX, KeyPair, crypto_kx_client_session_keys, crypto_kx_server_session_keys, crypto_scalarmult, crypto_secretbox_NONCEBYTES, crypto_secretbox_easy, crypto_secretbox_open_easy, crypto_sign_ed25519_pk_to_curve25519, crypto_sign_ed25519_sk_to_curve25519, crypto_sign_keypair, ready, to_base64 } from "libsodium-wrappers-sumo" import * as bip39 from "bip39" import { randomBytes } from "crypto" -import { ContextualCryptoApi, Encoding, KeyContext, SignMetadata } from "./contextual.api.crypto" +import { ContextualCryptoApi, ERROR_BAD_DATA, Encoding, KeyContext, SignMetadata } from "./contextual.api.crypto" +import * as msgpack from "algo-msgpack-with-bigint" describe("Contextual Derivation & Signing", () => { @@ -131,6 +132,67 @@ describe("Contextual Derivation & Signing", () => { const metadata: SignMetadata = { encoding: Encoding.BASE64, schema: jsonSchema } expect(cryptoService.signData(KeyContext.Identity,0, 0, encoded, metadata)).rejects.toThrowError() }) + + describe("Reject Regular Transaction Signing. IF TAG Prexies are present signing must fail", () => { + describe("Reject tags present in the encoded payload", () => { + it("\(FAIL) [TX] Tag", async () => { + const transaction: Buffer = Buffer.concat([Buffer.from("TX"), msgpack.encode(randomBytes(64))]) + const metadata: SignMetadata = { encoding: Encoding.BASE64, schema: {} } + + const encodedTx: Buffer = Buffer.from(transaction.toString('base64')) + expect(cryptoService.signData(KeyContext.Identity,0, 0, encodedTx, metadata)).rejects.toThrowError(ERROR_BAD_DATA) + }) + + it("\(FAIL) [MX] Tag", async () => { + const transaction: Buffer = Buffer.concat([Buffer.from("MX"), msgpack.encode(randomBytes(64))]) + const metadata: SignMetadata = { encoding: Encoding.BASE64, schema: {} } + + const encodedTx: Buffer = Buffer.from(transaction.toString('base64')) + expect(cryptoService.signData(KeyContext.Identity,0, 0, encodedTx, metadata)).rejects.toThrowError(ERROR_BAD_DATA) + }) + + it("\(FAIL) [Program] Tag", async () => { + const transaction: Buffer = Buffer.concat([Buffer.from("Program"), msgpack.encode(randomBytes(64))]) + const metadata: SignMetadata = { encoding: Encoding.BASE64, schema: {} } + + const encodedTx: Buffer = Buffer.from(transaction.toString('base64')) + expect(cryptoService.signData(KeyContext.Identity,0, 0, encodedTx, metadata)).rejects.toThrowError(ERROR_BAD_DATA) + }) + + it("\(FAIL) [progData] Tag", async () => { + const transaction: Buffer = Buffer.concat([Buffer.from("progData"), msgpack.encode(randomBytes(64))]) + const metadata: SignMetadata = { encoding: Encoding.BASE64, schema: {} } + + const encodedTx: Buffer = Buffer.from(transaction.toString('base64')) + expect(cryptoService.signData(KeyContext.Identity,0, 0, encodedTx, metadata)).rejects.toThrowError(ERROR_BAD_DATA) + }) + }) + + it("\(FAIL) [TX] Tag", async () => { + const transaction: Buffer = Buffer.concat([Buffer.from("TX"), msgpack.encode(randomBytes(64))]) + const metadata: SignMetadata = { encoding: Encoding.MSGPACK, schema: {} } + expect(cryptoService.signData(KeyContext.Identity,0, 0, transaction, metadata)).rejects.toThrowError(ERROR_BAD_DATA) + }) + + it("\(FAIL) [MX] Tag", async () => { + const transaction: Buffer = Buffer.concat([Buffer.from("MX"), msgpack.encode(randomBytes(64))]) + const metadata: SignMetadata = { encoding: Encoding.MSGPACK, schema: {} } + expect(cryptoService.signData(KeyContext.Identity,0, 0, transaction, metadata)).rejects.toThrowError(ERROR_BAD_DATA) + }) + + it("\(FAIL) [Program] Tag", async () => { + const transaction: Buffer = Buffer.concat([Buffer.from("Program"), msgpack.encode(randomBytes(64))]) + const metadata: SignMetadata = { encoding: Encoding.MSGPACK, schema: {} } + expect(cryptoService.signData(KeyContext.Identity,0, 0, transaction, metadata)).rejects.toThrowError(ERROR_BAD_DATA) + }) + + it("\(FAIL) [progData] Tag", async () => { + const transaction: Buffer = Buffer.concat([Buffer.from("progData"), msgpack.encode(randomBytes(64))]) + const metadata: SignMetadata = { encoding: Encoding.MSGPACK, schema: {} } + expect(cryptoService.signData(KeyContext.Identity,0, 0, transaction, metadata)).rejects.toThrowError(ERROR_BAD_DATA) + }) + }) + }) diff --git a/assets/arc-0052/contextual.api.crypto.ts b/assets/arc-0052/contextual.api.crypto.ts index 3105cb6f9..306befe67 100644 --- a/assets/arc-0052/contextual.api.crypto.ts +++ b/assets/arc-0052/contextual.api.crypto.ts @@ -9,7 +9,7 @@ import { crypto_sign_ed25519_pk_to_curve25519, crypto_scalarmult } from 'libsodium-wrappers-sumo'; - +import * as msgpack from "algo-msgpack-with-bigint" import Ajv from "ajv" import { deriveChildNodePrivate, fromSeed } from './bip32-ed25519'; @@ -54,6 +54,8 @@ function GetBIP44PathFromContext(context: KeyContext, account:number, key_index: } } +export const ERROR_BAD_DATA: Error = new Error("Invalid Data. Unable to decode or algorand protocol specific tags found") + export class ContextualCryptoApi { // Only for testing, seed shouldn't be persisted @@ -126,7 +128,7 @@ export class ContextualCryptoApi { async signData(context: KeyContext, account: number, keyIndex: number, data: Uint8Array, metadata: SignMetadata): Promise { // validate data if (!this.validateData(data, metadata)) { - throw new Error("Invalid data") + throw ERROR_BAD_DATA } await ready // libsodium @@ -177,21 +179,54 @@ export class ContextualCryptoApi { * @returns */ private validateData(message: Uint8Array, metadata: SignMetadata): boolean { + + // Check that decoded doesn't include the following prefixes: TX, MX, progData, Program + // These prefixes are reserved for the protocol + if (this.hasAlgorandTags(message)) { + return false + } + let decoded: Buffer switch (metadata.encoding) { case Encoding.BASE64: decoded = Buffer.from(message.toString(), 'base64') break + case Encoding.MSGPACK: + decoded = msgpack.decode(message) as Buffer + break default: throw new Error("Invalid encoding") } + // Check after decoding too + // Some one might try to encode a regular transaction with the protocol reserved prefixes + if (this.hasAlgorandTags(decoded)) { + return false + } + // validate with schema const ajv = new Ajv() const validate = ajv.compile(metadata.schema) return validate(decoded) } + /** + * Detect if the message has Algorand protocol specific tags + * + * @param message - raw bytes of the message + * @returns - true if message has Algorand protocol specific tags, false otherwise + */ + private hasAlgorandTags(message: Uint8Array): boolean { + if (message.subarray(0, 2).toString() === "TX" || + message.subarray(0, 2).toString() === "MX" || + message.subarray(0, 8).toString() === "progData" || + message.subarray(0, 7).toString() === "Program") { + return true + } + + return false + } + /** * Wrapper around libsodium basica signature verification diff --git a/assets/arc-0052/package.json b/assets/arc-0052/package.json index 779b1d0a1..35f98cb53 100644 --- a/assets/arc-0052/package.json +++ b/assets/arc-0052/package.json @@ -19,6 +19,7 @@ }, "dependencies": { "ajv": "^8.12.0", + "algo-msgpack-with-bigint": "^2.1.1", "bip39": "^3.1.0", "bn.js": "^5.2.1", "hi-base32": "^0.5.1", diff --git a/assets/arc-0052/yarn.lock b/assets/arc-0052/yarn.lock index d3f4f117d..4eb6dd839 100644 --- a/assets/arc-0052/yarn.lock +++ b/assets/arc-0052/yarn.lock @@ -674,6 +674,11 @@ ajv@^8.12.0: require-from-string "^2.0.2" uri-js "^4.2.2" +algo-msgpack-with-bigint@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/algo-msgpack-with-bigint/-/algo-msgpack-with-bigint-2.1.1.tgz#38bb717220525b3ff42232eefdcd9efb9ad405d6" + integrity sha512-F1tGh056XczEaEAqu7s+hlZUDWwOBT70Eq0lfMpBP2YguSQVyxRbprLq5rELXKQOyOaixTWYhMeMQMzP0U5FoQ== + ansi-escapes@^4.2.1: version "4.3.2" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" From ca9086836cf8abc671971a1ddabae94787c4ec71 Mon Sep 17 00:00:00 2001 From: ehanoc Date: Wed, 1 Nov 2023 13:46:04 +0000 Subject: [PATCH 15/47] Small refactor Signed-off-by: ehanoc --- assets/arc-0052/bip32-ed25519.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/assets/arc-0052/bip32-ed25519.ts b/assets/arc-0052/bip32-ed25519.ts index ecf9b865b..4b609957c 100644 --- a/assets/arc-0052/bip32-ed25519.ts +++ b/assets/arc-0052/bip32-ed25519.ts @@ -64,14 +64,15 @@ export function deriveChildNodePrivate( const { z, childChainCode } = index < 0x80000000 ? derivedNonHardened(kl, cc, index) : deriveHardened(kl, kr, cc, index); - var chainCode = childChainCode.subarray(32, 64); - var zl = z.subarray(0, 32); - var zr = z.subarray(32, 64); + const chainCode = childChainCode.subarray(32, 64); + const zl = z.subarray(0, 32); + const zr = z.subarray(32, 64); // left = kl + 8 * trunc28(zl) // right = zr + kr - var left = new BN(kl, 16, "le").add(new BN(zl.subarray(0, 28), 16, "le").mul(new BN(8))).toArrayLike(Buffer, "le", 32); - var right = new BN(kr, 16, "le").add(new BN(zr, 16, "le")).toArrayLike(Buffer, "le").slice(0, 32); + + const left = new BN(kl, 16, "le").add(new BN(zl.subarray(0, 28), 16, "le").mul(new BN(8))).toArrayLike(Buffer, "le", 32); + let right = new BN(kr, 16, "le").add(new BN(zr, 16, "le")).toArrayLike(Buffer, "le").slice(0, 32); // just padding if (right.length !== 32) { @@ -109,13 +110,11 @@ export function deriveChildNodePublic(extendedKey: Uint8Array, index: number): U const i: Buffer = createHmac("sha512", cc).update(data).digest(); - // truncanted to the 32 bytes on the right hand side // Section V. BIP32-Ed25519: Specification; subsection D) Public Child Key Derivation const chainCode: Buffer = i.subarray(32, 64); const zl: Buffer = z.subarray(0, 32); - // left = 8 * trunc28(zl) - // const scalar = new BN(zl.subarray(0, 28), 16, 'le').mul(new BN(8)); + // left = 8 * 28bytesOf(zl) const left = new BN(zl.subarray(0, 28), 16, 'le').mul(new BN(8)).toArrayLike(Buffer, 'le', 32); const p: Uint8Array = crypto_scalarmult_ed25519_base_noclamp(left); From 8e2990101369f252e7b08951fd44945d87b965ff Mon Sep 17 00:00:00 2001 From: ehanoc Date: Fri, 3 Nov 2023 10:53:16 +0000 Subject: [PATCH 16/47] Test Vectors against known ed25519 javascript lib Signed-off-by: ehanoc --- assets/arc-0052/contextual.api.crypto.spec.ts | 92 ++++++++++++++++++- assets/arc-0052/contextual.api.crypto.ts | 1 + assets/arc-0052/package.json | 1 + assets/arc-0052/yarn.lock | 63 ++++++++++++- 4 files changed, 153 insertions(+), 4 deletions(-) diff --git a/assets/arc-0052/contextual.api.crypto.spec.ts b/assets/arc-0052/contextual.api.crypto.spec.ts index e12ad3c44..d24dab870 100644 --- a/assets/arc-0052/contextual.api.crypto.spec.ts +++ b/assets/arc-0052/contextual.api.crypto.spec.ts @@ -1,8 +1,10 @@ -import { CryptoKX, KeyPair, crypto_kx_client_session_keys, crypto_kx_server_session_keys, crypto_scalarmult, crypto_secretbox_NONCEBYTES, crypto_secretbox_easy, crypto_secretbox_open_easy, crypto_sign_ed25519_pk_to_curve25519, crypto_sign_ed25519_sk_to_curve25519, crypto_sign_keypair, ready, to_base64 } from "libsodium-wrappers-sumo" +import { CryptoKX, KeyPair, crypto_kx_client_session_keys, crypto_kx_server_session_keys, crypto_scalarmult, crypto_scalarmult_ed25519_base_noclamp, crypto_secretbox_NONCEBYTES, crypto_secretbox_easy, crypto_secretbox_open_easy, crypto_sign_ed25519_pk_to_curve25519, crypto_sign_ed25519_sk_to_curve25519, crypto_sign_keypair, ready, to_base64 } from "libsodium-wrappers-sumo" import * as bip39 from "bip39" import { randomBytes } from "crypto" -import { ContextualCryptoApi, ERROR_BAD_DATA, Encoding, KeyContext, SignMetadata } from "./contextual.api.crypto" +import { ContextualCryptoApi, ERROR_BAD_DATA, Encoding, KeyContext, SignMetadata, harden } from "./contextual.api.crypto" import * as msgpack from "algo-msgpack-with-bigint" +import { fromSeed } from "./bip32-ed25519" +const libBip32Ed25519 = require('bip32-ed25519') describe("Contextual Derivation & Signing", () => { @@ -20,6 +22,92 @@ describe("Contextual Derivation & Signing", () => { afterEach(() => {}) + describe("\(JS Library) Reference Implementation alignment with known BIP32-Ed25519 JS LIB", () => { + it("\(OK) BIP32-Ed25519 derive key m'/44'/283'/0'/0/0", async () => { + await ready + const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 0) + + const rooKey: Uint8Array = fromSeed(seed) + + let derivedKey: Uint8Array = libBip32Ed25519.derivePrivate(Buffer.from(rooKey), harden(44)) + derivedKey = libBip32Ed25519.derivePrivate(derivedKey, harden(283)) + derivedKey = libBip32Ed25519.derivePrivate(derivedKey, harden(0)) + derivedKey = libBip32Ed25519.derivePrivate(derivedKey, 0) + derivedKey = libBip32Ed25519.derivePrivate(derivedKey, 0) + + const scalar = derivedKey.subarray(0, 32) // scalar == pvtKey + const derivedPub: Uint8Array = crypto_scalarmult_ed25519_base_noclamp(scalar) // calculate public key + expect(derivedPub).toEqual(key) + }) + + it("\(OK) BIP32-Ed25519 derive key m'/44'/283'/0'/0/1", async () => { + await ready + const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 1) + + const rooKey: Uint8Array = fromSeed(seed) + + let derivedKey: Uint8Array = libBip32Ed25519.derivePrivate(Buffer.from(rooKey), harden(44)) + derivedKey = libBip32Ed25519.derivePrivate(derivedKey, harden(283)) + derivedKey = libBip32Ed25519.derivePrivate(derivedKey, harden(0)) + derivedKey = libBip32Ed25519.derivePrivate(derivedKey, 0) + derivedKey = libBip32Ed25519.derivePrivate(derivedKey, 1) + + const scalar = derivedKey.subarray(0, 32) // scalar == pvtKey + const derivedPub: Uint8Array = crypto_scalarmult_ed25519_base_noclamp(scalar) // calculate public key + expect(derivedPub).toEqual(key) + }) + + it("\(OK) BIP32-Ed25519 derive PUBLIC key m'/44'/283'/1'/0/1", async () => { + await ready + const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 1, 1) + + const rooKey: Uint8Array = fromSeed(seed) + + let derivedKey: Uint8Array = libBip32Ed25519.derivePrivate(Buffer.from(rooKey), harden(44)) + derivedKey = libBip32Ed25519.derivePrivate(derivedKey, harden(283)) + derivedKey = libBip32Ed25519.derivePrivate(derivedKey, harden(1)) + + // ext private => ext public format! + const nodeScalar: Uint8Array = derivedKey.subarray(0, 32) + const nodePublic: Uint8Array = crypto_scalarmult_ed25519_base_noclamp(nodeScalar) + const nodeCC: Uint8Array = derivedKey.subarray(64, 96) + + // [Public][ChainCode] + const extPub: Buffer = Buffer.concat([nodePublic, nodeCC]) + + derivedKey = libBip32Ed25519.derivePublic(extPub, 0) + derivedKey = libBip32Ed25519.derivePublic(derivedKey, 1) + + const derivedPub = new Uint8Array(derivedKey.subarray(0, 32)) // public key from extended format + expect(derivedPub).toEqual(key) + }) + + it("\(OK) BIP32-Ed25519 derive PUBLIC key m'/44'/0'/1'/0/2", async () => { + await ready + const key: Uint8Array = await cryptoService.keyGen(KeyContext.Identity, 1, 2) + + const rooKey: Uint8Array = fromSeed(seed) + + let derivedKey: Uint8Array = libBip32Ed25519.derivePrivate(Buffer.from(rooKey), harden(44)) + derivedKey = libBip32Ed25519.derivePrivate(derivedKey, harden(0)) + derivedKey = libBip32Ed25519.derivePrivate(derivedKey, harden(1)) + + // ext private => ext public format! + const nodeScalar: Uint8Array = derivedKey.subarray(0, 32) + const nodePublic: Uint8Array = crypto_scalarmult_ed25519_base_noclamp(nodeScalar) + const nodeCC: Uint8Array = derivedKey.subarray(64, 96) + + // [Public][ChainCode] + const extPub: Buffer = Buffer.concat([nodePublic, nodeCC]) + + derivedKey = libBip32Ed25519.derivePublic(extPub, 0) + derivedKey = libBip32Ed25519.derivePublic(derivedKey, 2) + + const derivedPub = new Uint8Array(derivedKey.subarray(0, 32)) // public key from extended format + expect(derivedPub).toEqual(key) + }) + }) + describe("\(Derivations) Context", () => { describe("Addresses", () => { describe("Soft Derivations", () => { diff --git a/assets/arc-0052/contextual.api.crypto.ts b/assets/arc-0052/contextual.api.crypto.ts index 306befe67..92a7b5a3a 100644 --- a/assets/arc-0052/contextual.api.crypto.ts +++ b/assets/arc-0052/contextual.api.crypto.ts @@ -13,6 +13,7 @@ import * as msgpack from "algo-msgpack-with-bigint" import Ajv from "ajv" import { deriveChildNodePrivate, fromSeed } from './bip32-ed25519'; + /** * */ diff --git a/assets/arc-0052/package.json b/assets/arc-0052/package.json index 35f98cb53..21cf3dca6 100644 --- a/assets/arc-0052/package.json +++ b/assets/arc-0052/package.json @@ -20,6 +20,7 @@ "dependencies": { "ajv": "^8.12.0", "algo-msgpack-with-bigint": "^2.1.1", + "bip32-ed25519": "^0.0.4", "bip39": "^3.1.0", "bn.js": "^5.2.1", "hi-base32": "^0.5.1", diff --git a/assets/arc-0052/yarn.lock b/assets/arc-0052/yarn.lock index 4eb6dd839..28a22b2d1 100644 --- a/assets/arc-0052/yarn.lock +++ b/assets/arc-0052/yarn.lock @@ -790,6 +790,15 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +bip32-ed25519@^0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/bip32-ed25519/-/bip32-ed25519-0.0.4.tgz#218943e212c2d3152dfd6f3a929305e3fe86534c" + integrity sha512-KfazzGVLwl70WZ1r98dO+8yaJRTGgWHL9ITn4bXHQi2mB4cT3Hjh53tXWUpEWE1zKCln7PbyX8Z337VapAOb5w== + dependencies: + bn.js "^5.1.1" + elliptic "^6.4.1" + hash.js "^1.1.7" + bip39@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/bip39/-/bip39-3.1.0.tgz#c55a418deaf48826a6ceb34ac55b3ee1577e18a3" @@ -797,7 +806,12 @@ bip39@^3.1.0: dependencies: "@noble/hashes" "^1.2.0" -bn.js@^5.2.1: +bn.js@^4.11.9: + version "4.12.0" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" + integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== + +bn.js@^5.1.1, bn.js@^5.2.1: version "5.2.1" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== @@ -817,6 +831,11 @@ braces@^3.0.2: dependencies: fill-range "^7.0.1" +brorand@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w== + browserslist@^4.21.9: version "4.22.1" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.22.1.tgz#ba91958d1a59b87dab6fed8dfbcb3da5e2e9c619" @@ -1005,6 +1024,19 @@ electron-to-chromium@^1.4.535: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.569.tgz#1298b67727187ffbaac005a7425490d157f3ad03" integrity sha512-LsrJjZ0IbVy12ApW3gpYpcmHS3iRxH4bkKOW98y1/D+3cvDUWGcbzbsFinfUS8knpcZk/PG/2p/RnkMCYN7PVg== +elliptic@^6.4.1: + version "6.5.4" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" + integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== + dependencies: + bn.js "^4.11.9" + brorand "^1.1.0" + hash.js "^1.0.0" + hmac-drbg "^1.0.1" + inherits "^2.0.4" + minimalistic-assert "^1.0.1" + minimalistic-crypto-utils "^1.0.1" + emittery@^0.13.1: version "0.13.1" resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad" @@ -1172,6 +1204,14 @@ has-flag@^4.0.0: resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +hash.js@^1.0.0, hash.js@^1.0.3, hash.js@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" + integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.1" + hasown@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c" @@ -1184,6 +1224,15 @@ hi-base32@^0.5.1: resolved "https://registry.yarnpkg.com/hi-base32/-/hi-base32-0.5.1.tgz#1279f2ddae2673219ea5870c2121d2a33132857e" integrity sha512-EmBBpvdYh/4XxsnUybsPag6VikPYnN30td+vQk+GI3qpahVEG9+gTkG0aXVxTjBqQ5T6ijbWIu77O+C5WFWsnA== +hmac-drbg@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + integrity sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg== + dependencies: + hash.js "^1.0.3" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.1" + html-escaper@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" @@ -1215,7 +1264,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2: +inherits@2, inherits@^2.0.3, inherits@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -1796,6 +1845,16 @@ mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== +minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimalistic-crypto-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== + minimatch@^3.0.4, minimatch@^3.1.1: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" From 8ad26e3418fbf6d5e65486842adc96798a7ac0a6 Mon Sep 17 00:00:00 2001 From: ehanoc Date: Fri, 3 Nov 2023 12:53:07 +0000 Subject: [PATCH 17/47] Test Vectors bip39 bip32-ed25519 bip44 Signed-off-by: ehanoc --- assets/arc-0052/README.md | 105 +++++++++++++----- assets/arc-0052/contextual.api.crypto.spec.ts | 15 ++- assets/arc-0052/contextual.api.crypto.ts | 4 +- 3 files changed, 91 insertions(+), 33 deletions(-) diff --git a/assets/arc-0052/README.md b/assets/arc-0052/README.md index 8d823c1fc..3761084c9 100644 --- a/assets/arc-0052/README.md +++ b/assets/arc-0052/README.md @@ -16,41 +16,92 @@ $ yarn test ## Output ```shell - PASS ./contextual.api.crypto.spec.ts (5.393 s) + PASS ./contextual.api.crypto.spec.ts Contextual Derivation & Signing + ✓ (OK) Root Key (2 ms) + (JS Library) Reference Implementation alignment with known BIP32-Ed25519 JS LIB + ✓ (OK) BIP32-Ed25519 derive key m'/44'/283'/0'/0/0 (135 ms) + ✓ (OK) BIP32-Ed25519 derive key m'/44'/283'/0'/0/1 (120 ms) + ✓ (OK) BIP32-Ed25519 derive PUBLIC key m'/44'/283'/1'/0/1 (284 ms) + ✓ (OK) BIP32-Ed25519 derive PUBLIC key m'/44'/0'/1'/0/2 (277 ms) (Derivations) Context - ✓ (OK) ECDH (342 ms) - ✓ (OK) ECDH, Encrypt and Decrypt (365 ms) - ✓ Libsodium example ECDH (4 ms) + ✓ (OK) ECDH (4 ms) + ✓ (OK) ECDH, Encrypt and Decrypt (5 ms) + ✓ Libsodium example ECDH (8 ms) Addresses Soft Derivations - ✓ (OK) Derive m'/44'/283'/0'/0/0 Algorand Address Key (152 ms) - ✓ (OK) Derive m'/44'/283'/0'/0/1 Algorand Address Key (115 ms) - ✓ (OK) Derive m'/44'/283'/0'/0/2 Algorand Address Key (88 ms) - ✓ (OK) Derive m'/44'/283'/3'/0/0 Algorand Address Key (98 ms) + ✓ (OK) Derive m'/44'/283'/0'/0/0 Algorand Address Key (1 ms) + ✓ (OK) Derive m'/44'/283'/0'/0/1 Algorand Address Key (1 ms) + ✓ (OK) Derive m'/44'/283'/0'/0/2 Algorand Address Key (2 ms) Hard Derivations - ✓ (OK) Derive m'/44'/283'/1'/0/0 Algorand Address Key (94 ms) - ✓ (OK) Derive m'/44'/283'/2'/0/1 Algorand Address Key (96 ms) - Ledger Addresses test Vectors - ✓ (OK) Derive m'/44'/283'/0'/0/0 Algorand Address (94 ms) - ✓ (OK) Derive m'/44'/283'/1'/0/0 Algorand Address (90 ms) - ✓ (OK) Derive m'/44'/283'/2'/0/0 Algorand Address (92 ms) - ✓ (OK) Derive m'/44'/283'/3'/0/0 Algorand Address (91 ms) - ✓ (OK) Derive m'/44'/283'/4'/0/0 Algorand Address (100 ms) - ✓ (OK) Derive m'/44'/283'/5'/0/0 Algorand Address (93 ms) + ✓ (OK) Derive m'/44'/283'/1'/0/0 Algorand Address Key (3 ms) + ✓ (OK) Derive m'/44'/283'/2'/0/1 Algorand Address Key (2 ms) + ✓ (OK) Derive m'/44'/283'/3'/0/0 Algorand Address Key (1 ms) Identities Soft Derivations - ✓ (OK) Derive m'/44'/0'/0'/0/0 Identity Key (95 ms) - ✓ (OK) Derive m'/44'/0'/0'/0/1 Identity Key (94 ms) - ✓ (OK) Derive m'/44'/0'/0'/0/2 Identity Key (91 ms) + ✓ (OK) Derive m'/44'/0'/0'/0/0 Identity Key (1 ms) + ✓ (OK) Derive m'/44'/0'/0'/0/1 Identity Key (2 ms) + ✓ (OK) Derive m'/44'/0'/0'/0/2 Identity Key (1 ms) Hard Derivations - ✓ (OK) Derive m'/44'/0'/1'/0/0 Identity Key (89 ms) - ✓ (OK) Derive m'/44'/0'/2'/0/1 Identity Key (95 ms) + ✓ (OK) Derive m'/44'/0'/1'/0/0 Identity Key (2 ms) + ✓ (OK) Derive m'/44'/0'/2'/0/1 Identity Key (1 ms) Signing Typed Data - ✓ (OK) Sign Arbitrary Message against Schem (232 ms) - ✓ (FAIL) Signing attempt fails because of invalid data against Schema (38 ms) + ✓ (OK) Sign Arbitrary Message against Schem (54 ms) + ✓ (FAIL) Signing attempt fails because of invalid data against Schema (33 ms) + Reject Regular Transaction Signing. IF TAG Prexies are present signing must fail + ✓ (FAIL) [TX] Tag + ✓ (FAIL) [MX] Tag (1 ms) + ✓ (FAIL) [Program] Tag + ✓ (FAIL) [progData] Tag (1 ms) + Reject tags present in the encoded payload + ✓ (FAIL) [TX] Tag (2 ms) + ✓ (FAIL) [MX] Tag + ✓ (FAIL) [Program] Tag (1 ms) + ✓ (FAIL) [progData] Tag -Test Suites: 1 passed, 1 total -Tests: 22 passed, 22 total -``` \ No newline at end of file +``` + +## BIP39 / BIP32-ed25519 / BIP44 Test Vectors + +All keys are in extended format: [kl][kr][chaincode] + +Public key = kl * Ed25519GeneratorPoint + +- `BIP39 mnemonic`: _salon zoo engage submit smile frost later decide wing sight chaos renew lizard rely canal coral scene hobby scare step bus leaf tobacco slice_ + +- `root key (hex)`: a8ba80028922d9fcfa055c78aede55b5c575bcd8d5a53168edf45f36d9ec8f4694592b4bc892907583e22669ecdf1b0409a9f3bd5549f2dd751b51360909cd05b4b67277d74d4ddb3688daeeb02075482ceb812db8a5757c9e792d14ec791554 + +### BIP44 paths + +#### Child Private Derivation + +- `m'/44'/283'/0'/0/0`: 70db1fe0dd722955f44b57b26eee09ac282172559eb2bc6b408f69e2e9ec8f461b8fbc75f0e2a9f454d7de35a812c97fa08b984df342389f4d1f9aec5637e67468c11d826d16f7f505d27bd52c314caea79a7b9b6e87f5aa7e20f6cf06ac51f9 + - corresponding public key: 7915e7ecbaad1dc9bc22a9e496686687f1a8cb4895b7ca46f86d64dd56c6cd97 (kl * Ed25519GeneratorPoint) + +- `m'/44'/283'/0'/0/1`: 58678408f4f88d7ec700be1090940bedd584c21dc7f8b00028b69e01edec8f4645779d1c90ecc5168945024e4201552a349a825518badb8d4b017bf1dcce4dac910cb8dbcd10680c7754ca101089f4c9da61a2dff79e0301d9a1fb7656fb8c73 + - public key: 054a6881d8809c348a402d67ba2feedcd8e3145f40f21a6bbd0de09c30c78d0a + + +- `m'/44'/283'/0'/0/2`: 88ce207945dfecea41acbb2a9b4563268d3bee03ea2e199af502c500e7ec8f467b95d890e8c7e5aa79d9905f3c6794e1fc3bb9478552b7f24c751b94dd3becd76699130de29273b2ae742991b7daa0adae6fb656038b25895d4af3c85769a64d + - public key: 8cea8052cfa1fd8cec0b4fad6241a91f2edbfe9f072586f243839174e40a25ef + +- `m'/44'/283'/1'/0/0`: 18ada9291ae27f7415e99f4485d81493e68952c8b1af4f335cc52962ecec8f464c106ef1f56fa64b2a13631a68a6d6c9902eec52e2c6226a3ed953ae94bc8a613303bbf403f94b5eb189eed703b3985e12c6726d6b06d4ed8aac3d5b258e3c8c + - public key: 04f3ba279aa781ab4f8f79aaf6cf91e3d7ff75429064dc757001a30e10c628df + +- `m'/44'/283'/2'/0/0`: 209dc0b3458ee1f7484189bf584c02807f1a5726168aff5ea14fc117f3ec8f46b68ea14ca84c0da34aa4990416c6da01b14fbe30c5c225238515a93a0a28a33dbecbe8e64a514d44c8c0a3775844d5aa8e18ea2182321f1ff87061e49adc4a77 + - public key: 400d78302258dc7b3cb56d1a09f85b018e8100865ced0b5cda474c26bbc07c30 + + +- `m'/44'/283'/3'/0/0`: 3008165b92e6b29d6a3f28b593f15dcf6ce4c9a5a1aadc3a43ca068ce7ec8f46b8251db83f68dedaee1054ec545257f95ba00e33566d99926bef8743b77b42b8867472d1f1886c88ec36991f14a333454003236b5375d4a8bf4f01b2ff85ec9d + - public key: cf8d28a3d41bc656acbfeadb64d06054142c97bee6a987c11d934f84853df866 + +- `m'/44'/0'/0'/0/0`: 08b10dfcfc37a0cc998ec3305c6c32d4412de73f3f9633342248d14aeeec8f461997b1136524a5d3499d933c6f6e739d38fbf7404a183f4d835c6fe105cf44d016634694e4087b2d547c8b550a0f053d2fba990ba7e5e58186963393994d06a9 + - public key: 28804f08d8c145e172c998fe75058237b8181846ca763894ae3eefea6ab88352 + +- `m'/44'/0'/0'/0/1`: 80d6dd1cefb42fc93187739ad102e5dfd533b54b847b0693661bfee9edec8f466cab918344a8e7e9ae196d94cce301aa1d7360f550b25f3dc7468a006e423e3de80e43cd2588a9996b8156d3dc233ca31470f7d49261edd2e13d8c4c0096163c + - public key: fc8d1c79edd406fa415cb0a76435eb83b6f8af72cd9bd673753471470205057a + +- `m'/44'/0'/0'/0/2`: 7847139bee38330c809a56f3d2261bfbd7da7c1c88999c38bb12175cefec8f467617b7dba8330b644eae1c24a2d6b7211f5f36d9919d9a240777fa32d382feb3debfe21afa0de5021342f3bfafe18a91e11c441ab98ff5bcbba2dfba3190ce6f + - public key: f7e317899420454886fe79f24e25af0bbb1856c440b14829674015e5fc2ad28a + diff --git a/assets/arc-0052/contextual.api.crypto.spec.ts b/assets/arc-0052/contextual.api.crypto.spec.ts index d24dab870..704f77d6f 100644 --- a/assets/arc-0052/contextual.api.crypto.spec.ts +++ b/assets/arc-0052/contextual.api.crypto.spec.ts @@ -108,6 +108,12 @@ describe("Contextual Derivation & Signing", () => { }) }) + it("\(OK) Root Key", async () => { + const rootKey: Uint8Array = fromSeed(seed) + expect(rootKey.length).toBe(96) + expect(Buffer.from(rootKey)).toEqual(Buffer.from("a8ba80028922d9fcfa055c78aede55b5c575bcd8d5a53168edf45f36d9ec8f4694592b4bc892907583e22669ecdf1b0409a9f3bd5549f2dd751b51360909cd05b4b67277d74d4ddb3688daeeb02075482ceb812db8a5757c9e792d14ec791554", "hex")) + }) + describe("\(Derivations) Context", () => { describe("Addresses", () => { describe("Soft Derivations", () => { @@ -126,10 +132,6 @@ describe("Contextual Derivation & Signing", () => { expect(key).toEqual(new Uint8Array(Buffer.from("8cea8052cfa1fd8cec0b4fad6241a91f2edbfe9f072586f243839174e40a25ef", "hex"))) }) - it("\(OK) Derive m'/44'/283'/3'/0/0 Algorand Address Key", async () => { - const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 3, 0) - expect(key).toEqual(new Uint8Array(Buffer.from("cf8d28a3d41bc656acbfeadb64d06054142c97bee6a987c11d934f84853df866", "hex"))) - }) }) describe("Hard Derivations", () => { @@ -142,6 +144,11 @@ describe("Contextual Derivation & Signing", () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 2, 1) expect(key).toEqual(new Uint8Array(Buffer.from("400d78302258dc7b3cb56d1a09f85b018e8100865ced0b5cda474c26bbc07c30", "hex"))) }) + + it("\(OK) Derive m'/44'/283'/3'/0/0 Algorand Address Key", async () => { + const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 3, 0) + expect(key).toEqual(new Uint8Array(Buffer.from("cf8d28a3d41bc656acbfeadb64d06054142c97bee6a987c11d934f84853df866", "hex"))) + }) }) }) diff --git a/assets/arc-0052/contextual.api.crypto.ts b/assets/arc-0052/contextual.api.crypto.ts index 92a7b5a3a..c9e58f04b 100644 --- a/assets/arc-0052/contextual.api.crypto.ts +++ b/assets/arc-0052/contextual.api.crypto.ts @@ -71,10 +71,10 @@ export class ContextualCryptoApi { * @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 - if true, return the private key, otherwise return the public key - * @returns + * @returns - The public key of 32 bytes. If isPrivate is true, returns the private key instead. */ private async deriveKey(rootKey: Uint8Array, bip44Path: number[], isPrivate: boolean = true): Promise { - let derived = deriveChildNodePrivate(Buffer.from(rootKey), bip44Path[0]) + let derived: Uint8Array = deriveChildNodePrivate(Buffer.from(rootKey), bip44Path[0]) derived = deriveChildNodePrivate(derived, bip44Path[1]) derived = deriveChildNodePrivate(derived, bip44Path[2]) derived = deriveChildNodePrivate(derived, bip44Path[3]) From 6692a77221ef13ba45d5cba0215d0302aecf971d Mon Sep 17 00:00:00 2001 From: ehanoc Date: Sat, 4 Nov 2023 22:08:00 +0000 Subject: [PATCH 18/47] Authentication challenge Schema example. Added cases for when encoding is not supplied. README update Signed-off-by: ehanoc --- ARCs/arc-0052.md | 4 +- assets/arc-0052/contextual.api.crypto.spec.ts | 78 ++++++- assets/arc-0052/contextual.api.crypto.ts | 49 +++-- assets/arc-0052/schemas/auth.request.json | 203 ++++++++++++++++++ 4 files changed, 308 insertions(+), 26 deletions(-) create mode 100644 assets/arc-0052/schemas/auth.request.json diff --git a/ARCs/arc-0052.md b/ARCs/arc-0052.md index e7df2a6ca..726dd5366 100644 --- a/ARCs/arc-0052.md +++ b/ARCs/arc-0052.md @@ -99,6 +99,8 @@ Signing operation that can be used to perform arbitrary signing operations in a - Identity signatures (Documents, DIDs, etc) - Address signatures (i.e Signing auth challenges) +The function should check if the data, before and after decoding, isn't a regular Algorand transaction. If it is, the function should throw an error. + - **method name**: `sign` - **parameters**: - `data`: The data to be signed @@ -107,7 +109,7 @@ Signing operation that can be used to perform arbitrary signing operations in a - `keyIndex`: The key index to be used for key derivation. - `metadata`: Object to describe the type of data that is being signed. It should specify the encoding used (i.e msgpack, cbor, etc) and a schema so that the wallet can decode it to show the user. - `encoding`: The encoding used for the data - - `schema`: The schema of the data in the form of a JSON schema + - `schema`: The schema of the data in the form of a JSON schema. The schemas are an powerful tool for wallet providers to be able to enforce the type and format of the data that is being signed. - **returns**: Byte array containing the signature. The signature is encoded as a 64-byte array (32 + 32 bytes). It holds a compressed point R + the integer s (confirming that the signer knows the msg and the privKey). - Throws error if data doesn't match the schema diff --git a/assets/arc-0052/contextual.api.crypto.spec.ts b/assets/arc-0052/contextual.api.crypto.spec.ts index 704f77d6f..a5d454c26 100644 --- a/assets/arc-0052/contextual.api.crypto.spec.ts +++ b/assets/arc-0052/contextual.api.crypto.spec.ts @@ -1,11 +1,38 @@ import { CryptoKX, KeyPair, crypto_kx_client_session_keys, crypto_kx_server_session_keys, crypto_scalarmult, crypto_scalarmult_ed25519_base_noclamp, crypto_secretbox_NONCEBYTES, crypto_secretbox_easy, crypto_secretbox_open_easy, crypto_sign_ed25519_pk_to_curve25519, crypto_sign_ed25519_sk_to_curve25519, crypto_sign_keypair, ready, to_base64 } from "libsodium-wrappers-sumo" import * as bip39 from "bip39" import { randomBytes } from "crypto" -import { ContextualCryptoApi, ERROR_BAD_DATA, Encoding, KeyContext, SignMetadata, harden } from "./contextual.api.crypto" +import { ContextualCryptoApi, ERROR_BAD_DATA, ERROR_TAGS_FOUND, Encoding, KeyContext, SignMetadata, harden } from "./contextual.api.crypto" import * as msgpack from "algo-msgpack-with-bigint" import { fromSeed } from "./bip32-ed25519" +import { sha512_256 } from "js-sha512" +import base32 from "hi-base32" +import { JSONSchemaType } from "ajv" +import { readFileSync } from "fs" +import path from "path" const libBip32Ed25519 = require('bip32-ed25519') +function encodeAddress(publicKey: Buffer): string { + const keyHash: string = sha512_256.create().update(publicKey).hex() + + // last 4 bytes of the hash + const checksum: string = keyHash.slice(-8) + + return base32.encode(ConcatArrays(publicKey, Buffer.from(checksum, "hex"))).slice(0, 58) +} + +function ConcatArrays(...arrs: ArrayLike[]) { + const size = arrs.reduce((sum, arr) => sum + arr.length, 0) + const c = new Uint8Array(size) + + let offset = 0 + for (let i = 0; i < arrs.length; i++) { + c.set(arrs[i], offset) + offset += arrs[i].length + } + + return c +} + describe("Contextual Derivation & Signing", () => { let cryptoService: ContextualCryptoApi @@ -184,6 +211,37 @@ describe("Contextual Derivation & Signing", () => { }) describe("Signing Typed Data", () => { + it("\(OK) Sign authentication challenge of 32 bytes, encoded base 64", async () => { + const challenge: Uint8Array = new Uint8Array(randomBytes(32)) + + // read auth schema file for authentication. 32 bytes challenge to sign + const authSchema: JSONSchemaType = JSON.parse(readFileSync(path.resolve(__dirname, "schemas/auth.request.json"), "utf8")) + const metadata: SignMetadata = { encoding: Encoding.BASE64, schema: authSchema } + const base64Challenge: string = Buffer.from(challenge).toString("base64") + + const encoded: Uint8Array = new Uint8Array(Buffer.from(base64Challenge)) + + const signature: Uint8Array = await cryptoService.signData(KeyContext.Address,0, 0, encoded, metadata) + expect(signature).toHaveLength(64) + + const isValid: boolean = await cryptoService.verifyWithPublicKey(signature, encoded, await cryptoService.keyGen(KeyContext.Address, 0, 0)) + expect(isValid).toBe(true) + }) + + it ("\(OK) Sign authentication challenge of 32 bytes, no encoding", async () => { + const challenge: Uint8Array = new Uint8Array(randomBytes(32)) + + // read auth schema file for authentication. 32 bytes challenge to sign + const authSchema: JSONSchemaType = JSON.parse(readFileSync(path.resolve(__dirname, "schemas/auth.request.json"), "utf8")) + const metadata: SignMetadata = { encoding: Encoding.NONE, schema: authSchema } + + const signature: Uint8Array = await cryptoService.signData(KeyContext.Address,0, 0, challenge, metadata) + expect(signature).toHaveLength(64) + + const isValid: boolean = await cryptoService.verifyWithPublicKey(signature, challenge, await cryptoService.keyGen(KeyContext.Address, 0, 0)) + expect(isValid).toBe(true) + }) + it("\(OK) Sign Arbitrary Message against Schem", async () => { const firstKey: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 0) @@ -234,8 +292,8 @@ describe("Contextual Derivation & Signing", () => { const transaction: Buffer = Buffer.concat([Buffer.from("TX"), msgpack.encode(randomBytes(64))]) const metadata: SignMetadata = { encoding: Encoding.BASE64, schema: {} } - const encodedTx: Buffer = Buffer.from(transaction.toString('base64')) - expect(cryptoService.signData(KeyContext.Identity,0, 0, encodedTx, metadata)).rejects.toThrowError(ERROR_BAD_DATA) + const encodedTx: Uint8Array = new Uint8Array(Buffer.from(transaction.toString('base64'))) + expect(cryptoService.signData(KeyContext.Identity,0, 0, encodedTx, metadata)).rejects.toThrowError(ERROR_TAGS_FOUND) }) it("\(FAIL) [MX] Tag", async () => { @@ -243,7 +301,7 @@ describe("Contextual Derivation & Signing", () => { const metadata: SignMetadata = { encoding: Encoding.BASE64, schema: {} } const encodedTx: Buffer = Buffer.from(transaction.toString('base64')) - expect(cryptoService.signData(KeyContext.Identity,0, 0, encodedTx, metadata)).rejects.toThrowError(ERROR_BAD_DATA) + expect(cryptoService.signData(KeyContext.Identity,0, 0, encodedTx, metadata)).rejects.toThrowError(ERROR_TAGS_FOUND) }) it("\(FAIL) [Program] Tag", async () => { @@ -251,7 +309,7 @@ describe("Contextual Derivation & Signing", () => { const metadata: SignMetadata = { encoding: Encoding.BASE64, schema: {} } const encodedTx: Buffer = Buffer.from(transaction.toString('base64')) - expect(cryptoService.signData(KeyContext.Identity,0, 0, encodedTx, metadata)).rejects.toThrowError(ERROR_BAD_DATA) + expect(cryptoService.signData(KeyContext.Identity,0, 0, encodedTx, metadata)).rejects.toThrowError(ERROR_TAGS_FOUND) }) it("\(FAIL) [progData] Tag", async () => { @@ -259,32 +317,32 @@ describe("Contextual Derivation & Signing", () => { const metadata: SignMetadata = { encoding: Encoding.BASE64, schema: {} } const encodedTx: Buffer = Buffer.from(transaction.toString('base64')) - expect(cryptoService.signData(KeyContext.Identity,0, 0, encodedTx, metadata)).rejects.toThrowError(ERROR_BAD_DATA) + expect(cryptoService.signData(KeyContext.Identity,0, 0, encodedTx, metadata)).rejects.toThrowError(ERROR_TAGS_FOUND) }) }) it("\(FAIL) [TX] Tag", async () => { const transaction: Buffer = Buffer.concat([Buffer.from("TX"), msgpack.encode(randomBytes(64))]) const metadata: SignMetadata = { encoding: Encoding.MSGPACK, schema: {} } - expect(cryptoService.signData(KeyContext.Identity,0, 0, transaction, metadata)).rejects.toThrowError(ERROR_BAD_DATA) + expect(cryptoService.signData(KeyContext.Identity,0, 0, transaction, metadata)).rejects.toThrowError(ERROR_TAGS_FOUND) }) it("\(FAIL) [MX] Tag", async () => { const transaction: Buffer = Buffer.concat([Buffer.from("MX"), msgpack.encode(randomBytes(64))]) const metadata: SignMetadata = { encoding: Encoding.MSGPACK, schema: {} } - expect(cryptoService.signData(KeyContext.Identity,0, 0, transaction, metadata)).rejects.toThrowError(ERROR_BAD_DATA) + expect(cryptoService.signData(KeyContext.Identity,0, 0, transaction, metadata)).rejects.toThrowError(ERROR_TAGS_FOUND) }) it("\(FAIL) [Program] Tag", async () => { const transaction: Buffer = Buffer.concat([Buffer.from("Program"), msgpack.encode(randomBytes(64))]) const metadata: SignMetadata = { encoding: Encoding.MSGPACK, schema: {} } - expect(cryptoService.signData(KeyContext.Identity,0, 0, transaction, metadata)).rejects.toThrowError(ERROR_BAD_DATA) + expect(cryptoService.signData(KeyContext.Identity,0, 0, transaction, metadata)).rejects.toThrowError(ERROR_TAGS_FOUND) }) it("\(FAIL) [progData] Tag", async () => { const transaction: Buffer = Buffer.concat([Buffer.from("progData"), msgpack.encode(randomBytes(64))]) const metadata: SignMetadata = { encoding: Encoding.MSGPACK, schema: {} } - expect(cryptoService.signData(KeyContext.Identity,0, 0, transaction, metadata)).rejects.toThrowError(ERROR_BAD_DATA) + expect(cryptoService.signData(KeyContext.Identity,0, 0, transaction, metadata)).rejects.toThrowError(ERROR_TAGS_FOUND) }) }) diff --git a/assets/arc-0052/contextual.api.crypto.ts b/assets/arc-0052/contextual.api.crypto.ts index c9e58f04b..4c128cab9 100644 --- a/assets/arc-0052/contextual.api.crypto.ts +++ b/assets/arc-0052/contextual.api.crypto.ts @@ -34,7 +34,8 @@ export interface ChannelKeys { export enum Encoding { CBOR = "cbor", MSGPACK = "msgpack", - BASE64 = "base64" + BASE64 = "base64", + NONE = "none" } export interface SignMetadata { @@ -55,7 +56,8 @@ function GetBIP44PathFromContext(context: KeyContext, account:number, key_index: } } -export const ERROR_BAD_DATA: Error = new Error("Invalid Data. Unable to decode or algorand protocol specific tags found") +export const ERROR_BAD_DATA: Error = new Error("Invalid Data") +export const ERROR_TAGS_FOUND: Error = new Error("Transactions tags found") export class ContextualCryptoApi { @@ -128,7 +130,13 @@ export class ContextualCryptoApi { * */ async signData(context: KeyContext, account: number, keyIndex: number, data: Uint8Array, metadata: SignMetadata): Promise { // validate data - if (!this.validateData(data, metadata)) { + const result: boolean | Error = this.validateData(data, metadata) + + if (result instanceof Error) { // decoding errors + throw result + } + + if (!result) { // failed schema validation throw ERROR_BAD_DATA } @@ -179,36 +187,45 @@ export class ContextualCryptoApi { * @param metadata * @returns */ - private validateData(message: Uint8Array, metadata: SignMetadata): boolean { + private validateData(message: Uint8Array, metadata: SignMetadata): boolean | Error { // Check that decoded doesn't include the following prefixes: TX, MX, progData, Program // These prefixes are reserved for the protocol if (this.hasAlgorandTags(message)) { - return false + return ERROR_TAGS_FOUND } - let decoded: Buffer + let decoded: Uint8Array switch (metadata.encoding) { case Encoding.BASE64: - decoded = Buffer.from(message.toString(), 'base64') + decoded = new Uint8Array(Buffer.from(Buffer.from(message).toString(), 'base64')) break case Encoding.MSGPACK: - decoded = msgpack.decode(message) as Buffer + decoded = msgpack.decode(message) as Uint8Array + break + + case Encoding.NONE: + decoded = message break default: throw new Error("Invalid encoding") } - + // Check after decoding too // Some one might try to encode a regular transaction with the protocol reserved prefixes if (this.hasAlgorandTags(decoded)) { - return false + return ERROR_TAGS_FOUND } // validate with schema const ajv = new Ajv() const validate = ajv.compile(metadata.schema) - return validate(decoded) + + const valid = validate(decoded) + + if (!valid) console.log(ajv.errors) + + return valid } /** @@ -218,10 +235,12 @@ export class ContextualCryptoApi { * @returns - true if message has Algorand protocol specific tags, false otherwise */ private hasAlgorandTags(message: Uint8Array): boolean { - if (message.subarray(0, 2).toString() === "TX" || - message.subarray(0, 2).toString() === "MX" || - message.subarray(0, 8).toString() === "progData" || - message.subarray(0, 7).toString() === "Program") { + + // Check that decoded doesn't include the following prefixes: TX, MX, progData, Program + if (Buffer.from(message.subarray(0, 2)).toString("ascii") === "TX" || + Buffer.from(message.subarray(0, 2)).toString("ascii") === "MX" || + Buffer.from(message.subarray(0, 8)).toString("ascii") === "progData" || + Buffer.from(message.subarray(0, 7)).toString("ascii") === "Program") { return true } diff --git a/assets/arc-0052/schemas/auth.request.json b/assets/arc-0052/schemas/auth.request.json new file mode 100644 index 000000000..9097118e3 --- /dev/null +++ b/assets/arc-0052/schemas/auth.request.json @@ -0,0 +1,203 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://arc52/schemas/auth.request.json", + "title": "Payment Transaction", + "type": "object", + "additionalProperties": false, + "properties": { + "0": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "1": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "2": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "3": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "4": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "5": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "6": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "7": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "8": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "9": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "10": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "11": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "12": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "13": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "14": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "15": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "16": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "17": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "18": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "19": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "20": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "21": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "22": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "23": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "24": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "25": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "26": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "27": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "28": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "29": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "30": { + "type": "integer", + "minimum": 0, + "maximum": 255 + }, + "31": { + "type": "integer", + "minimum": 0, + "maximum": 255 + } + }, + "required": [ + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "12", + "13", + "14", + "15", + "16", + "17", + "18", + "19", + "20", + "21", + "22", + "23", + "24", + "25", + "26", + "27", + "28", + "29", + "30", + "31" + ] +} \ No newline at end of file From f45955386228cc19c2d048ebc72639c3f9d5ce6a Mon Sep 17 00:00:00 2001 From: ehanoc Date: Wed, 8 Nov 2023 10:53:34 +0000 Subject: [PATCH 19/47] Link schema examples on spec Signed-off-by: ehanoc --- ARCs/arc-0052.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ARCs/arc-0052.md b/ARCs/arc-0052.md index 726dd5366..1127601c5 100644 --- a/ARCs/arc-0052.md +++ b/ARCs/arc-0052.md @@ -109,7 +109,7 @@ The function should check if the data, before and after decoding, isn't a regula - `keyIndex`: The key index to be used for key derivation. - `metadata`: Object to describe the type of data that is being signed. It should specify the encoding used (i.e msgpack, cbor, etc) and a schema so that the wallet can decode it to show the user. - `encoding`: The encoding used for the data - - `schema`: The schema of the data in the form of a JSON schema. The schemas are an powerful tool for wallet providers to be able to enforce the type and format of the data that is being signed. + - `schema`: The schema of the data in the form of a JSON schema. The schemas are an powerful tool for wallet providers to be able to enforce the type and format of the data that is being signed. You can check schema examples [here](../assets/arc-0052/schemas/) - **returns**: Byte array containing the signature. The signature is encoded as a 64-byte array (32 + 32 bytes). It holds a compressed point R + the integer s (confirming that the signer knows the msg and the privKey). - Throws error if data doesn't match the schema From ea9dd28eef7698ae1caf484548a4f7a1c80cf319 Mon Sep 17 00:00:00 2001 From: ehanoc Date: Mon, 27 Nov 2023 08:53:56 +0000 Subject: [PATCH 20/47] Fix: SHA256(0x01||seed), 0x01 was 0x00 array Signed-off-by: ehanoc --- assets/arc-0052/bip32-ed25519.ts | 3 ++- assets/arc-0052/contextual.api.crypto.spec.ts | 26 +++++++++---------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/assets/arc-0052/bip32-ed25519.ts b/assets/arc-0052/bip32-ed25519.ts index 4b609957c..62c8997ca 100644 --- a/assets/arc-0052/bip32-ed25519.ts +++ b/assets/arc-0052/bip32-ed25519.ts @@ -37,7 +37,8 @@ export function fromSeed(seed: Buffer): Uint8Array { kL[31] |= 0b01_00_00_00; // the second highest bit of the last byte is set // chain root code - const c: Buffer = createHash("sha256").update(Buffer.concat([new Uint8Array(0x01), seed])).digest(); + // SHA256(0x01||k) + const c: Buffer = createHash("sha256").update(Buffer.concat([new Uint8Array([0x01]), seed])).digest(); return new Uint8Array(Buffer.concat([kL, kR, c])); } diff --git a/assets/arc-0052/contextual.api.crypto.spec.ts b/assets/arc-0052/contextual.api.crypto.spec.ts index a5d454c26..688dd98aa 100644 --- a/assets/arc-0052/contextual.api.crypto.spec.ts +++ b/assets/arc-0052/contextual.api.crypto.spec.ts @@ -138,7 +138,7 @@ describe("Contextual Derivation & Signing", () => { it("\(OK) Root Key", async () => { const rootKey: Uint8Array = fromSeed(seed) expect(rootKey.length).toBe(96) - expect(Buffer.from(rootKey)).toEqual(Buffer.from("a8ba80028922d9fcfa055c78aede55b5c575bcd8d5a53168edf45f36d9ec8f4694592b4bc892907583e22669ecdf1b0409a9f3bd5549f2dd751b51360909cd05b4b67277d74d4ddb3688daeeb02075482ceb812db8a5757c9e792d14ec791554", "hex")) + expect(Buffer.from(rootKey)).toEqual(Buffer.from("a8ba80028922d9fcfa055c78aede55b5c575bcd8d5a53168edf45f36d9ec8f4694592b4bc892907583e22669ecdf1b0409a9f3bd5549f2dd751b51360909cd05796b9206ec30e142e94b790a98805bf999042b55046963174ee6cee2d0375946", "hex")) }) describe("\(Derivations) Context", () => { @@ -146,17 +146,17 @@ describe("Contextual Derivation & Signing", () => { describe("Soft Derivations", () => { it("\(OK) Derive m'/44'/283'/0'/0/0 Algorand Address Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 0) - expect(key).toEqual(new Uint8Array(Buffer.from("7915e7ecbaad1dc9bc22a9e496686687f1a8cb4895b7ca46f86d64dd56c6cd97", "hex"))) + expect(key).toEqual(new Uint8Array(Buffer.from("62fe832b7ad10544be8337a670435e5064ae4a66e77bd78909765b46b576a6f3", "hex"))) }) it("\(OK) Derive m'/44'/283'/0'/0/1 Algorand Address Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 1) - expect(key).toEqual(new Uint8Array(Buffer.from("054a6881d8809c348a402d67ba2feedcd8e3145f40f21a6bbd0de09c30c78d0a", "hex"))) + expect(key).toEqual(new Uint8Array(Buffer.from("530461002eaccec0c7b5795925aa104a7fb45f85ef0aa95bbb5be93b6f8537ad", "hex"))) }) it("\(OK) Derive m'/44'/283'/0'/0/2 Algorand Address Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 2) - expect(key).toEqual(new Uint8Array(Buffer.from("8cea8052cfa1fd8cec0b4fad6241a91f2edbfe9f072586f243839174e40a25ef", "hex"))) + expect(key).toEqual(new Uint8Array(Buffer.from("2281c81bee04ee039fa482c283541c6ab06c8324db6f1cc59c68252e1d58bcb3", "hex"))) }) }) @@ -164,17 +164,17 @@ describe("Contextual Derivation & Signing", () => { describe("Hard Derivations", () => { it("\(OK) Derive m'/44'/283'/1'/0/0 Algorand Address Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 1, 0) - expect(key).toEqual(new Uint8Array(Buffer.from("04f3ba279aa781ab4f8f79aaf6cf91e3d7ff75429064dc757001a30e10c628df", "hex"))) + expect(key).toEqual(new Uint8Array(Buffer.from("9e12643f6c0068dcf53b04daced6f8c1a90ad21c954a66df4140d79303166a67", "hex"))) }) it("\(OK) Derive m'/44'/283'/2'/0/1 Algorand Address Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 2, 1) - expect(key).toEqual(new Uint8Array(Buffer.from("400d78302258dc7b3cb56d1a09f85b018e8100865ced0b5cda474c26bbc07c30", "hex"))) + expect(key).toEqual(new Uint8Array(Buffer.from("8a5ddf62d51a2c50e51dbad4634356cc72314a81edd917ac91da96477a9fb5b0", "hex"))) }) it("\(OK) Derive m'/44'/283'/3'/0/0 Algorand Address Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 3, 0) - expect(key).toEqual(new Uint8Array(Buffer.from("cf8d28a3d41bc656acbfeadb64d06054142c97bee6a987c11d934f84853df866", "hex"))) + expect(key).toEqual(new Uint8Array(Buffer.from("2358e0f2b465ab3e8f55139d8316654d4be39ebb22367d36409fd02a20b0e017", "hex"))) }) }) }) @@ -183,29 +183,29 @@ describe("Contextual Derivation & Signing", () => { describe("Soft Derivations", () => { it("\(OK) Derive m'/44'/0'/0'/0/0 Identity Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Identity, 0, 0) - expect(key).toEqual(new Uint8Array(Buffer.from("28804f08d8c145e172c998fe75058237b8181846ca763894ae3eefea6ab88352", "hex"))) + expect(key).toEqual(new Uint8Array(Buffer.from("b6d7eea5af0ad83edf4340659e72f0ea2b4566de1fc3b63a40a425aabebe5e49", "hex"))) }) it("\(OK) Derive m'/44'/0'/0'/0/1 Identity Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Identity, 0, 1) - expect(key).toEqual(new Uint8Array(Buffer.from("fc8d1c79edd406fa415cb0a76435eb83b6f8af72cd9bd673753471470205057a", "hex"))) + expect(key).toEqual(new Uint8Array(Buffer.from("b5cec676c5a2129ed1be4223a2702439bbb2462fd77b43f27e2f79fd194a30a2", "hex"))) }) it("\(OK) Derive m'/44'/0'/0'/0/2 Identity Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Identity, 0, 2) - expect(key).toEqual(new Uint8Array(Buffer.from("f7e317899420454886fe79f24e25af0bbb1856c440b14829674015e5fc2ad28a", "hex"))) + expect(key).toEqual(new Uint8Array(Buffer.from("435e5e3446431d462572abee1b8badb88608906a6af27b8497bccfd503edb6fe", "hex"))) }) }) describe("Hard Derivations", () => { it("\(OK) Derive m'/44'/0'/1'/0/0 Identity Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Identity, 1, 0) - expect(key).toEqual(new Uint8Array(Buffer.from("dddea98ec1fd24f9045ef0cade9055116079c87d16e51a0a081e94a8fb905435", "hex"))) + expect(key).toEqual(new Uint8Array(Buffer.from("bf63be83fff9bc9d0aebc231d50342110e5220247e50de376b47e154b5d32a3e", "hex"))) }) it("\(OK) Derive m'/44'/0'/2'/0/1 Identity Key", async () => { - const key: Uint8Array = await cryptoService.keyGen(KeyContext.Identity, 2, 1) - expect(key).toEqual(new Uint8Array(Buffer.from("2623ec5aad0ed598fcba654d9abd24c0c3f4804d5ac5f486e618fa1c66da139a", "hex"))) + const key: Uint8Array = await cryptoService.keyGen(KeyContext.Identity, 2, 1) + expect(key).toEqual(new Uint8Array(Buffer.from("edb10fff24a4745df52f1a0ab1ae71b3752d019c8c2437d46ab8c8e634a74cd4", "hex"))) }) }) }) From 62b841e3415fcbcf9ef998901525f566015cb7a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane?= Date: Thu, 11 Jan 2024 10:16:39 +0100 Subject: [PATCH 21/47] Fix Html link to allow access from generated website --- ARCs/arc-0052.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ARCs/arc-0052.md b/ARCs/arc-0052.md index 1127601c5..a91376d06 100644 --- a/ARCs/arc-0052.md +++ b/ARCs/arc-0052.md @@ -109,7 +109,7 @@ The function should check if the data, before and after decoding, isn't a regula - `keyIndex`: The key index to be used for key derivation. - `metadata`: Object to describe the type of data that is being signed. It should specify the encoding used (i.e msgpack, cbor, etc) and a schema so that the wallet can decode it to show the user. - `encoding`: The encoding used for the data - - `schema`: The schema of the data in the form of a JSON schema. The schemas are an powerful tool for wallet providers to be able to enforce the type and format of the data that is being signed. You can check schema examples [here](../assets/arc-0052/schemas/) + - `schema`: The schema of the data in the form of a JSON schema. The schemas are an powerful tool for wallet providers to be able to enforce the type and format of the data that is being signed. You can check schema examples here. - **returns**: Byte array containing the signature. The signature is encoded as a 64-byte array (32 + 32 bytes). It holds a compressed point R + the integer s (confirming that the signer knows the msg and the privKey). - Throws error if data doesn't match the schema @@ -199,7 +199,7 @@ The current wallet APIs available only support key derivation for addresses and ## Reference Implementation -Reference Implementation is included in the `assets` folder [here](../assets/arc-0052/README.md) +Reference Implementation is included in the `assets` folder here. ## Security Considerations From 660e82fee0976205e50c5a957e7d23802afca60e Mon Sep 17 00:00:00 2001 From: ehanoc Date: Wed, 24 Jan 2024 10:03:35 +0000 Subject: [PATCH 22/47] Updating ECDH tests so that bob has different rootKey than Alice Signed-off-by: ehanoc --- assets/arc-0052/contextual.api.crypto.spec.ts | 128 ++++++++++-------- 1 file changed, 68 insertions(+), 60 deletions(-) diff --git a/assets/arc-0052/contextual.api.crypto.spec.ts b/assets/arc-0052/contextual.api.crypto.spec.ts index 688dd98aa..0c8231859 100644 --- a/assets/arc-0052/contextual.api.crypto.spec.ts +++ b/assets/arc-0052/contextual.api.crypto.spec.ts @@ -348,68 +348,76 @@ describe("Contextual Derivation & Signing", () => { }) + describe("ECDH cases", () => { + // Making sure Alice & Bob Have different root keys + let bobCryptoService: ContextualCryptoApi + beforeEach(() => { + let seed: Buffer = Buffer.from(crypto.getRandomValues(new Uint8Array(32))) + bobCryptoService = new ContextualCryptoApi(seed) + }) - it("\(OK) ECDH", async () => { - const aliceKey: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 0) - const bobKey: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 1) - - const aliceSharedSecret: Uint8Array = await cryptoService.ECDH(KeyContext.Address, 0, 0, bobKey) - const bobSharedSecret: Uint8Array = await cryptoService.ECDH(KeyContext.Address, 0, 1, aliceKey) - - expect(aliceSharedSecret).toEqual(bobSharedSecret) - }) - - it("\(OK) ECDH, Encrypt and Decrypt", async () => { - const aliceKey: Uint8Array = await cryptoService.keyGen(KeyContext.Identity, 0, 0) - const bobKey: Uint8Array = await cryptoService.keyGen(KeyContext.Identity, 0, 1) - - const aliceSharedSecret: Uint8Array = await cryptoService.ECDH(KeyContext.Identity, 0, 0, bobKey) - const bobSharedSecret: Uint8Array = await cryptoService.ECDH(KeyContext.Identity, 0, 1, aliceKey) - - expect(aliceSharedSecret).toEqual(bobSharedSecret) - - const message: Uint8Array = new Uint8Array(Buffer.from("Hello World")) - const nonce: Uint8Array = randomBytes(crypto_secretbox_NONCEBYTES) - - // encrypt - const cipherText: Uint8Array = crypto_secretbox_easy(message, nonce, aliceSharedSecret) - - // decrypt - const plainText: Uint8Array = crypto_secretbox_open_easy(cipherText, nonce, bobSharedSecret) - expect(plainText).toEqual(message) - }) - - it("Libsodium example ECDH", async () => { - await ready - // keypair - const alice: KeyPair = crypto_sign_keypair() - - const alicePvtKey: Uint8Array = alice.privateKey - const alicePubKey: Uint8Array = alice.publicKey - - const aliceXPvt: Uint8Array = crypto_sign_ed25519_sk_to_curve25519(alicePvtKey) - const aliceXPub: Uint8Array = crypto_sign_ed25519_pk_to_curve25519(alicePubKey) + it("\(OK) ECDH", async () => { + const aliceKey: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 0) + const bobKey: Uint8Array = await bobCryptoService.keyGen(KeyContext.Address, 0, 1) - // bob - const bob: KeyPair = crypto_sign_keypair() - - const bobPvtKey: Uint8Array = bob.privateKey - const bobPubKey: Uint8Array = bob.publicKey - - const bobXPvt: Uint8Array = crypto_sign_ed25519_sk_to_curve25519(bobPvtKey) - const bobXPub: Uint8Array = crypto_sign_ed25519_pk_to_curve25519(bobPubKey) - - // shared secret - const aliceSecret: Uint8Array = crypto_scalarmult(aliceXPvt, bobXPub) - const bobSecret: Uint8Array = crypto_scalarmult(bobXPvt, aliceXPub) - expect(aliceSecret).toEqual(bobSecret) - - const aliceSharedSecret: CryptoKX = crypto_kx_client_session_keys(aliceXPub, aliceXPvt, bobXPub) - const bobSharedSecret: CryptoKX = crypto_kx_server_session_keys(bobXPub, bobXPvt, aliceXPub) - - // bilateral encryption channels - expect(aliceSharedSecret.sharedRx).toEqual(bobSharedSecret.sharedTx) - expect(bobSharedSecret.sharedTx).toEqual(aliceSharedSecret.sharedRx) + const aliceSharedSecret: Uint8Array = await cryptoService.ECDH(KeyContext.Address, 0, 0, bobKey) + const bobSharedSecret: Uint8Array = await bobCryptoService.ECDH(KeyContext.Address, 0, 1, aliceKey) + + expect(aliceSharedSecret).toEqual(bobSharedSecret) + }) + + it("\(OK) ECDH, Encrypt and Decrypt", async () => { + const aliceKey: Uint8Array = await cryptoService.keyGen(KeyContext.Identity, 0, 0) + const bobKey: Uint8Array = await bobCryptoService.keyGen(KeyContext.Identity, 0, 1) + + const aliceSharedSecret: Uint8Array = await cryptoService.ECDH(KeyContext.Identity, 0, 0, bobKey) + const bobSharedSecret: Uint8Array = await bobCryptoService.ECDH(KeyContext.Identity, 0, 1, aliceKey) + + expect(aliceSharedSecret).toEqual(bobSharedSecret) + + const message: Uint8Array = new Uint8Array(Buffer.from("Hello World")) + const nonce: Uint8Array = randomBytes(crypto_secretbox_NONCEBYTES) + + // encrypt + const cipherText: Uint8Array = crypto_secretbox_easy(message, nonce, aliceSharedSecret) + + // decrypt + const plainText: Uint8Array = crypto_secretbox_open_easy(cipherText, nonce, bobSharedSecret) + expect(plainText).toEqual(message) + }) + + it("Libsodium example ECDH", async () => { + await ready + // keypair + const alice: KeyPair = crypto_sign_keypair() + + const alicePvtKey: Uint8Array = alice.privateKey + const alicePubKey: Uint8Array = alice.publicKey + + const aliceXPvt: Uint8Array = crypto_sign_ed25519_sk_to_curve25519(alicePvtKey) + const aliceXPub: Uint8Array = crypto_sign_ed25519_pk_to_curve25519(alicePubKey) + + // bob + const bob: KeyPair = crypto_sign_keypair() + + const bobPvtKey: Uint8Array = bob.privateKey + const bobPubKey: Uint8Array = bob.publicKey + + const bobXPvt: Uint8Array = crypto_sign_ed25519_sk_to_curve25519(bobPvtKey) + const bobXPub: Uint8Array = crypto_sign_ed25519_pk_to_curve25519(bobPubKey) + + // shared secret + const aliceSecret: Uint8Array = crypto_scalarmult(aliceXPvt, bobXPub) + const bobSecret: Uint8Array = crypto_scalarmult(bobXPvt, aliceXPub) + expect(aliceSecret).toEqual(bobSecret) + + const aliceSharedSecret: CryptoKX = crypto_kx_client_session_keys(aliceXPub, aliceXPvt, bobXPub) + const bobSharedSecret: CryptoKX = crypto_kx_server_session_keys(bobXPub, bobXPvt, aliceXPub) + + // bilateral encryption channels + expect(aliceSharedSecret.sharedRx).toEqual(bobSharedSecret.sharedTx) + expect(bobSharedSecret.sharedTx).toEqual(aliceSharedSecret.sharedRx) + }) }) }) }) From 4efe10571af1658b2d92ec1ff15e5278a6460370 Mon Sep 17 00:00:00 2001 From: ehanoc Date: Tue, 27 Feb 2024 11:07:47 +0000 Subject: [PATCH 23/47] Fix: slicing scalar too soon before signing Signed-off-by: ehanoc --- assets/arc-0052/bip32-ed25519.ts | 13 +++++++++---- assets/arc-0052/contextual.api.crypto.ts | 12 +++++++++--- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/assets/arc-0052/bip32-ed25519.ts b/assets/arc-0052/bip32-ed25519.ts index 62c8997ca..d6d471df3 100644 --- a/assets/arc-0052/bip32-ed25519.ts +++ b/assets/arc-0052/bip32-ed25519.ts @@ -75,12 +75,17 @@ export function deriveChildNodePrivate( const left = new BN(kl, 16, "le").add(new BN(zl.subarray(0, 28), 16, "le").mul(new BN(8))).toArrayLike(Buffer, "le", 32); let right = new BN(kr, 16, "le").add(new BN(zr, 16, "le")).toArrayLike(Buffer, "le").slice(0, 32); + + const rightBuffer = Buffer.alloc(32); + Buffer.from(right).copy(rightBuffer, 0, 0, right.length) + + // just padding - if (right.length !== 32) { - right = Buffer.from(right.toString("hex") + "00", "hex"); - } + // if (right.length !== 32) { + // right = Buffer.from(right.toString("hex") + "00", "hex"); + // } - return Buffer.concat([left, right, chainCode]); + return Buffer.concat([left, rightBuffer, chainCode]); } /** diff --git a/assets/arc-0052/contextual.api.crypto.ts b/assets/arc-0052/contextual.api.crypto.ts index 4c128cab9..d472df0b7 100644 --- a/assets/arc-0052/contextual.api.crypto.ts +++ b/assets/arc-0052/contextual.api.crypto.ts @@ -7,11 +7,17 @@ import { crypto_sign_verify_detached, ready, crypto_sign_ed25519_pk_to_curve25519, - crypto_scalarmult + crypto_scalarmult, + crypto_sign_detached, + crypto_sign, + crypto_sign_SECRETKEYBYTES, + crypto_sign_ed25519_sk_to_pk, + crypto_scalarmult_ed25519_base } from 'libsodium-wrappers-sumo'; import * as msgpack from "algo-msgpack-with-bigint" import Ajv from "ajv" import { deriveChildNodePrivate, fromSeed } from './bip32-ed25519'; +import { randomBytes } from 'crypto'; /** @@ -94,8 +100,8 @@ export class ContextualCryptoApi { derived = deriveChildNodePrivate(derived, bip44Path[4]) - const scalar = derived.subarray(0, 32) // scalar == pvtKey - return isPrivate ? scalar : crypto_scalarmult_ed25519_base_noclamp(scalar) + // const scalar = derived.subarray(0, 32) // scalar == pvtKey + return isPrivate ? derived : crypto_scalarmult_ed25519_base_noclamp(derived.subarray(0, 32)) } /** From f36cf8b3cd7816afa168e3aa48f49a3a8a879595 Mon Sep 17 00:00:00 2001 From: ehanoc Date: Tue, 27 Feb 2024 11:09:36 +0000 Subject: [PATCH 24/47] chore: specify types Signed-off-by: ehanoc --- assets/arc-0052/contextual.api.crypto.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/arc-0052/contextual.api.crypto.ts b/assets/arc-0052/contextual.api.crypto.ts index d472df0b7..3da50dd20 100644 --- a/assets/arc-0052/contextual.api.crypto.ts +++ b/assets/arc-0052/contextual.api.crypto.ts @@ -152,8 +152,8 @@ export class ContextualCryptoApi { const bip44Path: number[] = GetBIP44PathFromContext(context, account, keyIndex) const raw: Uint8Array = await this.deriveKey(rootKey, bip44Path, true) - const scalar = raw.slice(0, 32); - const c = raw.slice(32, 64); + const scalar: Uint8Array = raw.slice(0, 32); + const c: Uint8Array = raw.slice(32, 64); // \(1): pubKey = scalar * G (base point, no clamp) const publicKey = crypto_scalarmult_ed25519_base_noclamp(scalar); From 8e26a6fe7dea1af23b5764a4c231f0cf1218d6f6 Mon Sep 17 00:00:00 2001 From: ehanoc Date: Tue, 27 Feb 2024 12:11:53 +0000 Subject: [PATCH 25/47] fix: ECDH using common Curve25519 point and concatenation into hash for the secret Signed-off-by: ehanoc --- assets/arc-0052/contextual.api.crypto.spec.ts | 10 ++++----- assets/arc-0052/contextual.api.crypto.ts | 21 ++++++++++++++++--- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/assets/arc-0052/contextual.api.crypto.spec.ts b/assets/arc-0052/contextual.api.crypto.spec.ts index 0c8231859..630359954 100644 --- a/assets/arc-0052/contextual.api.crypto.spec.ts +++ b/assets/arc-0052/contextual.api.crypto.spec.ts @@ -359,9 +359,9 @@ describe("Contextual Derivation & Signing", () => { it("\(OK) ECDH", async () => { const aliceKey: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 0) const bobKey: Uint8Array = await bobCryptoService.keyGen(KeyContext.Address, 0, 1) - - const aliceSharedSecret: Uint8Array = await cryptoService.ECDH(KeyContext.Address, 0, 0, bobKey) - const bobSharedSecret: Uint8Array = await bobCryptoService.ECDH(KeyContext.Address, 0, 1, aliceKey) + + const aliceSharedSecret: Uint8Array = await cryptoService.ECDH(KeyContext.Address, 0, 0, bobKey, true) + const bobSharedSecret: Uint8Array = await bobCryptoService.ECDH(KeyContext.Address, 0, 1, aliceKey, false) expect(aliceSharedSecret).toEqual(bobSharedSecret) }) @@ -370,8 +370,8 @@ describe("Contextual Derivation & Signing", () => { const aliceKey: Uint8Array = await cryptoService.keyGen(KeyContext.Identity, 0, 0) const bobKey: Uint8Array = await bobCryptoService.keyGen(KeyContext.Identity, 0, 1) - const aliceSharedSecret: Uint8Array = await cryptoService.ECDH(KeyContext.Identity, 0, 0, bobKey) - const bobSharedSecret: Uint8Array = await bobCryptoService.ECDH(KeyContext.Identity, 0, 1, aliceKey) + const aliceSharedSecret: Uint8Array = await cryptoService.ECDH(KeyContext.Identity, 0, 0, bobKey, true) + const bobSharedSecret: Uint8Array = await bobCryptoService.ECDH(KeyContext.Identity, 0, 1, aliceKey, false) expect(aliceSharedSecret).toEqual(bobSharedSecret) diff --git a/assets/arc-0052/contextual.api.crypto.ts b/assets/arc-0052/contextual.api.crypto.ts index 3da50dd20..fd4894d82 100644 --- a/assets/arc-0052/contextual.api.crypto.ts +++ b/assets/arc-0052/contextual.api.crypto.ts @@ -12,7 +12,9 @@ import { crypto_sign, crypto_sign_SECRETKEYBYTES, crypto_sign_ed25519_sk_to_pk, - crypto_scalarmult_ed25519_base + crypto_scalarmult_ed25519_base, + crypto_generichash, + crypto_scalarmult_base } from 'libsodium-wrappers-sumo'; import * as msgpack from "algo-msgpack-with-bigint" import Ajv from "ajv" @@ -283,7 +285,7 @@ export class ContextualCryptoApi { * @param otherPartyPub - raw 32 bytes public key of the other party * @returns - raw 32 bytes shared secret */ - async ECDH(context: KeyContext, account: number, keyIndex: number, otherPartyPub: Uint8Array): Promise { + async ECDH(context: KeyContext, account: number, keyIndex: number, otherPartyPub: Uint8Array, meFirst: boolean): Promise { await ready const rootKey: Uint8Array = fromSeed(this.seed) @@ -293,6 +295,19 @@ export class ContextualCryptoApi { const scalar: Uint8Array = childKey.slice(0, 32) - return crypto_scalarmult(scalar, crypto_sign_ed25519_pk_to_curve25519(otherPartyPub)) + // our public key is derived from the private key + const ourPub: Uint8Array = crypto_scalarmult_ed25519_base_noclamp(scalar) + + // find common point + const sharedPoint: Uint8Array = crypto_scalarmult(scalar, crypto_sign_ed25519_pk_to_curve25519(otherPartyPub)) + + let firstKey: Uint8Array = crypto_sign_ed25519_pk_to_curve25519(ourPub) + let secondKey: Uint8Array = crypto_sign_ed25519_pk_to_curve25519(otherPartyPub) + if (!meFirst) { // swap + firstKey = crypto_sign_ed25519_pk_to_curve25519(otherPartyPub) + secondKey = crypto_sign_ed25519_pk_to_curve25519(ourPub) + } + + return crypto_generichash(32, Buffer.concat([sharedPoint, firstKey, secondKey])) } } \ No newline at end of file From 0192f5c2e859d4df25f5220adc46fba61d64e069 Mon Sep 17 00:00:00 2001 From: ehanoc Date: Tue, 27 Feb 2024 12:16:01 +0000 Subject: [PATCH 26/47] chore: comment in ECDH function Signed-off-by: ehanoc --- assets/arc-0052/contextual.api.crypto.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/arc-0052/contextual.api.crypto.ts b/assets/arc-0052/contextual.api.crypto.ts index fd4894d82..2b59c64d5 100644 --- a/assets/arc-0052/contextual.api.crypto.ts +++ b/assets/arc-0052/contextual.api.crypto.ts @@ -283,6 +283,7 @@ export class ContextualCryptoApi { * @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 - defines the order in which the keys will be considered for the shared secret. If true, our key will be used first, otherwise the other party's key will be used first * @returns - raw 32 bytes shared secret */ async ECDH(context: KeyContext, account: number, keyIndex: number, otherPartyPub: Uint8Array, meFirst: boolean): Promise { From d44a8e9ecb62152d419f1b4ea50d72baba6b5ba3 Mon Sep 17 00:00:00 2001 From: HashMapsData2Value <83883690+HashMapsData2Value@users.noreply.github.com> Date: Tue, 27 Feb 2024 20:45:50 +0100 Subject: [PATCH 27/47] feat: simplify signData --- assets/arc-0052/contextual.api.crypto.ts | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/assets/arc-0052/contextual.api.crypto.ts b/assets/arc-0052/contextual.api.crypto.ts index 2b59c64d5..726c72c49 100644 --- a/assets/arc-0052/contextual.api.crypto.ts +++ b/assets/arc-0052/contextual.api.crypto.ts @@ -159,27 +159,15 @@ export class ContextualCryptoApi { // \(1): pubKey = scalar * G (base point, no clamp) const publicKey = crypto_scalarmult_ed25519_base_noclamp(scalar); - - // \(2): h = hash(c + msg) mod q - const hash: bigint = Buffer.from(crypto_hash_sha512(Buffer.concat([c, data]))).readBigInt64LE() - // \(3): r = hash(hash(privKey) + msg) mod q - const q: bigint = BigInt(2n ** 252n + 27742317777372353535851937790883648493n); - const rBigInt = hash % q - const rBString = rBigInt.toString(16) // convert to hex string - - // fill 32 bytes of r - // convert to Uint8Array - const r = new Uint8Array(32) - for (let i = 0; i < rBString.length; i += 2) { - r[i / 2] = parseInt(rBString.substring(i, i + 2), 16); - } + // \(2): h = hash(c || msg) mod q + const r = crypto_core_ed25519_scalar_reduce(crypto_hash_sha512(Buffer.concat([c, data]))) // \(4): R = r * G (base point, no clamp) const R = crypto_scalarmult_ed25519_base_noclamp(r) - let h = crypto_hash_sha512(Buffer.concat([R, publicKey, data])); - h = crypto_core_ed25519_scalar_reduce(h); + // h = hash(R || pubKey || msg) mod q + let h = crypto_core_ed25519_scalar_reduce(crypto_hash_sha512(Buffer.concat([R, publicKey, data]))); // \(5): S = (r + h * k) mod q const S = crypto_core_ed25519_scalar_add(r, crypto_core_ed25519_scalar_mul(h, scalar)) From 5766c5f0169be8632f180afa2e34ad5ede088184 Mon Sep 17 00:00:00 2001 From: HashMapsData2Value <83883690+HashMapsData2Value@users.noreply.github.com> Date: Mon, 4 Mar 2024 23:07:48 +0100 Subject: [PATCH 28/47] chore: clarifies tests, small refactor of ecdh --- assets/arc-0052/contextual.api.crypto.spec.ts | 54 +++++++++++++------ assets/arc-0052/contextual.api.crypto.ts | 20 ++++--- assets/arc-0052/schemas/msg.schema.json | 14 +++++ 3 files changed, 65 insertions(+), 23 deletions(-) create mode 100644 assets/arc-0052/schemas/msg.schema.json diff --git a/assets/arc-0052/contextual.api.crypto.spec.ts b/assets/arc-0052/contextual.api.crypto.spec.ts index 630359954..8c250f57c 100644 --- a/assets/arc-0052/contextual.api.crypto.spec.ts +++ b/assets/arc-0052/contextual.api.crypto.spec.ts @@ -211,6 +211,24 @@ describe("Contextual Derivation & Signing", () => { }) describe("Signing Typed Data", () => { + + // it("\(OK) Sign hardcoded data, compare hardcoded signature", async () => { + // const message: Uint8Array = new Uint8Array(Buffer.from("{'text': 'Hello, World'}")) + + // // read auth schema file for authentication. 32 bytes challenge to sign + // const msgSchema: JSONSchemaType = JSON.parse(readFileSync(path.resolve(__dirname, "schemas/msg.schema.json"), "utf8")) + // const metadata: SignMetadata = { encoding: Encoding.NONE, schema: msgSchema } + + // const signature: Uint8Array = await cryptoService.signData(KeyContext.Address, 0, 0, message, metadata) + // expect(signature).toHaveLength(64) + // console.log(signature) + + // const expectedSignature = Uint8Array.from([137,13,247,162,115,48,233,188,188,81,7,167,158,250,252,66,138,30,3,65,88,209,92,250,43,13,60,193,44,175,87,93,60,73,243,145,170,38,214,152,29,54,61,109,241,24,238,186,159,45,149,15,141,69,118,162,31,148,162,221,29,156,226,1]) + // console.log("expected signature:", expectedSignature) + // const isValid: boolean = await cryptoService.verifyWithPublicKey(signature, message, await cryptoService.keyGen(KeyContext.Address, 0, 0)) + // expect(isValid).toBe(true) + // }) + it("\(OK) Sign authentication challenge of 32 bytes, encoded base 64", async () => { const challenge: Uint8Array = new Uint8Array(randomBytes(32)) @@ -350,37 +368,43 @@ describe("Contextual Derivation & Signing", () => { describe("ECDH cases", () => { // Making sure Alice & Bob Have different root keys + let aliceCryptoService: ContextualCryptoApi let bobCryptoService: ContextualCryptoApi beforeEach(() => { - let seed: Buffer = Buffer.from(crypto.getRandomValues(new Uint8Array(32))) - bobCryptoService = new ContextualCryptoApi(seed) + aliceCryptoService = new ContextualCryptoApi(bip39.mnemonicToSeedSync("exact remain north lesson program series excess lava material second riot error boss planet brick rotate scrap army riot banner adult fashion casino bamboo", "")) + bobCryptoService = new ContextualCryptoApi(bip39.mnemonicToSeedSync("identify length ranch make silver fog much puzzle borrow relax occur drum blue oval book pledge reunion coral grace lamp recall fever route carbon", "")) }) it("\(OK) ECDH", async () => { - const aliceKey: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 0) - const bobKey: Uint8Array = await bobCryptoService.keyGen(KeyContext.Address, 0, 1) + const aliceKey: Uint8Array = await aliceCryptoService.keyGen(KeyContext.Identity, 0, 0) + const bobKey: Uint8Array = await bobCryptoService.keyGen(KeyContext.Identity, 0, 0) - const aliceSharedSecret: Uint8Array = await cryptoService.ECDH(KeyContext.Address, 0, 0, bobKey, true) - const bobSharedSecret: Uint8Array = await bobCryptoService.ECDH(KeyContext.Address, 0, 1, aliceKey, false) - + const aliceSharedSecret: Uint8Array = await aliceCryptoService.ECDH(KeyContext.Identity, 0, 0, bobKey, true) + const bobSharedSecret: Uint8Array = await bobCryptoService.ECDH(KeyContext.Identity, 0, 0, aliceKey, false) expect(aliceSharedSecret).toEqual(bobSharedSecret) + + const aliceSharedSecret2: Uint8Array = await aliceCryptoService.ECDH(KeyContext.Identity, 0, 0, bobKey, false) + const bobSharedSecret2: Uint8Array = await bobCryptoService.ECDH(KeyContext.Identity, 0, 0, aliceKey, true) + expect(aliceSharedSecret2).toEqual(bobSharedSecret2) + expect(aliceSharedSecret2).not.toEqual(aliceSharedSecret) + }) it("\(OK) ECDH, Encrypt and Decrypt", async () => { - const aliceKey: Uint8Array = await cryptoService.keyGen(KeyContext.Identity, 0, 0) - const bobKey: Uint8Array = await bobCryptoService.keyGen(KeyContext.Identity, 0, 1) - - const aliceSharedSecret: Uint8Array = await cryptoService.ECDH(KeyContext.Identity, 0, 0, bobKey, true) - const bobSharedSecret: Uint8Array = await bobCryptoService.ECDH(KeyContext.Identity, 0, 1, aliceKey, false) + const aliceKey: Uint8Array = await aliceCryptoService.keyGen(KeyContext.Identity, 0, 0) + const bobKey: Uint8Array = await bobCryptoService.keyGen(KeyContext.Identity, 0, 0) + const aliceSharedSecret: Uint8Array = await aliceCryptoService.ECDH(KeyContext.Identity, 0, 0, bobKey, true) + const bobSharedSecret: Uint8Array = await bobCryptoService.ECDH(KeyContext.Identity, 0, 0, aliceKey, false) + expect(aliceSharedSecret).toEqual(bobSharedSecret) - const message: Uint8Array = new Uint8Array(Buffer.from("Hello World")) - const nonce: Uint8Array = randomBytes(crypto_secretbox_NONCEBYTES) + const message: Uint8Array = new Uint8Array(Buffer.from("Hello, World!")) + const nonce: Uint8Array = new Uint8Array([16,197,142,8,174,91,118,244,202,136,43,200,97,242,104,99,42,154,191,32,67,30,6,123]) // encrypt const cipherText: Uint8Array = crypto_secretbox_easy(message, nonce, aliceSharedSecret) - + expect(cipherText).toEqual(new Uint8Array([251,7,48,58,57,22,135,152,150,116,242,138,26,155,136,252,163,209,7,34,125,135,218,222,102,45,250,55,34])) // decrypt const plainText: Uint8Array = crypto_secretbox_open_easy(cipherText, nonce, bobSharedSecret) expect(plainText).toEqual(message) diff --git a/assets/arc-0052/contextual.api.crypto.ts b/assets/arc-0052/contextual.api.crypto.ts index 726c72c49..eb694177d 100644 --- a/assets/arc-0052/contextual.api.crypto.ts +++ b/assets/arc-0052/contextual.api.crypto.ts @@ -286,17 +286,21 @@ export class ContextualCryptoApi { // our public key is derived from the private key const ourPub: Uint8Array = crypto_scalarmult_ed25519_base_noclamp(scalar) + + // convert from ed25519 to curve25519 + const ourPubCurve25519: Uint8Array = crypto_sign_ed25519_pk_to_curve25519(ourPub) + const otherPartyPubCurve25519: Uint8Array = crypto_sign_ed25519_pk_to_curve25519(otherPartyPub) // find common point - const sharedPoint: Uint8Array = crypto_scalarmult(scalar, crypto_sign_ed25519_pk_to_curve25519(otherPartyPub)) + const sharedPoint: Uint8Array = crypto_scalarmult(scalar, otherPartyPubCurve25519) + + let concatenation: Uint8Array + if (meFirst) { + concatenation = Buffer.concat([sharedPoint, ourPubCurve25519, otherPartyPubCurve25519]) + } else { + concatenation = Buffer.concat([sharedPoint, otherPartyPubCurve25519, ourPubCurve25519]) - let firstKey: Uint8Array = crypto_sign_ed25519_pk_to_curve25519(ourPub) - let secondKey: Uint8Array = crypto_sign_ed25519_pk_to_curve25519(otherPartyPub) - if (!meFirst) { // swap - firstKey = crypto_sign_ed25519_pk_to_curve25519(otherPartyPub) - secondKey = crypto_sign_ed25519_pk_to_curve25519(ourPub) } - - return crypto_generichash(32, Buffer.concat([sharedPoint, firstKey, secondKey])) + return crypto_generichash(32, new Uint8Array(concatenation)) } } \ No newline at end of file diff --git a/assets/arc-0052/schemas/msg.schema.json b/assets/arc-0052/schemas/msg.schema.json new file mode 100644 index 000000000..6dab29f16 --- /dev/null +++ b/assets/arc-0052/schemas/msg.schema.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://arc52/schemas/msg.schema.json", + "title": "Arbitrary Message", + "type": "object", + "additionalProperties": false, + "properties": { + "text": { + "type": "string" + } + }, + "required": ["text"] + } + \ No newline at end of file From 3ab2169d7b8bcc7719b933f9bb98672c0f094c80 Mon Sep 17 00:00:00 2001 From: HashMapsData2Value <83883690+HashMapsData2Value@users.noreply.github.com> Date: Mon, 4 Mar 2024 23:08:58 +0100 Subject: [PATCH 29/47] fix: remove commented out test, for now --- assets/arc-0052/contextual.api.crypto.spec.ts | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/assets/arc-0052/contextual.api.crypto.spec.ts b/assets/arc-0052/contextual.api.crypto.spec.ts index 8c250f57c..ddbffe14c 100644 --- a/assets/arc-0052/contextual.api.crypto.spec.ts +++ b/assets/arc-0052/contextual.api.crypto.spec.ts @@ -211,24 +211,6 @@ describe("Contextual Derivation & Signing", () => { }) describe("Signing Typed Data", () => { - - // it("\(OK) Sign hardcoded data, compare hardcoded signature", async () => { - // const message: Uint8Array = new Uint8Array(Buffer.from("{'text': 'Hello, World'}")) - - // // read auth schema file for authentication. 32 bytes challenge to sign - // const msgSchema: JSONSchemaType = JSON.parse(readFileSync(path.resolve(__dirname, "schemas/msg.schema.json"), "utf8")) - // const metadata: SignMetadata = { encoding: Encoding.NONE, schema: msgSchema } - - // const signature: Uint8Array = await cryptoService.signData(KeyContext.Address, 0, 0, message, metadata) - // expect(signature).toHaveLength(64) - // console.log(signature) - - // const expectedSignature = Uint8Array.from([137,13,247,162,115,48,233,188,188,81,7,167,158,250,252,66,138,30,3,65,88,209,92,250,43,13,60,193,44,175,87,93,60,73,243,145,170,38,214,152,29,54,61,109,241,24,238,186,159,45,149,15,141,69,118,162,31,148,162,221,29,156,226,1]) - // console.log("expected signature:", expectedSignature) - // const isValid: boolean = await cryptoService.verifyWithPublicKey(signature, message, await cryptoService.keyGen(KeyContext.Address, 0, 0)) - // expect(isValid).toBe(true) - // }) - it("\(OK) Sign authentication challenge of 32 bytes, encoded base 64", async () => { const challenge: Uint8Array = new Uint8Array(randomBytes(32)) From f66b4369d0dc49b12fa2aafb3609ed5120a0d0c2 Mon Sep 17 00:00:00 2001 From: ehanoc Date: Wed, 13 Mar 2024 12:11:41 +0000 Subject: [PATCH 30/47] chore: Refactor, include tweetnacl to verify correctness Signed-off-by: ehanoc --- assets/arc-0052/contextual.api.crypto.spec.ts | 2 +- assets/arc-0052/contextual.api.crypto.ts | 15 +++++++-------- assets/arc-0052/package.json | 3 ++- assets/arc-0052/yarn.lock | 5 +++++ 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/assets/arc-0052/contextual.api.crypto.spec.ts b/assets/arc-0052/contextual.api.crypto.spec.ts index ddbffe14c..6ef546bbe 100644 --- a/assets/arc-0052/contextual.api.crypto.spec.ts +++ b/assets/arc-0052/contextual.api.crypto.spec.ts @@ -1,7 +1,7 @@ import { CryptoKX, KeyPair, crypto_kx_client_session_keys, crypto_kx_server_session_keys, crypto_scalarmult, crypto_scalarmult_ed25519_base_noclamp, crypto_secretbox_NONCEBYTES, crypto_secretbox_easy, crypto_secretbox_open_easy, crypto_sign_ed25519_pk_to_curve25519, crypto_sign_ed25519_sk_to_curve25519, crypto_sign_keypair, ready, to_base64 } from "libsodium-wrappers-sumo" import * as bip39 from "bip39" import { randomBytes } from "crypto" -import { ContextualCryptoApi, ERROR_BAD_DATA, ERROR_TAGS_FOUND, Encoding, KeyContext, SignMetadata, harden } from "./contextual.api.crypto" +import { ContextualCryptoApi, ERROR_TAGS_FOUND, Encoding, KeyContext, SignMetadata, harden } from "./contextual.api.crypto" import * as msgpack from "algo-msgpack-with-bigint" import { fromSeed } from "./bip32-ed25519" import { sha512_256 } from "js-sha512" diff --git a/assets/arc-0052/contextual.api.crypto.ts b/assets/arc-0052/contextual.api.crypto.ts index eb694177d..495f32588 100644 --- a/assets/arc-0052/contextual.api.crypto.ts +++ b/assets/arc-0052/contextual.api.crypto.ts @@ -8,19 +8,12 @@ import { ready, crypto_sign_ed25519_pk_to_curve25519, crypto_scalarmult, - crypto_sign_detached, - crypto_sign, - crypto_sign_SECRETKEYBYTES, - crypto_sign_ed25519_sk_to_pk, crypto_scalarmult_ed25519_base, crypto_generichash, - crypto_scalarmult_base } from 'libsodium-wrappers-sumo'; import * as msgpack from "algo-msgpack-with-bigint" import Ajv from "ajv" import { deriveChildNodePrivate, fromSeed } from './bip32-ed25519'; -import { randomBytes } from 'crypto'; - /** * @@ -98,7 +91,7 @@ export class ContextualCryptoApi { // // [Public][ChainCode] // const extPub: Uint8Array = new Uint8Array(Buffer.concat([nodePublic, nodeCC])) - // const publicKey: Uint8Array = deriveChildNodePublic(extPub, bip44Path[4]).subarray(0, 32) + // const publicKey: Uint8Array = deriveChildNodePublic(extPub, bip44Path[4]).subarray(0, 32)) derived = deriveChildNodePrivate(derived, bip44Path[4]) @@ -301,6 +294,12 @@ export class ContextualCryptoApi { concatenation = Buffer.concat([sharedPoint, otherPartyPubCurve25519, ourPubCurve25519]) } + + // check for adversarial public keys + // scalar was being clamped? + // dedicated keys for ECDH? + + return crypto_generichash(32, new Uint8Array(concatenation)) } } \ No newline at end of file diff --git a/assets/arc-0052/package.json b/assets/arc-0052/package.json index 21cf3dca6..55c5c8b25 100644 --- a/assets/arc-0052/package.json +++ b/assets/arc-0052/package.json @@ -28,7 +28,8 @@ "js-sha512": "^0.8.0", "libsodium-wrappers-sumo": "^0.7.13", "ts-custom-error": "^3.2.0", - "ts-log": "^2.2.4" + "ts-log": "^2.2.4", + "tweetnacl": "1.0.3" }, "jest": { "moduleFileExtensions": [ diff --git a/assets/arc-0052/yarn.lock b/assets/arc-0052/yarn.lock index 28a22b2d1..e736cc062 100644 --- a/assets/arc-0052/yarn.lock +++ b/assets/arc-0052/yarn.lock @@ -2233,6 +2233,11 @@ ts-log@^2.2.4: resolved "https://registry.yarnpkg.com/ts-log/-/ts-log-2.2.5.tgz#aef3252f1143d11047e2cb6f7cfaac7408d96623" integrity sha512-PGcnJoTBnVGy6yYNFxWVNkdcAuAMstvutN9MgDJIV6L0oG8fB+ZNNy1T+wJzah8RPGor1mZuPQkVfXNDpy9eHA== +tweetnacl@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596" + integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw== + type-detect@4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" From 950020c35eebd9d2a17440ca57329c12f4a03f46 Mon Sep 17 00:00:00 2001 From: Ori Shem-Tov Date: Mon, 25 Mar 2024 16:13:10 +0200 Subject: [PATCH 31/47] 1-1 feature parity with Kotlin --- assets/arc-0052/contextual.api.crypto.spec.ts | 111 ++++++++++------ assets/arc-0052/contextual.api.crypto.ts | 122 ++++++++++++------ assets/arc-0052/package.json | 2 + assets/arc-0052/yarn.lock | 65 ++++++++++ 4 files changed, 227 insertions(+), 73 deletions(-) diff --git a/assets/arc-0052/contextual.api.crypto.spec.ts b/assets/arc-0052/contextual.api.crypto.spec.ts index ddbffe14c..ef745648a 100644 --- a/assets/arc-0052/contextual.api.crypto.spec.ts +++ b/assets/arc-0052/contextual.api.crypto.spec.ts @@ -1,4 +1,4 @@ -import { CryptoKX, KeyPair, crypto_kx_client_session_keys, crypto_kx_server_session_keys, crypto_scalarmult, crypto_scalarmult_ed25519_base_noclamp, crypto_secretbox_NONCEBYTES, crypto_secretbox_easy, crypto_secretbox_open_easy, crypto_sign_ed25519_pk_to_curve25519, crypto_sign_ed25519_sk_to_curve25519, crypto_sign_keypair, ready, to_base64 } from "libsodium-wrappers-sumo" +import { CryptoKX, KeyPair, crypto_kx_client_session_keys, crypto_kx_server_session_keys, crypto_scalarmult, crypto_scalarmult_ed25519_base_noclamp, crypto_secretbox_easy, crypto_secretbox_open_easy, crypto_sign_ed25519_pk_to_curve25519, crypto_sign_ed25519_sk_to_curve25519, crypto_sign_keypair, ready, to_base64 } from "libsodium-wrappers-sumo" import * as bip39 from "bip39" import { randomBytes } from "crypto" import { ContextualCryptoApi, ERROR_BAD_DATA, ERROR_TAGS_FOUND, Encoding, KeyContext, SignMetadata, harden } from "./contextual.api.crypto" @@ -9,6 +9,8 @@ import base32 from "hi-base32" import { JSONSchemaType } from "ajv" import { readFileSync } from "fs" import path from "path" +import { decodeUnsignedTransaction } from "algosdk" +import nacl from "tweetnacl" const libBip32Ed25519 = require('bip32-ed25519') function encodeAddress(publicKey: Buffer): string { @@ -211,7 +213,7 @@ describe("Contextual Derivation & Signing", () => { }) describe("Signing Typed Data", () => { - it("\(OK) Sign authentication challenge of 32 bytes, encoded base 64", async () => { + it("\(OK) Sign authentication challenge of 32 bytes, encoded base64", async () => { const challenge: Uint8Array = new Uint8Array(randomBytes(32)) // read auth schema file for authentication. 32 bytes challenge to sign @@ -228,6 +230,21 @@ describe("Contextual Derivation & Signing", () => { expect(isValid).toBe(true) }) + it("\(OK) Sign authentication challenge of 32 bytes, encoded msgpack", async () => { + const challenge: Uint8Array = new Uint8Array(randomBytes(32)) + + // read auth schema file for authentication. 32 bytes challenge to sign + const authSchema: JSONSchemaType = JSON.parse(readFileSync(path.resolve(__dirname, "schemas/auth.request.json"), "utf8")) + const metadata: SignMetadata = { encoding: Encoding.MSGPACK, schema: authSchema } + const encoded: Uint8Array = msgpack.encode(challenge) + + const signature: Uint8Array = await cryptoService.signData(KeyContext.Address,0, 0, encoded, metadata) + expect(signature).toHaveLength(64) + + const isValid: boolean = await cryptoService.verifyWithPublicKey(signature, encoded, await cryptoService.keyGen(KeyContext.Address, 0, 0)) + expect(isValid).toBe(true) + }) + it ("\(OK) Sign authentication challenge of 32 bytes, no encoding", async () => { const challenge: Uint8Array = new Uint8Array(randomBytes(32)) @@ -242,7 +259,7 @@ describe("Contextual Derivation & Signing", () => { expect(isValid).toBe(true) }) - it("\(OK) Sign Arbitrary Message against Schem", async () => { + it("\(OK) Sign Arbitrary Message against Schema, encoded base64", async () => { const firstKey: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 0) const message = { @@ -270,7 +287,35 @@ describe("Contextual Derivation & Signing", () => { expect(isValid).toBe(true) }) - it("\(FAIL) Signing attempt fails because of invalid data against Schema", async () => { + it("\(OK) Sign Arbitrary Message against Schema, encoded msgpack", async () => { + const firstKey: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 0) + + const message = { + letter: "Hello World" + } + + const encoded: Buffer = Buffer.from(msgpack.encode(message)) + + // Schema of what we are signing + const jsonSchema = { + type: "object", + properties: { + letter: { + type: "string" + } + } + } + + const metadata: SignMetadata = { encoding: Encoding.MSGPACK, schema: jsonSchema } + + const signature: Uint8Array = await cryptoService.signData(KeyContext.Address,0, 0, encoded, metadata) + expect(signature).toHaveLength(64) + + const isValid: boolean = await cryptoService.verifyWithPublicKey(signature, encoded, firstKey) + expect(isValid).toBe(true) + }) + + it("\(FAIL) Signing attempt fails because of invalid data against Schema, encoded base64", async () => { const message = { letter: "Hello World" } @@ -286,41 +331,23 @@ describe("Contextual Derivation & Signing", () => { expect(cryptoService.signData(KeyContext.Identity,0, 0, encoded, metadata)).rejects.toThrowError() }) - describe("Reject Regular Transaction Signing. IF TAG Prexies are present signing must fail", () => { - describe("Reject tags present in the encoded payload", () => { - it("\(FAIL) [TX] Tag", async () => { - const transaction: Buffer = Buffer.concat([Buffer.from("TX"), msgpack.encode(randomBytes(64))]) - const metadata: SignMetadata = { encoding: Encoding.BASE64, schema: {} } - - const encodedTx: Uint8Array = new Uint8Array(Buffer.from(transaction.toString('base64'))) - expect(cryptoService.signData(KeyContext.Identity,0, 0, encodedTx, metadata)).rejects.toThrowError(ERROR_TAGS_FOUND) - }) + it("\(FAIL) Signing attempt fails because of invalid data against Schema, encoded msgpack", async () => { + const message = { + letter: "Hello World" + } - it("\(FAIL) [MX] Tag", async () => { - const transaction: Buffer = Buffer.concat([Buffer.from("MX"), msgpack.encode(randomBytes(64))]) - const metadata: SignMetadata = { encoding: Encoding.BASE64, schema: {} } - - const encodedTx: Buffer = Buffer.from(transaction.toString('base64')) - expect(cryptoService.signData(KeyContext.Identity,0, 0, encodedTx, metadata)).rejects.toThrowError(ERROR_TAGS_FOUND) - }) + const encoded: Buffer = Buffer.from(msgpack.encode(message)) - it("\(FAIL) [Program] Tag", async () => { - const transaction: Buffer = Buffer.concat([Buffer.from("Program"), msgpack.encode(randomBytes(64))]) - const metadata: SignMetadata = { encoding: Encoding.BASE64, schema: {} } - - const encodedTx: Buffer = Buffer.from(transaction.toString('base64')) - expect(cryptoService.signData(KeyContext.Identity,0, 0, encodedTx, metadata)).rejects.toThrowError(ERROR_TAGS_FOUND) - }) + // Schema of what we are signing + const jsonSchema = { + type: "string" + } - it("\(FAIL) [progData] Tag", async () => { - const transaction: Buffer = Buffer.concat([Buffer.from("progData"), msgpack.encode(randomBytes(64))]) - const metadata: SignMetadata = { encoding: Encoding.BASE64, schema: {} } - - const encodedTx: Buffer = Buffer.from(transaction.toString('base64')) - expect(cryptoService.signData(KeyContext.Identity,0, 0, encodedTx, metadata)).rejects.toThrowError(ERROR_TAGS_FOUND) - }) - }) - + const metadata: SignMetadata = { encoding: Encoding.MSGPACK, schema: jsonSchema } + expect(cryptoService.signData(KeyContext.Identity,0, 0, encoded, metadata)).rejects.toThrowError() + }) + + describe("Reject Regular Transaction Signing. IF TAG Prexies are present signing must fail", () => { it("\(FAIL) [TX] Tag", async () => { const transaction: Buffer = Buffer.concat([Buffer.from("TX"), msgpack.encode(randomBytes(64))]) const metadata: SignMetadata = { encoding: Encoding.MSGPACK, schema: {} } @@ -340,12 +367,22 @@ describe("Contextual Derivation & Signing", () => { }) it("\(FAIL) [progData] Tag", async () => { - const transaction: Buffer = Buffer.concat([Buffer.from("progData"), msgpack.encode(randomBytes(64))]) + const transaction: Buffer = Buffer.concat([Buffer.from("ProgData"), msgpack.encode(randomBytes(64))]) const metadata: SignMetadata = { encoding: Encoding.MSGPACK, schema: {} } expect(cryptoService.signData(KeyContext.Identity,0, 0, transaction, metadata)).rejects.toThrowError(ERROR_TAGS_FOUND) }) }) + }) + describe("signing transactions", () => { + it("\(OK) Sign Transaction", async () => { + const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 0) + // this transaction wes successfully submitted to the network https://testnet.explorer.perawallet.app/tx/UJG3NVCSCW5A63KPV35BPAABLXMXTTEM2CVUKNS4EML3H3EYGMCQ/ + const tx = decodeUnsignedTransaction(Buffer.from("iaNhbXTNA+ijZmVlzQPoomZ2zgJHknejZ2VurHRlc3RuZXQtdjEuMKJnaMQgSGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiKibHbOAkeWX6NyY3bEIGL+gyt60QVEvoM3pnBDXlBkrkpm53vXiQl2W0a1dqbzo3NuZMQgYv6DK3rRBUS+gzemcENeUGSuSmbne9eJCXZbRrV2pvOkdHlwZaNwYXk=", "base64")) + const signed = await cryptoService.signAlgoTransaction(KeyContext.Address, 0, 0, tx) + const to_be_signed = tx.bytesToSign() + expect(nacl.sign.detached.verify(to_be_signed, signed.sig!, key)).toBe(true) + }) }) describe("ECDH cases", () => { diff --git a/assets/arc-0052/contextual.api.crypto.ts b/assets/arc-0052/contextual.api.crypto.ts index eb694177d..ee7fb827f 100644 --- a/assets/arc-0052/contextual.api.crypto.ts +++ b/assets/arc-0052/contextual.api.crypto.ts @@ -8,18 +8,12 @@ import { ready, crypto_sign_ed25519_pk_to_curve25519, crypto_scalarmult, - crypto_sign_detached, - crypto_sign, - crypto_sign_SECRETKEYBYTES, - crypto_sign_ed25519_sk_to_pk, - crypto_scalarmult_ed25519_base, crypto_generichash, - crypto_scalarmult_base } from 'libsodium-wrappers-sumo'; import * as msgpack from "algo-msgpack-with-bigint" import Ajv from "ajv" import { deriveChildNodePrivate, fromSeed } from './bip32-ed25519'; -import { randomBytes } from 'crypto'; +import { Transaction, EncodedSignedTransaction } from 'algosdk'; /** @@ -40,7 +34,6 @@ export interface ChannelKeys { } export enum Encoding { - CBOR = "cbor", MSGPACK = "msgpack", BASE64 = "base64", NONE = "none" @@ -123,6 +116,48 @@ export class ContextualCryptoApi { return await this.deriveKey(rootKey, bip44Path, false) } + /** + * 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 + */ + private async rawSign(bip44Path: number[], data: Uint8Array): Promise { + await ready // libsodium + + const rootKey: Uint8Array = fromSeed(this.seed) + const raw: Uint8Array = await this.deriveKey(rootKey, bip44Path, true) + + const scalar: Uint8Array = raw.slice(0, 32); + const c: Uint8Array = raw.slice(32, 64); + + // \(1): pubKey = scalar * G (base point, no clamp) + const publicKey = crypto_scalarmult_ed25519_base_noclamp(scalar); + + // \(2): h = hash(c || msg) mod q + const r = crypto_core_ed25519_scalar_reduce(crypto_hash_sha512(Buffer.concat([c, data]))) + + // \(4): R = r * G (base point, no clamp) + const R = crypto_scalarmult_ed25519_base_noclamp(r) + + // h = hash(R || pubKey || msg) mod q + let h = crypto_core_ed25519_scalar_reduce(crypto_hash_sha512(Buffer.concat([R, publicKey, data]))); + + // \(5): S = (r + h * k) mod q + const S = crypto_core_ed25519_scalar_add(r, crypto_core_ed25519_scalar_mul(h, scalar)) + + return Buffer.concat([R, S]); + } + /** * Ref: https://datatracker.ietf.org/doc/html/rfc8032#section-5.1.6 * @@ -150,29 +185,44 @@ export class ContextualCryptoApi { await ready // libsodium - const rootKey: Uint8Array = fromSeed(this.seed) const bip44Path: number[] = GetBIP44PathFromContext(context, account, keyIndex) - const raw: Uint8Array = await this.deriveKey(rootKey, bip44Path, true) - const scalar: Uint8Array = raw.slice(0, 32); - const c: Uint8Array = raw.slice(32, 64); + return await this.rawSign(bip44Path, data) + } - // \(1): pubKey = scalar * G (base point, no clamp) - const publicKey = crypto_scalarmult_ed25519_base_noclamp(scalar); - - // \(2): h = hash(c || msg) mod q - const r = crypto_core_ed25519_scalar_reduce(crypto_hash_sha512(Buffer.concat([c, data]))) + /** + * 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 + * - EncodedSignedTransaction object + */ + async signAlgoTransaction(context: KeyContext, account: number, keyIndex: number, tx: Transaction): Promise { + await ready // libsodium - // \(4): R = r * G (base point, no clamp) - const R = crypto_scalarmult_ed25519_base_noclamp(r) + const prefixEncodedTx: Uint8Array = new Uint8Array(tx.bytesToSign()) + const pk = await this.keyGen(context, account, keyIndex) - // h = hash(R || pubKey || msg) mod q - let h = crypto_core_ed25519_scalar_reduce(crypto_hash_sha512(Buffer.concat([R, publicKey, data]))); + const bip44Path: number[] = GetBIP44PathFromContext(context, account, keyIndex) - // \(5): S = (r + h * k) mod q - const S = crypto_core_ed25519_scalar_add(r, crypto_core_ed25519_scalar_mul(h, scalar)) + const sig = await this.rawSign(bip44Path, prefixEncodedTx) - return Buffer.concat([R, S]); + const stx: EncodedSignedTransaction = { + sig: Buffer.from(sig), + txn: tx.get_obj_for_encoding() + } + + if (pk !== tx.from.publicKey) { + stx.sgnr = Buffer.from(pk) + } + return stx } @@ -206,12 +256,6 @@ export class ContextualCryptoApi { default: throw new Error("Invalid encoding") } - - // Check after decoding too - // Some one might try to encode a regular transaction with the protocol reserved prefixes - if (this.hasAlgorandTags(decoded)) { - return ERROR_TAGS_FOUND - } // validate with schema const ajv = new Ajv() @@ -232,12 +276,18 @@ export class ContextualCryptoApi { */ private hasAlgorandTags(message: Uint8Array): boolean { - // Check that decoded doesn't include the following prefixes: TX, MX, progData, Program - if (Buffer.from(message.subarray(0, 2)).toString("ascii") === "TX" || - Buffer.from(message.subarray(0, 2)).toString("ascii") === "MX" || - Buffer.from(message.subarray(0, 8)).toString("ascii") === "progData" || - Buffer.from(message.subarray(0, 7)).toString("ascii") === "Program") { - return true + // Check that decoded doesn't include the following prefixes + // Prefixes taken from go-algorand node software code + // https://github.com/algorand/go-algorand/blob/master/protocol/hash.go + const prefixes: string[] = [ + "appID","arc","aB","aD","aO","aP","aS","AS","B256","BH","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" + ] + for (const prefix of prefixes) { + if (Buffer.from(message.subarray(0, prefix.length)).toString("ascii") === prefix) { + return true + } } return false diff --git a/assets/arc-0052/package.json b/assets/arc-0052/package.json index 21cf3dca6..44b1c6834 100644 --- a/assets/arc-0052/package.json +++ b/assets/arc-0052/package.json @@ -15,11 +15,13 @@ "@types/libsodium-wrappers-sumo": "^0.7.7", "@types/node": "^20.7.1", "ts-jest": "^29.1.1", + "tweetnacl": "^1.0.3", "typescript": "^5.2.2" }, "dependencies": { "ajv": "^8.12.0", "algo-msgpack-with-bigint": "^2.1.1", + "algosdk": "^2.7.0", "bip32-ed25519": "^0.0.4", "bip39": "^3.1.0", "bn.js": "^5.2.1", diff --git a/assets/arc-0052/yarn.lock b/assets/arc-0052/yarn.lock index 28a22b2d1..96d4ce8a1 100644 --- a/assets/arc-0052/yarn.lock +++ b/assets/arc-0052/yarn.lock @@ -679,6 +679,21 @@ algo-msgpack-with-bigint@^2.1.1: resolved "https://registry.yarnpkg.com/algo-msgpack-with-bigint/-/algo-msgpack-with-bigint-2.1.1.tgz#38bb717220525b3ff42232eefdcd9efb9ad405d6" integrity sha512-F1tGh056XczEaEAqu7s+hlZUDWwOBT70Eq0lfMpBP2YguSQVyxRbprLq5rELXKQOyOaixTWYhMeMQMzP0U5FoQ== +algosdk@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/algosdk/-/algosdk-2.7.0.tgz#6ebab130d25fc3cb4c74dce4d8753c7e86db1404" + integrity sha512-sBE9lpV7bup3rZ+q2j3JQaFAE9JwZvjWKX00vPlG8e9txctXbgLL56jZhSWZndqhDI9oI+0P4NldkuQIWdrUyg== + dependencies: + algo-msgpack-with-bigint "^2.1.1" + buffer "^6.0.3" + hi-base32 "^0.5.1" + js-sha256 "^0.9.0" + js-sha3 "^0.8.0" + js-sha512 "^0.8.0" + json-bigint "^1.0.0" + tweetnacl "^1.0.3" + vlq "^2.0.4" + ansi-escapes@^4.2.1: version "4.3.2" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" @@ -790,6 +805,16 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +bignumber.js@^9.0.0: + version "9.1.2" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c" + integrity sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug== + bip32-ed25519@^0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/bip32-ed25519/-/bip32-ed25519-0.0.4.tgz#218943e212c2d3152dfd6f3a929305e3fe86534c" @@ -865,6 +890,14 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -1243,6 +1276,11 @@ human-signals@^2.1.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== +ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + import-local@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" @@ -1717,6 +1755,16 @@ jest@^29.7.0: import-local "^3.0.2" jest-cli "^29.7.0" +js-sha256@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.9.0.tgz#0b89ac166583e91ef9123644bd3c5334ce9d0966" + integrity sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA== + +js-sha3@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" + integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== + js-sha512@^0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/js-sha512/-/js-sha512-0.8.0.tgz#dd22db8d02756faccf19f218e3ed61ec8249f7d4" @@ -1740,6 +1788,13 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== +json-bigint@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1" + integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ== + dependencies: + bignumber.js "^9.0.0" + json-parse-even-better-errors@^2.3.0: version "2.3.1" resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" @@ -2233,6 +2288,11 @@ ts-log@^2.2.4: resolved "https://registry.yarnpkg.com/ts-log/-/ts-log-2.2.5.tgz#aef3252f1143d11047e2cb6f7cfaac7408d96623" integrity sha512-PGcnJoTBnVGy6yYNFxWVNkdcAuAMstvutN9MgDJIV6L0oG8fB+ZNNy1T+wJzah8RPGor1mZuPQkVfXNDpy9eHA== +tweetnacl@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596" + integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw== + type-detect@4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" @@ -2277,6 +2337,11 @@ v8-to-istanbul@^9.0.1: "@types/istanbul-lib-coverage" "^2.0.1" convert-source-map "^2.0.0" +vlq@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/vlq/-/vlq-2.0.4.tgz#6057b85729245b9829e3cc7755f95b228d4fe041" + integrity sha512-aodjPa2wPQFkra1G8CzJBTHXhgk3EVSwxSWXNPr1fgdFLUb8kvLV1iEb6rFgasIsjP82HWI6dsb5Io26DDnasA== + walker@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" From 87d5291fe7feb4cb1a8f7cfbeccb68dadd87fc82 Mon Sep 17 00:00:00 2001 From: Ori Shem-Tov Date: Tue, 26 Mar 2024 16:45:58 +0200 Subject: [PATCH 32/47] add public key verification to trnasaction signing test --- assets/arc-0052/contextual.api.crypto.spec.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/assets/arc-0052/contextual.api.crypto.spec.ts b/assets/arc-0052/contextual.api.crypto.spec.ts index ef745648a..cfce66fe6 100644 --- a/assets/arc-0052/contextual.api.crypto.spec.ts +++ b/assets/arc-0052/contextual.api.crypto.spec.ts @@ -9,19 +9,10 @@ import base32 from "hi-base32" import { JSONSchemaType } from "ajv" import { readFileSync } from "fs" import path from "path" -import { decodeUnsignedTransaction } from "algosdk" +import { decodeUnsignedTransaction, encodeAddress } from "algosdk" import nacl from "tweetnacl" const libBip32Ed25519 = require('bip32-ed25519') -function encodeAddress(publicKey: Buffer): string { - const keyHash: string = sha512_256.create().update(publicKey).hex() - - // last 4 bytes of the hash - const checksum: string = keyHash.slice(-8) - - return base32.encode(ConcatArrays(publicKey, Buffer.from(checksum, "hex"))).slice(0, 58) -} - function ConcatArrays(...arrs: ArrayLike[]) { const size = arrs.reduce((sum, arr) => sum + arr.length, 0) const c = new Uint8Array(size) @@ -381,6 +372,7 @@ describe("Contextual Derivation & Signing", () => { const tx = decodeUnsignedTransaction(Buffer.from("iaNhbXTNA+ijZmVlzQPoomZ2zgJHknejZ2VurHRlc3RuZXQtdjEuMKJnaMQgSGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiKibHbOAkeWX6NyY3bEIGL+gyt60QVEvoM3pnBDXlBkrkpm53vXiQl2W0a1dqbzo3NuZMQgYv6DK3rRBUS+gzemcENeUGSuSmbne9eJCXZbRrV2pvOkdHlwZaNwYXk=", "base64")) const signed = await cryptoService.signAlgoTransaction(KeyContext.Address, 0, 0, tx) const to_be_signed = tx.bytesToSign() + expect(encodeAddress(key)).toEqual("ML7IGK322ECUJPUDG6THAQ26KBSK4STG4555PCIJOZNUNNLWU3Z3ZFXITA") expect(nacl.sign.detached.verify(to_be_signed, signed.sig!, key)).toBe(true) }) }) From 6e8337c2ddb05ca69ad47ca6dc894eff1b1516a1 Mon Sep 17 00:00:00 2001 From: ehanoc Date: Tue, 26 Mar 2024 15:24:12 +0000 Subject: [PATCH 33/47] Revert "1-1 feature parity with Kotlin" --- assets/arc-0052/contextual.api.crypto.spec.ts | 121 +++++++---------- assets/arc-0052/contextual.api.crypto.ts | 122 ++++++------------ assets/arc-0052/package.json | 2 - assets/arc-0052/yarn.lock | 65 ---------- 4 files changed, 82 insertions(+), 228 deletions(-) diff --git a/assets/arc-0052/contextual.api.crypto.spec.ts b/assets/arc-0052/contextual.api.crypto.spec.ts index cfce66fe6..ddbffe14c 100644 --- a/assets/arc-0052/contextual.api.crypto.spec.ts +++ b/assets/arc-0052/contextual.api.crypto.spec.ts @@ -1,4 +1,4 @@ -import { CryptoKX, KeyPair, crypto_kx_client_session_keys, crypto_kx_server_session_keys, crypto_scalarmult, crypto_scalarmult_ed25519_base_noclamp, crypto_secretbox_easy, crypto_secretbox_open_easy, crypto_sign_ed25519_pk_to_curve25519, crypto_sign_ed25519_sk_to_curve25519, crypto_sign_keypair, ready, to_base64 } from "libsodium-wrappers-sumo" +import { CryptoKX, KeyPair, crypto_kx_client_session_keys, crypto_kx_server_session_keys, crypto_scalarmult, crypto_scalarmult_ed25519_base_noclamp, crypto_secretbox_NONCEBYTES, crypto_secretbox_easy, crypto_secretbox_open_easy, crypto_sign_ed25519_pk_to_curve25519, crypto_sign_ed25519_sk_to_curve25519, crypto_sign_keypair, ready, to_base64 } from "libsodium-wrappers-sumo" import * as bip39 from "bip39" import { randomBytes } from "crypto" import { ContextualCryptoApi, ERROR_BAD_DATA, ERROR_TAGS_FOUND, Encoding, KeyContext, SignMetadata, harden } from "./contextual.api.crypto" @@ -9,10 +9,17 @@ import base32 from "hi-base32" import { JSONSchemaType } from "ajv" import { readFileSync } from "fs" import path from "path" -import { decodeUnsignedTransaction, encodeAddress } from "algosdk" -import nacl from "tweetnacl" const libBip32Ed25519 = require('bip32-ed25519') +function encodeAddress(publicKey: Buffer): string { + const keyHash: string = sha512_256.create().update(publicKey).hex() + + // last 4 bytes of the hash + const checksum: string = keyHash.slice(-8) + + return base32.encode(ConcatArrays(publicKey, Buffer.from(checksum, "hex"))).slice(0, 58) +} + function ConcatArrays(...arrs: ArrayLike[]) { const size = arrs.reduce((sum, arr) => sum + arr.length, 0) const c = new Uint8Array(size) @@ -204,7 +211,7 @@ describe("Contextual Derivation & Signing", () => { }) describe("Signing Typed Data", () => { - it("\(OK) Sign authentication challenge of 32 bytes, encoded base64", async () => { + it("\(OK) Sign authentication challenge of 32 bytes, encoded base 64", async () => { const challenge: Uint8Array = new Uint8Array(randomBytes(32)) // read auth schema file for authentication. 32 bytes challenge to sign @@ -221,21 +228,6 @@ describe("Contextual Derivation & Signing", () => { expect(isValid).toBe(true) }) - it("\(OK) Sign authentication challenge of 32 bytes, encoded msgpack", async () => { - const challenge: Uint8Array = new Uint8Array(randomBytes(32)) - - // read auth schema file for authentication. 32 bytes challenge to sign - const authSchema: JSONSchemaType = JSON.parse(readFileSync(path.resolve(__dirname, "schemas/auth.request.json"), "utf8")) - const metadata: SignMetadata = { encoding: Encoding.MSGPACK, schema: authSchema } - const encoded: Uint8Array = msgpack.encode(challenge) - - const signature: Uint8Array = await cryptoService.signData(KeyContext.Address,0, 0, encoded, metadata) - expect(signature).toHaveLength(64) - - const isValid: boolean = await cryptoService.verifyWithPublicKey(signature, encoded, await cryptoService.keyGen(KeyContext.Address, 0, 0)) - expect(isValid).toBe(true) - }) - it ("\(OK) Sign authentication challenge of 32 bytes, no encoding", async () => { const challenge: Uint8Array = new Uint8Array(randomBytes(32)) @@ -250,7 +242,7 @@ describe("Contextual Derivation & Signing", () => { expect(isValid).toBe(true) }) - it("\(OK) Sign Arbitrary Message against Schema, encoded base64", async () => { + it("\(OK) Sign Arbitrary Message against Schem", async () => { const firstKey: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 0) const message = { @@ -278,35 +270,7 @@ describe("Contextual Derivation & Signing", () => { expect(isValid).toBe(true) }) - it("\(OK) Sign Arbitrary Message against Schema, encoded msgpack", async () => { - const firstKey: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 0) - - const message = { - letter: "Hello World" - } - - const encoded: Buffer = Buffer.from(msgpack.encode(message)) - - // Schema of what we are signing - const jsonSchema = { - type: "object", - properties: { - letter: { - type: "string" - } - } - } - - const metadata: SignMetadata = { encoding: Encoding.MSGPACK, schema: jsonSchema } - - const signature: Uint8Array = await cryptoService.signData(KeyContext.Address,0, 0, encoded, metadata) - expect(signature).toHaveLength(64) - - const isValid: boolean = await cryptoService.verifyWithPublicKey(signature, encoded, firstKey) - expect(isValid).toBe(true) - }) - - it("\(FAIL) Signing attempt fails because of invalid data against Schema, encoded base64", async () => { + it("\(FAIL) Signing attempt fails because of invalid data against Schema", async () => { const message = { letter: "Hello World" } @@ -322,23 +286,41 @@ describe("Contextual Derivation & Signing", () => { expect(cryptoService.signData(KeyContext.Identity,0, 0, encoded, metadata)).rejects.toThrowError() }) - it("\(FAIL) Signing attempt fails because of invalid data against Schema, encoded msgpack", async () => { - const message = { - letter: "Hello World" - } - - const encoded: Buffer = Buffer.from(msgpack.encode(message)) + describe("Reject Regular Transaction Signing. IF TAG Prexies are present signing must fail", () => { + describe("Reject tags present in the encoded payload", () => { + it("\(FAIL) [TX] Tag", async () => { + const transaction: Buffer = Buffer.concat([Buffer.from("TX"), msgpack.encode(randomBytes(64))]) + const metadata: SignMetadata = { encoding: Encoding.BASE64, schema: {} } + + const encodedTx: Uint8Array = new Uint8Array(Buffer.from(transaction.toString('base64'))) + expect(cryptoService.signData(KeyContext.Identity,0, 0, encodedTx, metadata)).rejects.toThrowError(ERROR_TAGS_FOUND) + }) - // Schema of what we are signing - const jsonSchema = { - type: "string" - } + it("\(FAIL) [MX] Tag", async () => { + const transaction: Buffer = Buffer.concat([Buffer.from("MX"), msgpack.encode(randomBytes(64))]) + const metadata: SignMetadata = { encoding: Encoding.BASE64, schema: {} } + + const encodedTx: Buffer = Buffer.from(transaction.toString('base64')) + expect(cryptoService.signData(KeyContext.Identity,0, 0, encodedTx, metadata)).rejects.toThrowError(ERROR_TAGS_FOUND) + }) - const metadata: SignMetadata = { encoding: Encoding.MSGPACK, schema: jsonSchema } - expect(cryptoService.signData(KeyContext.Identity,0, 0, encoded, metadata)).rejects.toThrowError() - }) + it("\(FAIL) [Program] Tag", async () => { + const transaction: Buffer = Buffer.concat([Buffer.from("Program"), msgpack.encode(randomBytes(64))]) + const metadata: SignMetadata = { encoding: Encoding.BASE64, schema: {} } + + const encodedTx: Buffer = Buffer.from(transaction.toString('base64')) + expect(cryptoService.signData(KeyContext.Identity,0, 0, encodedTx, metadata)).rejects.toThrowError(ERROR_TAGS_FOUND) + }) - describe("Reject Regular Transaction Signing. IF TAG Prexies are present signing must fail", () => { + it("\(FAIL) [progData] Tag", async () => { + const transaction: Buffer = Buffer.concat([Buffer.from("progData"), msgpack.encode(randomBytes(64))]) + const metadata: SignMetadata = { encoding: Encoding.BASE64, schema: {} } + + const encodedTx: Buffer = Buffer.from(transaction.toString('base64')) + expect(cryptoService.signData(KeyContext.Identity,0, 0, encodedTx, metadata)).rejects.toThrowError(ERROR_TAGS_FOUND) + }) + }) + it("\(FAIL) [TX] Tag", async () => { const transaction: Buffer = Buffer.concat([Buffer.from("TX"), msgpack.encode(randomBytes(64))]) const metadata: SignMetadata = { encoding: Encoding.MSGPACK, schema: {} } @@ -358,23 +340,12 @@ describe("Contextual Derivation & Signing", () => { }) it("\(FAIL) [progData] Tag", async () => { - const transaction: Buffer = Buffer.concat([Buffer.from("ProgData"), msgpack.encode(randomBytes(64))]) + const transaction: Buffer = Buffer.concat([Buffer.from("progData"), msgpack.encode(randomBytes(64))]) const metadata: SignMetadata = { encoding: Encoding.MSGPACK, schema: {} } expect(cryptoService.signData(KeyContext.Identity,0, 0, transaction, metadata)).rejects.toThrowError(ERROR_TAGS_FOUND) }) }) - }) - describe("signing transactions", () => { - it("\(OK) Sign Transaction", async () => { - const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 0) - // this transaction wes successfully submitted to the network https://testnet.explorer.perawallet.app/tx/UJG3NVCSCW5A63KPV35BPAABLXMXTTEM2CVUKNS4EML3H3EYGMCQ/ - const tx = decodeUnsignedTransaction(Buffer.from("iaNhbXTNA+ijZmVlzQPoomZ2zgJHknejZ2VurHRlc3RuZXQtdjEuMKJnaMQgSGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiKibHbOAkeWX6NyY3bEIGL+gyt60QVEvoM3pnBDXlBkrkpm53vXiQl2W0a1dqbzo3NuZMQgYv6DK3rRBUS+gzemcENeUGSuSmbne9eJCXZbRrV2pvOkdHlwZaNwYXk=", "base64")) - const signed = await cryptoService.signAlgoTransaction(KeyContext.Address, 0, 0, tx) - const to_be_signed = tx.bytesToSign() - expect(encodeAddress(key)).toEqual("ML7IGK322ECUJPUDG6THAQ26KBSK4STG4555PCIJOZNUNNLWU3Z3ZFXITA") - expect(nacl.sign.detached.verify(to_be_signed, signed.sig!, key)).toBe(true) - }) }) describe("ECDH cases", () => { diff --git a/assets/arc-0052/contextual.api.crypto.ts b/assets/arc-0052/contextual.api.crypto.ts index ee7fb827f..eb694177d 100644 --- a/assets/arc-0052/contextual.api.crypto.ts +++ b/assets/arc-0052/contextual.api.crypto.ts @@ -8,12 +8,18 @@ import { ready, crypto_sign_ed25519_pk_to_curve25519, crypto_scalarmult, + crypto_sign_detached, + crypto_sign, + crypto_sign_SECRETKEYBYTES, + crypto_sign_ed25519_sk_to_pk, + crypto_scalarmult_ed25519_base, crypto_generichash, + crypto_scalarmult_base } from 'libsodium-wrappers-sumo'; import * as msgpack from "algo-msgpack-with-bigint" import Ajv from "ajv" import { deriveChildNodePrivate, fromSeed } from './bip32-ed25519'; -import { Transaction, EncodedSignedTransaction } from 'algosdk'; +import { randomBytes } from 'crypto'; /** @@ -34,6 +40,7 @@ export interface ChannelKeys { } export enum Encoding { + CBOR = "cbor", MSGPACK = "msgpack", BASE64 = "base64", NONE = "none" @@ -116,48 +123,6 @@ export class ContextualCryptoApi { return await this.deriveKey(rootKey, bip44Path, false) } - /** - * 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 - */ - private async rawSign(bip44Path: number[], data: Uint8Array): Promise { - await ready // libsodium - - const rootKey: Uint8Array = fromSeed(this.seed) - const raw: Uint8Array = await this.deriveKey(rootKey, bip44Path, true) - - const scalar: Uint8Array = raw.slice(0, 32); - const c: Uint8Array = raw.slice(32, 64); - - // \(1): pubKey = scalar * G (base point, no clamp) - const publicKey = crypto_scalarmult_ed25519_base_noclamp(scalar); - - // \(2): h = hash(c || msg) mod q - const r = crypto_core_ed25519_scalar_reduce(crypto_hash_sha512(Buffer.concat([c, data]))) - - // \(4): R = r * G (base point, no clamp) - const R = crypto_scalarmult_ed25519_base_noclamp(r) - - // h = hash(R || pubKey || msg) mod q - let h = crypto_core_ed25519_scalar_reduce(crypto_hash_sha512(Buffer.concat([R, publicKey, data]))); - - // \(5): S = (r + h * k) mod q - const S = crypto_core_ed25519_scalar_add(r, crypto_core_ed25519_scalar_mul(h, scalar)) - - return Buffer.concat([R, S]); - } - /** * Ref: https://datatracker.ietf.org/doc/html/rfc8032#section-5.1.6 * @@ -185,44 +150,29 @@ export class ContextualCryptoApi { await ready // libsodium + const rootKey: Uint8Array = fromSeed(this.seed) const bip44Path: number[] = GetBIP44PathFromContext(context, account, keyIndex) + const raw: Uint8Array = await this.deriveKey(rootKey, bip44Path, true) - return await this.rawSign(bip44Path, data) - } - - /** - * 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 - * - EncodedSignedTransaction object - */ - async signAlgoTransaction(context: KeyContext, account: number, keyIndex: number, tx: Transaction): Promise { - await ready // libsodium + const scalar: Uint8Array = raw.slice(0, 32); + const c: Uint8Array = raw.slice(32, 64); - const prefixEncodedTx: Uint8Array = new Uint8Array(tx.bytesToSign()) - const pk = await this.keyGen(context, account, keyIndex) + // \(1): pubKey = scalar * G (base point, no clamp) + const publicKey = crypto_scalarmult_ed25519_base_noclamp(scalar); + + // \(2): h = hash(c || msg) mod q + const r = crypto_core_ed25519_scalar_reduce(crypto_hash_sha512(Buffer.concat([c, data]))) - const bip44Path: number[] = GetBIP44PathFromContext(context, account, keyIndex) + // \(4): R = r * G (base point, no clamp) + const R = crypto_scalarmult_ed25519_base_noclamp(r) - const sig = await this.rawSign(bip44Path, prefixEncodedTx) + // h = hash(R || pubKey || msg) mod q + let h = crypto_core_ed25519_scalar_reduce(crypto_hash_sha512(Buffer.concat([R, publicKey, data]))); - const stx: EncodedSignedTransaction = { - sig: Buffer.from(sig), - txn: tx.get_obj_for_encoding() - } + // \(5): S = (r + h * k) mod q + const S = crypto_core_ed25519_scalar_add(r, crypto_core_ed25519_scalar_mul(h, scalar)) - if (pk !== tx.from.publicKey) { - stx.sgnr = Buffer.from(pk) - } - return stx + return Buffer.concat([R, S]); } @@ -256,6 +206,12 @@ export class ContextualCryptoApi { default: throw new Error("Invalid encoding") } + + // Check after decoding too + // Some one might try to encode a regular transaction with the protocol reserved prefixes + if (this.hasAlgorandTags(decoded)) { + return ERROR_TAGS_FOUND + } // validate with schema const ajv = new Ajv() @@ -276,18 +232,12 @@ export class ContextualCryptoApi { */ private hasAlgorandTags(message: Uint8Array): boolean { - // Check that decoded doesn't include the following prefixes - // Prefixes taken from go-algorand node software code - // https://github.com/algorand/go-algorand/blob/master/protocol/hash.go - const prefixes: string[] = [ - "appID","arc","aB","aD","aO","aP","aS","AS","B256","BH","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" - ] - for (const prefix of prefixes) { - if (Buffer.from(message.subarray(0, prefix.length)).toString("ascii") === prefix) { - return true - } + // Check that decoded doesn't include the following prefixes: TX, MX, progData, Program + if (Buffer.from(message.subarray(0, 2)).toString("ascii") === "TX" || + Buffer.from(message.subarray(0, 2)).toString("ascii") === "MX" || + Buffer.from(message.subarray(0, 8)).toString("ascii") === "progData" || + Buffer.from(message.subarray(0, 7)).toString("ascii") === "Program") { + return true } return false diff --git a/assets/arc-0052/package.json b/assets/arc-0052/package.json index 44b1c6834..21cf3dca6 100644 --- a/assets/arc-0052/package.json +++ b/assets/arc-0052/package.json @@ -15,13 +15,11 @@ "@types/libsodium-wrappers-sumo": "^0.7.7", "@types/node": "^20.7.1", "ts-jest": "^29.1.1", - "tweetnacl": "^1.0.3", "typescript": "^5.2.2" }, "dependencies": { "ajv": "^8.12.0", "algo-msgpack-with-bigint": "^2.1.1", - "algosdk": "^2.7.0", "bip32-ed25519": "^0.0.4", "bip39": "^3.1.0", "bn.js": "^5.2.1", diff --git a/assets/arc-0052/yarn.lock b/assets/arc-0052/yarn.lock index 96d4ce8a1..28a22b2d1 100644 --- a/assets/arc-0052/yarn.lock +++ b/assets/arc-0052/yarn.lock @@ -679,21 +679,6 @@ algo-msgpack-with-bigint@^2.1.1: resolved "https://registry.yarnpkg.com/algo-msgpack-with-bigint/-/algo-msgpack-with-bigint-2.1.1.tgz#38bb717220525b3ff42232eefdcd9efb9ad405d6" integrity sha512-F1tGh056XczEaEAqu7s+hlZUDWwOBT70Eq0lfMpBP2YguSQVyxRbprLq5rELXKQOyOaixTWYhMeMQMzP0U5FoQ== -algosdk@^2.7.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/algosdk/-/algosdk-2.7.0.tgz#6ebab130d25fc3cb4c74dce4d8753c7e86db1404" - integrity sha512-sBE9lpV7bup3rZ+q2j3JQaFAE9JwZvjWKX00vPlG8e9txctXbgLL56jZhSWZndqhDI9oI+0P4NldkuQIWdrUyg== - dependencies: - algo-msgpack-with-bigint "^2.1.1" - buffer "^6.0.3" - hi-base32 "^0.5.1" - js-sha256 "^0.9.0" - js-sha3 "^0.8.0" - js-sha512 "^0.8.0" - json-bigint "^1.0.0" - tweetnacl "^1.0.3" - vlq "^2.0.4" - ansi-escapes@^4.2.1: version "4.3.2" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" @@ -805,16 +790,6 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base64-js@^1.3.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - -bignumber.js@^9.0.0: - version "9.1.2" - resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c" - integrity sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug== - bip32-ed25519@^0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/bip32-ed25519/-/bip32-ed25519-0.0.4.tgz#218943e212c2d3152dfd6f3a929305e3fe86534c" @@ -890,14 +865,6 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== -buffer@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" - integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.2.1" - callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -1276,11 +1243,6 @@ human-signals@^2.1.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== -ieee754@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - import-local@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" @@ -1755,16 +1717,6 @@ jest@^29.7.0: import-local "^3.0.2" jest-cli "^29.7.0" -js-sha256@^0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.9.0.tgz#0b89ac166583e91ef9123644bd3c5334ce9d0966" - integrity sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA== - -js-sha3@^0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" - integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== - js-sha512@^0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/js-sha512/-/js-sha512-0.8.0.tgz#dd22db8d02756faccf19f218e3ed61ec8249f7d4" @@ -1788,13 +1740,6 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== -json-bigint@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1" - integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ== - dependencies: - bignumber.js "^9.0.0" - json-parse-even-better-errors@^2.3.0: version "2.3.1" resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" @@ -2288,11 +2233,6 @@ ts-log@^2.2.4: resolved "https://registry.yarnpkg.com/ts-log/-/ts-log-2.2.5.tgz#aef3252f1143d11047e2cb6f7cfaac7408d96623" integrity sha512-PGcnJoTBnVGy6yYNFxWVNkdcAuAMstvutN9MgDJIV6L0oG8fB+ZNNy1T+wJzah8RPGor1mZuPQkVfXNDpy9eHA== -tweetnacl@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596" - integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw== - type-detect@4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" @@ -2337,11 +2277,6 @@ v8-to-istanbul@^9.0.1: "@types/istanbul-lib-coverage" "^2.0.1" convert-source-map "^2.0.0" -vlq@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/vlq/-/vlq-2.0.4.tgz#6057b85729245b9829e3cc7755f95b228d4fe041" - integrity sha512-aodjPa2wPQFkra1G8CzJBTHXhgk3EVSwxSWXNPr1fgdFLUb8kvLV1iEb6rFgasIsjP82HWI6dsb5Io26DDnasA== - walker@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" From ad3642acdff73443198e32a9800c54c0822cff27 Mon Sep 17 00:00:00 2001 From: Ori Shem-Tov Date: Mon, 25 Mar 2024 16:13:10 +0200 Subject: [PATCH 34/47] 1-1 feature parity with Kotlin add public key verification to trnasaction signing test --- assets/arc-0052/contextual.api.crypto.spec.ts | 121 ++++++++++------- assets/arc-0052/contextual.api.crypto.ts | 122 ++++++++++++------ assets/arc-0052/package.json | 2 + assets/arc-0052/yarn.lock | 65 ++++++++++ 4 files changed, 228 insertions(+), 82 deletions(-) diff --git a/assets/arc-0052/contextual.api.crypto.spec.ts b/assets/arc-0052/contextual.api.crypto.spec.ts index ddbffe14c..cfce66fe6 100644 --- a/assets/arc-0052/contextual.api.crypto.spec.ts +++ b/assets/arc-0052/contextual.api.crypto.spec.ts @@ -1,4 +1,4 @@ -import { CryptoKX, KeyPair, crypto_kx_client_session_keys, crypto_kx_server_session_keys, crypto_scalarmult, crypto_scalarmult_ed25519_base_noclamp, crypto_secretbox_NONCEBYTES, crypto_secretbox_easy, crypto_secretbox_open_easy, crypto_sign_ed25519_pk_to_curve25519, crypto_sign_ed25519_sk_to_curve25519, crypto_sign_keypair, ready, to_base64 } from "libsodium-wrappers-sumo" +import { CryptoKX, KeyPair, crypto_kx_client_session_keys, crypto_kx_server_session_keys, crypto_scalarmult, crypto_scalarmult_ed25519_base_noclamp, crypto_secretbox_easy, crypto_secretbox_open_easy, crypto_sign_ed25519_pk_to_curve25519, crypto_sign_ed25519_sk_to_curve25519, crypto_sign_keypair, ready, to_base64 } from "libsodium-wrappers-sumo" import * as bip39 from "bip39" import { randomBytes } from "crypto" import { ContextualCryptoApi, ERROR_BAD_DATA, ERROR_TAGS_FOUND, Encoding, KeyContext, SignMetadata, harden } from "./contextual.api.crypto" @@ -9,17 +9,10 @@ import base32 from "hi-base32" import { JSONSchemaType } from "ajv" import { readFileSync } from "fs" import path from "path" +import { decodeUnsignedTransaction, encodeAddress } from "algosdk" +import nacl from "tweetnacl" const libBip32Ed25519 = require('bip32-ed25519') -function encodeAddress(publicKey: Buffer): string { - const keyHash: string = sha512_256.create().update(publicKey).hex() - - // last 4 bytes of the hash - const checksum: string = keyHash.slice(-8) - - return base32.encode(ConcatArrays(publicKey, Buffer.from(checksum, "hex"))).slice(0, 58) -} - function ConcatArrays(...arrs: ArrayLike[]) { const size = arrs.reduce((sum, arr) => sum + arr.length, 0) const c = new Uint8Array(size) @@ -211,7 +204,7 @@ describe("Contextual Derivation & Signing", () => { }) describe("Signing Typed Data", () => { - it("\(OK) Sign authentication challenge of 32 bytes, encoded base 64", async () => { + it("\(OK) Sign authentication challenge of 32 bytes, encoded base64", async () => { const challenge: Uint8Array = new Uint8Array(randomBytes(32)) // read auth schema file for authentication. 32 bytes challenge to sign @@ -228,6 +221,21 @@ describe("Contextual Derivation & Signing", () => { expect(isValid).toBe(true) }) + it("\(OK) Sign authentication challenge of 32 bytes, encoded msgpack", async () => { + const challenge: Uint8Array = new Uint8Array(randomBytes(32)) + + // read auth schema file for authentication. 32 bytes challenge to sign + const authSchema: JSONSchemaType = JSON.parse(readFileSync(path.resolve(__dirname, "schemas/auth.request.json"), "utf8")) + const metadata: SignMetadata = { encoding: Encoding.MSGPACK, schema: authSchema } + const encoded: Uint8Array = msgpack.encode(challenge) + + const signature: Uint8Array = await cryptoService.signData(KeyContext.Address,0, 0, encoded, metadata) + expect(signature).toHaveLength(64) + + const isValid: boolean = await cryptoService.verifyWithPublicKey(signature, encoded, await cryptoService.keyGen(KeyContext.Address, 0, 0)) + expect(isValid).toBe(true) + }) + it ("\(OK) Sign authentication challenge of 32 bytes, no encoding", async () => { const challenge: Uint8Array = new Uint8Array(randomBytes(32)) @@ -242,7 +250,7 @@ describe("Contextual Derivation & Signing", () => { expect(isValid).toBe(true) }) - it("\(OK) Sign Arbitrary Message against Schem", async () => { + it("\(OK) Sign Arbitrary Message against Schema, encoded base64", async () => { const firstKey: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 0) const message = { @@ -270,7 +278,35 @@ describe("Contextual Derivation & Signing", () => { expect(isValid).toBe(true) }) - it("\(FAIL) Signing attempt fails because of invalid data against Schema", async () => { + it("\(OK) Sign Arbitrary Message against Schema, encoded msgpack", async () => { + const firstKey: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 0) + + const message = { + letter: "Hello World" + } + + const encoded: Buffer = Buffer.from(msgpack.encode(message)) + + // Schema of what we are signing + const jsonSchema = { + type: "object", + properties: { + letter: { + type: "string" + } + } + } + + const metadata: SignMetadata = { encoding: Encoding.MSGPACK, schema: jsonSchema } + + const signature: Uint8Array = await cryptoService.signData(KeyContext.Address,0, 0, encoded, metadata) + expect(signature).toHaveLength(64) + + const isValid: boolean = await cryptoService.verifyWithPublicKey(signature, encoded, firstKey) + expect(isValid).toBe(true) + }) + + it("\(FAIL) Signing attempt fails because of invalid data against Schema, encoded base64", async () => { const message = { letter: "Hello World" } @@ -286,41 +322,23 @@ describe("Contextual Derivation & Signing", () => { expect(cryptoService.signData(KeyContext.Identity,0, 0, encoded, metadata)).rejects.toThrowError() }) - describe("Reject Regular Transaction Signing. IF TAG Prexies are present signing must fail", () => { - describe("Reject tags present in the encoded payload", () => { - it("\(FAIL) [TX] Tag", async () => { - const transaction: Buffer = Buffer.concat([Buffer.from("TX"), msgpack.encode(randomBytes(64))]) - const metadata: SignMetadata = { encoding: Encoding.BASE64, schema: {} } - - const encodedTx: Uint8Array = new Uint8Array(Buffer.from(transaction.toString('base64'))) - expect(cryptoService.signData(KeyContext.Identity,0, 0, encodedTx, metadata)).rejects.toThrowError(ERROR_TAGS_FOUND) - }) + it("\(FAIL) Signing attempt fails because of invalid data against Schema, encoded msgpack", async () => { + const message = { + letter: "Hello World" + } - it("\(FAIL) [MX] Tag", async () => { - const transaction: Buffer = Buffer.concat([Buffer.from("MX"), msgpack.encode(randomBytes(64))]) - const metadata: SignMetadata = { encoding: Encoding.BASE64, schema: {} } - - const encodedTx: Buffer = Buffer.from(transaction.toString('base64')) - expect(cryptoService.signData(KeyContext.Identity,0, 0, encodedTx, metadata)).rejects.toThrowError(ERROR_TAGS_FOUND) - }) + const encoded: Buffer = Buffer.from(msgpack.encode(message)) - it("\(FAIL) [Program] Tag", async () => { - const transaction: Buffer = Buffer.concat([Buffer.from("Program"), msgpack.encode(randomBytes(64))]) - const metadata: SignMetadata = { encoding: Encoding.BASE64, schema: {} } - - const encodedTx: Buffer = Buffer.from(transaction.toString('base64')) - expect(cryptoService.signData(KeyContext.Identity,0, 0, encodedTx, metadata)).rejects.toThrowError(ERROR_TAGS_FOUND) - }) + // Schema of what we are signing + const jsonSchema = { + type: "string" + } - it("\(FAIL) [progData] Tag", async () => { - const transaction: Buffer = Buffer.concat([Buffer.from("progData"), msgpack.encode(randomBytes(64))]) - const metadata: SignMetadata = { encoding: Encoding.BASE64, schema: {} } - - const encodedTx: Buffer = Buffer.from(transaction.toString('base64')) - expect(cryptoService.signData(KeyContext.Identity,0, 0, encodedTx, metadata)).rejects.toThrowError(ERROR_TAGS_FOUND) - }) - }) - + const metadata: SignMetadata = { encoding: Encoding.MSGPACK, schema: jsonSchema } + expect(cryptoService.signData(KeyContext.Identity,0, 0, encoded, metadata)).rejects.toThrowError() + }) + + describe("Reject Regular Transaction Signing. IF TAG Prexies are present signing must fail", () => { it("\(FAIL) [TX] Tag", async () => { const transaction: Buffer = Buffer.concat([Buffer.from("TX"), msgpack.encode(randomBytes(64))]) const metadata: SignMetadata = { encoding: Encoding.MSGPACK, schema: {} } @@ -340,12 +358,23 @@ describe("Contextual Derivation & Signing", () => { }) it("\(FAIL) [progData] Tag", async () => { - const transaction: Buffer = Buffer.concat([Buffer.from("progData"), msgpack.encode(randomBytes(64))]) + const transaction: Buffer = Buffer.concat([Buffer.from("ProgData"), msgpack.encode(randomBytes(64))]) const metadata: SignMetadata = { encoding: Encoding.MSGPACK, schema: {} } expect(cryptoService.signData(KeyContext.Identity,0, 0, transaction, metadata)).rejects.toThrowError(ERROR_TAGS_FOUND) }) }) + }) + describe("signing transactions", () => { + it("\(OK) Sign Transaction", async () => { + const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 0) + // this transaction wes successfully submitted to the network https://testnet.explorer.perawallet.app/tx/UJG3NVCSCW5A63KPV35BPAABLXMXTTEM2CVUKNS4EML3H3EYGMCQ/ + const tx = decodeUnsignedTransaction(Buffer.from("iaNhbXTNA+ijZmVlzQPoomZ2zgJHknejZ2VurHRlc3RuZXQtdjEuMKJnaMQgSGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiKibHbOAkeWX6NyY3bEIGL+gyt60QVEvoM3pnBDXlBkrkpm53vXiQl2W0a1dqbzo3NuZMQgYv6DK3rRBUS+gzemcENeUGSuSmbne9eJCXZbRrV2pvOkdHlwZaNwYXk=", "base64")) + const signed = await cryptoService.signAlgoTransaction(KeyContext.Address, 0, 0, tx) + const to_be_signed = tx.bytesToSign() + expect(encodeAddress(key)).toEqual("ML7IGK322ECUJPUDG6THAQ26KBSK4STG4555PCIJOZNUNNLWU3Z3ZFXITA") + expect(nacl.sign.detached.verify(to_be_signed, signed.sig!, key)).toBe(true) + }) }) describe("ECDH cases", () => { diff --git a/assets/arc-0052/contextual.api.crypto.ts b/assets/arc-0052/contextual.api.crypto.ts index eb694177d..ee7fb827f 100644 --- a/assets/arc-0052/contextual.api.crypto.ts +++ b/assets/arc-0052/contextual.api.crypto.ts @@ -8,18 +8,12 @@ import { ready, crypto_sign_ed25519_pk_to_curve25519, crypto_scalarmult, - crypto_sign_detached, - crypto_sign, - crypto_sign_SECRETKEYBYTES, - crypto_sign_ed25519_sk_to_pk, - crypto_scalarmult_ed25519_base, crypto_generichash, - crypto_scalarmult_base } from 'libsodium-wrappers-sumo'; import * as msgpack from "algo-msgpack-with-bigint" import Ajv from "ajv" import { deriveChildNodePrivate, fromSeed } from './bip32-ed25519'; -import { randomBytes } from 'crypto'; +import { Transaction, EncodedSignedTransaction } from 'algosdk'; /** @@ -40,7 +34,6 @@ export interface ChannelKeys { } export enum Encoding { - CBOR = "cbor", MSGPACK = "msgpack", BASE64 = "base64", NONE = "none" @@ -123,6 +116,48 @@ export class ContextualCryptoApi { return await this.deriveKey(rootKey, bip44Path, false) } + /** + * 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 + */ + private async rawSign(bip44Path: number[], data: Uint8Array): Promise { + await ready // libsodium + + const rootKey: Uint8Array = fromSeed(this.seed) + const raw: Uint8Array = await this.deriveKey(rootKey, bip44Path, true) + + const scalar: Uint8Array = raw.slice(0, 32); + const c: Uint8Array = raw.slice(32, 64); + + // \(1): pubKey = scalar * G (base point, no clamp) + const publicKey = crypto_scalarmult_ed25519_base_noclamp(scalar); + + // \(2): h = hash(c || msg) mod q + const r = crypto_core_ed25519_scalar_reduce(crypto_hash_sha512(Buffer.concat([c, data]))) + + // \(4): R = r * G (base point, no clamp) + const R = crypto_scalarmult_ed25519_base_noclamp(r) + + // h = hash(R || pubKey || msg) mod q + let h = crypto_core_ed25519_scalar_reduce(crypto_hash_sha512(Buffer.concat([R, publicKey, data]))); + + // \(5): S = (r + h * k) mod q + const S = crypto_core_ed25519_scalar_add(r, crypto_core_ed25519_scalar_mul(h, scalar)) + + return Buffer.concat([R, S]); + } + /** * Ref: https://datatracker.ietf.org/doc/html/rfc8032#section-5.1.6 * @@ -150,29 +185,44 @@ export class ContextualCryptoApi { await ready // libsodium - const rootKey: Uint8Array = fromSeed(this.seed) const bip44Path: number[] = GetBIP44PathFromContext(context, account, keyIndex) - const raw: Uint8Array = await this.deriveKey(rootKey, bip44Path, true) - const scalar: Uint8Array = raw.slice(0, 32); - const c: Uint8Array = raw.slice(32, 64); + return await this.rawSign(bip44Path, data) + } - // \(1): pubKey = scalar * G (base point, no clamp) - const publicKey = crypto_scalarmult_ed25519_base_noclamp(scalar); - - // \(2): h = hash(c || msg) mod q - const r = crypto_core_ed25519_scalar_reduce(crypto_hash_sha512(Buffer.concat([c, data]))) + /** + * 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 + * - EncodedSignedTransaction object + */ + async signAlgoTransaction(context: KeyContext, account: number, keyIndex: number, tx: Transaction): Promise { + await ready // libsodium - // \(4): R = r * G (base point, no clamp) - const R = crypto_scalarmult_ed25519_base_noclamp(r) + const prefixEncodedTx: Uint8Array = new Uint8Array(tx.bytesToSign()) + const pk = await this.keyGen(context, account, keyIndex) - // h = hash(R || pubKey || msg) mod q - let h = crypto_core_ed25519_scalar_reduce(crypto_hash_sha512(Buffer.concat([R, publicKey, data]))); + const bip44Path: number[] = GetBIP44PathFromContext(context, account, keyIndex) - // \(5): S = (r + h * k) mod q - const S = crypto_core_ed25519_scalar_add(r, crypto_core_ed25519_scalar_mul(h, scalar)) + const sig = await this.rawSign(bip44Path, prefixEncodedTx) - return Buffer.concat([R, S]); + const stx: EncodedSignedTransaction = { + sig: Buffer.from(sig), + txn: tx.get_obj_for_encoding() + } + + if (pk !== tx.from.publicKey) { + stx.sgnr = Buffer.from(pk) + } + return stx } @@ -206,12 +256,6 @@ export class ContextualCryptoApi { default: throw new Error("Invalid encoding") } - - // Check after decoding too - // Some one might try to encode a regular transaction with the protocol reserved prefixes - if (this.hasAlgorandTags(decoded)) { - return ERROR_TAGS_FOUND - } // validate with schema const ajv = new Ajv() @@ -232,12 +276,18 @@ export class ContextualCryptoApi { */ private hasAlgorandTags(message: Uint8Array): boolean { - // Check that decoded doesn't include the following prefixes: TX, MX, progData, Program - if (Buffer.from(message.subarray(0, 2)).toString("ascii") === "TX" || - Buffer.from(message.subarray(0, 2)).toString("ascii") === "MX" || - Buffer.from(message.subarray(0, 8)).toString("ascii") === "progData" || - Buffer.from(message.subarray(0, 7)).toString("ascii") === "Program") { - return true + // Check that decoded doesn't include the following prefixes + // Prefixes taken from go-algorand node software code + // https://github.com/algorand/go-algorand/blob/master/protocol/hash.go + const prefixes: string[] = [ + "appID","arc","aB","aD","aO","aP","aS","AS","B256","BH","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" + ] + for (const prefix of prefixes) { + if (Buffer.from(message.subarray(0, prefix.length)).toString("ascii") === prefix) { + return true + } } return false diff --git a/assets/arc-0052/package.json b/assets/arc-0052/package.json index 21cf3dca6..44b1c6834 100644 --- a/assets/arc-0052/package.json +++ b/assets/arc-0052/package.json @@ -15,11 +15,13 @@ "@types/libsodium-wrappers-sumo": "^0.7.7", "@types/node": "^20.7.1", "ts-jest": "^29.1.1", + "tweetnacl": "^1.0.3", "typescript": "^5.2.2" }, "dependencies": { "ajv": "^8.12.0", "algo-msgpack-with-bigint": "^2.1.1", + "algosdk": "^2.7.0", "bip32-ed25519": "^0.0.4", "bip39": "^3.1.0", "bn.js": "^5.2.1", diff --git a/assets/arc-0052/yarn.lock b/assets/arc-0052/yarn.lock index 28a22b2d1..96d4ce8a1 100644 --- a/assets/arc-0052/yarn.lock +++ b/assets/arc-0052/yarn.lock @@ -679,6 +679,21 @@ algo-msgpack-with-bigint@^2.1.1: resolved "https://registry.yarnpkg.com/algo-msgpack-with-bigint/-/algo-msgpack-with-bigint-2.1.1.tgz#38bb717220525b3ff42232eefdcd9efb9ad405d6" integrity sha512-F1tGh056XczEaEAqu7s+hlZUDWwOBT70Eq0lfMpBP2YguSQVyxRbprLq5rELXKQOyOaixTWYhMeMQMzP0U5FoQ== +algosdk@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/algosdk/-/algosdk-2.7.0.tgz#6ebab130d25fc3cb4c74dce4d8753c7e86db1404" + integrity sha512-sBE9lpV7bup3rZ+q2j3JQaFAE9JwZvjWKX00vPlG8e9txctXbgLL56jZhSWZndqhDI9oI+0P4NldkuQIWdrUyg== + dependencies: + algo-msgpack-with-bigint "^2.1.1" + buffer "^6.0.3" + hi-base32 "^0.5.1" + js-sha256 "^0.9.0" + js-sha3 "^0.8.0" + js-sha512 "^0.8.0" + json-bigint "^1.0.0" + tweetnacl "^1.0.3" + vlq "^2.0.4" + ansi-escapes@^4.2.1: version "4.3.2" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" @@ -790,6 +805,16 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +bignumber.js@^9.0.0: + version "9.1.2" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c" + integrity sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug== + bip32-ed25519@^0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/bip32-ed25519/-/bip32-ed25519-0.0.4.tgz#218943e212c2d3152dfd6f3a929305e3fe86534c" @@ -865,6 +890,14 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -1243,6 +1276,11 @@ human-signals@^2.1.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== +ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + import-local@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" @@ -1717,6 +1755,16 @@ jest@^29.7.0: import-local "^3.0.2" jest-cli "^29.7.0" +js-sha256@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.9.0.tgz#0b89ac166583e91ef9123644bd3c5334ce9d0966" + integrity sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA== + +js-sha3@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" + integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== + js-sha512@^0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/js-sha512/-/js-sha512-0.8.0.tgz#dd22db8d02756faccf19f218e3ed61ec8249f7d4" @@ -1740,6 +1788,13 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== +json-bigint@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1" + integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ== + dependencies: + bignumber.js "^9.0.0" + json-parse-even-better-errors@^2.3.0: version "2.3.1" resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" @@ -2233,6 +2288,11 @@ ts-log@^2.2.4: resolved "https://registry.yarnpkg.com/ts-log/-/ts-log-2.2.5.tgz#aef3252f1143d11047e2cb6f7cfaac7408d96623" integrity sha512-PGcnJoTBnVGy6yYNFxWVNkdcAuAMstvutN9MgDJIV6L0oG8fB+ZNNy1T+wJzah8RPGor1mZuPQkVfXNDpy9eHA== +tweetnacl@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596" + integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw== + type-detect@4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" @@ -2277,6 +2337,11 @@ v8-to-istanbul@^9.0.1: "@types/istanbul-lib-coverage" "^2.0.1" convert-source-map "^2.0.0" +vlq@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/vlq/-/vlq-2.0.4.tgz#6057b85729245b9829e3cc7755f95b228d4fe041" + integrity sha512-aodjPa2wPQFkra1G8CzJBTHXhgk3EVSwxSWXNPr1fgdFLUb8kvLV1iEb6rFgasIsjP82HWI6dsb5Io26DDnasA== + walker@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" From a151b98d7d206c0c26da097779c39b765ad7627e Mon Sep 17 00:00:00 2001 From: Ori Shem-Tov Date: Wed, 27 Mar 2024 16:31:41 +0200 Subject: [PATCH 35/47] remove algosdk dep --- assets/arc-0052/contextual.api.crypto.spec.ts | 19 ++++-- assets/arc-0052/contextual.api.crypto.ts | 20 ++----- assets/arc-0052/package.json | 1 - assets/arc-0052/yarn.lock | 60 ------------------- 4 files changed, 17 insertions(+), 83 deletions(-) diff --git a/assets/arc-0052/contextual.api.crypto.spec.ts b/assets/arc-0052/contextual.api.crypto.spec.ts index cfce66fe6..5c39622e7 100644 --- a/assets/arc-0052/contextual.api.crypto.spec.ts +++ b/assets/arc-0052/contextual.api.crypto.spec.ts @@ -9,10 +9,18 @@ import base32 from "hi-base32" import { JSONSchemaType } from "ajv" import { readFileSync } from "fs" import path from "path" -import { decodeUnsignedTransaction, encodeAddress } from "algosdk" import nacl from "tweetnacl" const libBip32Ed25519 = require('bip32-ed25519') +function encodeAddress(publicKey: Buffer): string { + const keyHash: string = sha512_256.create().update(publicKey).hex() + + // last 4 bytes of the hash + const checksum: string = keyHash.slice(-8) + + return base32.encode(ConcatArrays(publicKey, Buffer.from(checksum, "hex"))).slice(0, 58) +} + function ConcatArrays(...arrs: ArrayLike[]) { const size = arrs.reduce((sum, arr) => sum + arr.length, 0) const c = new Uint8Array(size) @@ -369,11 +377,10 @@ describe("Contextual Derivation & Signing", () => { it("\(OK) Sign Transaction", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 0) // this transaction wes successfully submitted to the network https://testnet.explorer.perawallet.app/tx/UJG3NVCSCW5A63KPV35BPAABLXMXTTEM2CVUKNS4EML3H3EYGMCQ/ - const tx = decodeUnsignedTransaction(Buffer.from("iaNhbXTNA+ijZmVlzQPoomZ2zgJHknejZ2VurHRlc3RuZXQtdjEuMKJnaMQgSGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiKibHbOAkeWX6NyY3bEIGL+gyt60QVEvoM3pnBDXlBkrkpm53vXiQl2W0a1dqbzo3NuZMQgYv6DK3rRBUS+gzemcENeUGSuSmbne9eJCXZbRrV2pvOkdHlwZaNwYXk=", "base64")) - const signed = await cryptoService.signAlgoTransaction(KeyContext.Address, 0, 0, tx) - const to_be_signed = tx.bytesToSign() - expect(encodeAddress(key)).toEqual("ML7IGK322ECUJPUDG6THAQ26KBSK4STG4555PCIJOZNUNNLWU3Z3ZFXITA") - expect(nacl.sign.detached.verify(to_be_signed, signed.sig!, key)).toBe(true) + const prefixEncodedTx = new Uint8Array(Buffer.from('VFiJo2FtdM0D6KNmZWXNA+iiZnbOAkeSd6NnZW6sdGVzdG5ldC12MS4womdoxCBIY7UYpLPITsgQ8i1PEIHLD3HwWaesIN7GL39w5Qk6IqJsds4CR5Zfo3JjdsQgYv6DK3rRBUS+gzemcENeUGSuSmbne9eJCXZbRrV2pvOjc25kxCBi/oMretEFRL6DN6ZwQ15QZK5KZud714kJdltGtXam86R0eXBlo3BheQ==', 'base64')) + const sig = await cryptoService.signAlgoTransaction(KeyContext.Address, 0, 0, prefixEncodedTx) + expect(encodeAddress(Buffer.from(key))).toEqual("ML7IGK322ECUJPUDG6THAQ26KBSK4STG4555PCIJOZNUNNLWU3Z3ZFXITA") + expect(nacl.sign.detached.verify(prefixEncodedTx, sig, key)).toBe(true) }) }) diff --git a/assets/arc-0052/contextual.api.crypto.ts b/assets/arc-0052/contextual.api.crypto.ts index ee7fb827f..a21c057bc 100644 --- a/assets/arc-0052/contextual.api.crypto.ts +++ b/assets/arc-0052/contextual.api.crypto.ts @@ -13,7 +13,6 @@ import { import * as msgpack from "algo-msgpack-with-bigint" import Ajv from "ajv" import { deriveChildNodePrivate, fromSeed } from './bip32-ed25519'; -import { Transaction, EncodedSignedTransaction } from 'algosdk'; /** @@ -198,31 +197,20 @@ export class ContextualCryptoApi { * - 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, + * @param prefixEncodedTx + * - Encoded transaction object * * @returns stx * - EncodedSignedTransaction object */ - async signAlgoTransaction(context: KeyContext, account: number, keyIndex: number, tx: Transaction): Promise { + async signAlgoTransaction(context: KeyContext, account: number, keyIndex: number, prefixEncodedTx: Uint8Array): Promise { await ready // libsodium - const prefixEncodedTx: Uint8Array = new Uint8Array(tx.bytesToSign()) - const pk = await this.keyGen(context, account, keyIndex) - const bip44Path: number[] = GetBIP44PathFromContext(context, account, keyIndex) const sig = await this.rawSign(bip44Path, prefixEncodedTx) - const stx: EncodedSignedTransaction = { - sig: Buffer.from(sig), - txn: tx.get_obj_for_encoding() - } - - if (pk !== tx.from.publicKey) { - stx.sgnr = Buffer.from(pk) - } - return stx + return sig } diff --git a/assets/arc-0052/package.json b/assets/arc-0052/package.json index 44b1c6834..fd635513f 100644 --- a/assets/arc-0052/package.json +++ b/assets/arc-0052/package.json @@ -21,7 +21,6 @@ "dependencies": { "ajv": "^8.12.0", "algo-msgpack-with-bigint": "^2.1.1", - "algosdk": "^2.7.0", "bip32-ed25519": "^0.0.4", "bip39": "^3.1.0", "bn.js": "^5.2.1", diff --git a/assets/arc-0052/yarn.lock b/assets/arc-0052/yarn.lock index 96d4ce8a1..c7e211fbd 100644 --- a/assets/arc-0052/yarn.lock +++ b/assets/arc-0052/yarn.lock @@ -679,21 +679,6 @@ algo-msgpack-with-bigint@^2.1.1: resolved "https://registry.yarnpkg.com/algo-msgpack-with-bigint/-/algo-msgpack-with-bigint-2.1.1.tgz#38bb717220525b3ff42232eefdcd9efb9ad405d6" integrity sha512-F1tGh056XczEaEAqu7s+hlZUDWwOBT70Eq0lfMpBP2YguSQVyxRbprLq5rELXKQOyOaixTWYhMeMQMzP0U5FoQ== -algosdk@^2.7.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/algosdk/-/algosdk-2.7.0.tgz#6ebab130d25fc3cb4c74dce4d8753c7e86db1404" - integrity sha512-sBE9lpV7bup3rZ+q2j3JQaFAE9JwZvjWKX00vPlG8e9txctXbgLL56jZhSWZndqhDI9oI+0P4NldkuQIWdrUyg== - dependencies: - algo-msgpack-with-bigint "^2.1.1" - buffer "^6.0.3" - hi-base32 "^0.5.1" - js-sha256 "^0.9.0" - js-sha3 "^0.8.0" - js-sha512 "^0.8.0" - json-bigint "^1.0.0" - tweetnacl "^1.0.3" - vlq "^2.0.4" - ansi-escapes@^4.2.1: version "4.3.2" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" @@ -805,16 +790,6 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base64-js@^1.3.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - -bignumber.js@^9.0.0: - version "9.1.2" - resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c" - integrity sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug== - bip32-ed25519@^0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/bip32-ed25519/-/bip32-ed25519-0.0.4.tgz#218943e212c2d3152dfd6f3a929305e3fe86534c" @@ -890,14 +865,6 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== -buffer@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" - integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.2.1" - callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -1276,11 +1243,6 @@ human-signals@^2.1.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== -ieee754@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - import-local@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" @@ -1755,16 +1717,6 @@ jest@^29.7.0: import-local "^3.0.2" jest-cli "^29.7.0" -js-sha256@^0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.9.0.tgz#0b89ac166583e91ef9123644bd3c5334ce9d0966" - integrity sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA== - -js-sha3@^0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" - integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== - js-sha512@^0.8.0: version "0.8.0" resolved "https://registry.yarnpkg.com/js-sha512/-/js-sha512-0.8.0.tgz#dd22db8d02756faccf19f218e3ed61ec8249f7d4" @@ -1788,13 +1740,6 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== -json-bigint@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1" - integrity sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ== - dependencies: - bignumber.js "^9.0.0" - json-parse-even-better-errors@^2.3.0: version "2.3.1" resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" @@ -2337,11 +2282,6 @@ v8-to-istanbul@^9.0.1: "@types/istanbul-lib-coverage" "^2.0.1" convert-source-map "^2.0.0" -vlq@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/vlq/-/vlq-2.0.4.tgz#6057b85729245b9829e3cc7755f95b228d4fe041" - integrity sha512-aodjPa2wPQFkra1G8CzJBTHXhgk3EVSwxSWXNPr1fgdFLUb8kvLV1iEb6rFgasIsjP82HWI6dsb5Io26DDnasA== - walker@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" From 421089f0c7534fce07ed77f83f91cdb1dc0cd8ea Mon Sep 17 00:00:00 2001 From: ehanoc Date: Wed, 27 Mar 2024 14:55:37 +0000 Subject: [PATCH 36/47] Update assets/arc-0052/contextual.api.crypto.ts Co-authored-by: ori-shem-tov <45538125+ori-shem-tov@users.noreply.github.com> --- assets/arc-0052/contextual.api.crypto.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/arc-0052/contextual.api.crypto.ts b/assets/arc-0052/contextual.api.crypto.ts index a21c057bc..e895e64b5 100644 --- a/assets/arc-0052/contextual.api.crypto.ts +++ b/assets/arc-0052/contextual.api.crypto.ts @@ -201,7 +201,7 @@ export class ContextualCryptoApi { * - Encoded transaction object * * @returns stx - * - EncodedSignedTransaction object + * - Raw bytes signature */ async signAlgoTransaction(context: KeyContext, account: number, keyIndex: number, prefixEncodedTx: Uint8Array): Promise { await ready // libsodium From 43aa9c0e30fff39385c801eb26168fde3fe32ee5 Mon Sep 17 00:00:00 2001 From: ehanoc Date: Wed, 27 Mar 2024 14:55:48 +0000 Subject: [PATCH 37/47] Update assets/arc-0052/contextual.api.crypto.ts Co-authored-by: ori-shem-tov <45538125+ori-shem-tov@users.noreply.github.com> --- assets/arc-0052/contextual.api.crypto.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/arc-0052/contextual.api.crypto.ts b/assets/arc-0052/contextual.api.crypto.ts index e895e64b5..49ca39c80 100644 --- a/assets/arc-0052/contextual.api.crypto.ts +++ b/assets/arc-0052/contextual.api.crypto.ts @@ -200,7 +200,7 @@ export class ContextualCryptoApi { * @param prefixEncodedTx * - Encoded transaction object * - * @returns stx + * @returns sig * - Raw bytes signature */ async signAlgoTransaction(context: KeyContext, account: number, keyIndex: number, prefixEncodedTx: Uint8Array): Promise { From ced1c70276a089a8ab97541c23465fdbcdae6c5f Mon Sep 17 00:00:00 2001 From: ehanoc Date: Wed, 24 Apr 2024 19:31:51 +0100 Subject: [PATCH 38/47] feat: support to Peikerts bip32ed25519 ammendent to derivations Signed-off-by: ehanoc --- assets/arc-0052/bip32-ed25519.ts | 115 ++++++++++++++---- assets/arc-0052/contextual.api.crypto.spec.ts | 58 +++++---- assets/arc-0052/contextual.api.crypto.ts | 40 +++--- 3 files changed, 144 insertions(+), 69 deletions(-) diff --git a/assets/arc-0052/bip32-ed25519.ts b/assets/arc-0052/bip32-ed25519.ts index d6d471df3..1fedaf035 100644 --- a/assets/arc-0052/bip32-ed25519.ts +++ b/assets/arc-0052/bip32-ed25519.ts @@ -42,6 +42,36 @@ export function fromSeed(seed: Buffer): Uint8Array { return new Uint8Array(Buffer.concat([kL, kR, c])); } +/** + * This function takes an array of up to 256 bits and sets the last g trailing bits to zero + * + * @param array - An array of up to 256 bits + * @param g - The number of bits to zero + * @returns - The array with the last g bits set to zero + */ +export function trunc_256_minus_g_bits(array: Uint8Array, g: number): Uint8Array { + if (g < 0 || g > 256) { + throw new Error("Number of bits to zero must be between 0 and 256."); + } + + let remainingBits = g; + + // Start from the last byte and move backward + for (let i = array.length - 1; i >= 0 && remainingBits > 0; i--) { + if (remainingBits >= 8) { + // If more than 8 bits remain to be zeroed, zero the entire byte + array[i] = 0; + remainingBits -= 8; + } else { + // Zero only the required bits in the last affected byte + array[i] &= ~(0xFF >> (8 - remainingBits)); // Mask to zero only the last remainingBits + break; + } + } + + return array +} + /** * @see section V. BIP32-Ed25519: Specification; * @@ -53,38 +83,62 @@ export function fromSeed(seed: Buffer): Uint8Array { * * @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 + * @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 - (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 */ export function deriveChildNodePrivate( extendedKey: Uint8Array, - index: number + index: number, + g: number = 9 ): Uint8Array { - const kl: Buffer = Buffer.from(extendedKey.subarray(0, 32)); - const kr: Buffer = Buffer.from(extendedKey.subarray(32, 64)); + const kL: Buffer = Buffer.from(extendedKey.subarray(0, 32)); + const kR: Buffer = Buffer.from(extendedKey.subarray(32, 64)); const cc: Uint8Array = extendedKey.subarray(64, 96); - const { z, childChainCode } = index < 0x80000000 ? derivedNonHardened(kl, cc, index) : deriveHardened(kl, kr, cc, index); + const { z, childChainCode } = index < 0x80000000 ? derivedNonHardened(kL, cc, index) : deriveHardened(kL, kR, cc, index); const chainCode = childChainCode.subarray(32, 64); - const zl = z.subarray(0, 32); - const zr = z.subarray(32, 64); - - // left = kl + 8 * trunc28(zl) - // right = zr + kr - - const left = new BN(kl, 16, "le").add(new BN(zl.subarray(0, 28), 16, "le").mul(new BN(8))).toArrayLike(Buffer, "le", 32); - let right = new BN(kr, 16, "le").add(new BN(zr, 16, "le")).toArrayLike(Buffer, "le").slice(0, 32); - - + const zLeft = z.subarray(0, 32); // 32 bytes + const zRight = z.subarray(32, 64); + + // ###################################### + // Standard BIP32-ed25519 derivation + // ####################################### + // zL = kl + 8 * trunc_keep_28_bytes (z_left_hand_side) + // zR = zr + kr + + // ###################################### + // Chris Peikert's ammendment to BIP32-ed25519 derivation + // ####################################### + // zL = kl + 8 * trunc_256_minus_g_bits (z_left_hand_side, g) + // Needs to satisfy g >= d + 6 + // + // D = 2 ^ d , D is the maximum levels of BIP32 derivations to ensure a more secure key derivation + + + // Picking g == 9 && d == 3 + // 256 - 9 == 247 bits (30 bytes + leftover) + // D = 2 ^ 3 == 8 Max Levels of derivations (Although we only need 5 due to BIP44) + + // making sure + // g == 9 >= 3 + 6 + + const zL: Uint8Array = trunc_256_minus_g_bits(zLeft, g); + + // zL = kL + 8 * truncated(z_left_hand_side) + // Big Integers + little Endianess + const klBigNum = new BN(kL, 16, "le") + const big8 = new BN(8); + const zlBigNum = new BN(zL, 16, "le"); + + const left = klBigNum.add(zlBigNum.mul(big8)).toArrayLike(Buffer, "le", 32); + + let right = new BN(kR, 16, "le").add(new BN(zRight, 16, "le")).toArrayLike(Buffer, "le").slice(0, 32); + const rightBuffer = Buffer.alloc(32); - Buffer.from(right).copy(rightBuffer, 0, 0, right.length) - - - // just padding - // if (right.length !== 32) { - // right = Buffer.from(right.toString("hex") + "00", "hex"); - // } + Buffer.from(right).copy(rightBuffer, 0, 0, right.length) // padding with zeros if needed + // return (kL, kR, c) return Buffer.concat([left, rightBuffer, chainCode]); } @@ -97,9 +151,10 @@ export function deriveChildNodePrivate( * * @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 */ -export function deriveChildNodePublic(extendedKey: Uint8Array, index: number): Uint8Array { +export function deriveChildNodePublic(extendedKey: Uint8Array, index: number, g: number = 9): Uint8Array { if (index > 0x80000000) throw new Error('can not derive public key with harden') const pk: Buffer = Buffer.from(extendedKey.subarray(0, 32)) @@ -118,10 +173,20 @@ export function deriveChildNodePublic(extendedKey: Uint8Array, index: number): U // Section V. BIP32-Ed25519: Specification; subsection D) Public Child Key Derivation const chainCode: Buffer = i.subarray(32, 64); - const zl: Buffer = z.subarray(0, 32); + + const zL: Uint8Array = trunc_256_minus_g_bits(z.subarray(0, 32), g) - // left = 8 * 28bytesOf(zl) - const left = new BN(zl.subarray(0, 28), 16, 'le').mul(new BN(8)).toArrayLike(Buffer, 'le', 32); + // ###################################### + // 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) + + const left = new BN(zL, 16, 'le').mul(new BN(8)).toArrayLike(Buffer, 'le', 32); const p: Uint8Array = crypto_scalarmult_ed25519_base_noclamp(left); return Buffer.concat([crypto_core_ed25519_add(p, pk), chainCode]); diff --git a/assets/arc-0052/contextual.api.crypto.spec.ts b/assets/arc-0052/contextual.api.crypto.spec.ts index 6ef546bbe..39af79298 100644 --- a/assets/arc-0052/contextual.api.crypto.spec.ts +++ b/assets/arc-0052/contextual.api.crypto.spec.ts @@ -1,7 +1,7 @@ import { CryptoKX, KeyPair, crypto_kx_client_session_keys, crypto_kx_server_session_keys, crypto_scalarmult, crypto_scalarmult_ed25519_base_noclamp, crypto_secretbox_NONCEBYTES, crypto_secretbox_easy, crypto_secretbox_open_easy, crypto_sign_ed25519_pk_to_curve25519, crypto_sign_ed25519_sk_to_curve25519, crypto_sign_keypair, ready, to_base64 } from "libsodium-wrappers-sumo" import * as bip39 from "bip39" import { randomBytes } from "crypto" -import { ContextualCryptoApi, ERROR_TAGS_FOUND, Encoding, KeyContext, SignMetadata, harden } from "./contextual.api.crypto" +import { BIP32DerivationType, ContextualCryptoApi, ERROR_TAGS_FOUND, Encoding, KeyContext, SignMetadata, harden } from "./contextual.api.crypto" import * as msgpack from "algo-msgpack-with-bigint" import { fromSeed } from "./bip32-ed25519" import { sha512_256 } from "js-sha512" @@ -49,10 +49,12 @@ describe("Contextual Derivation & Signing", () => { afterEach(() => {}) + // Skipping because we are not diverging from standard BIP32-Ed25519 implementation as we are doing improvements + // Keeping tests so we can always go back and check for correctness describe("\(JS Library) Reference Implementation alignment with known BIP32-Ed25519 JS LIB", () => { it("\(OK) BIP32-Ed25519 derive key m'/44'/283'/0'/0/0", async () => { await ready - const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 0) + const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 0, BIP32DerivationType.Khovratovich) const rooKey: Uint8Array = fromSeed(seed) @@ -69,7 +71,7 @@ describe("Contextual Derivation & Signing", () => { it("\(OK) BIP32-Ed25519 derive key m'/44'/283'/0'/0/1", async () => { await ready - const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 1) + const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 1, BIP32DerivationType.Khovratovich) const rooKey: Uint8Array = fromSeed(seed) @@ -86,7 +88,7 @@ describe("Contextual Derivation & Signing", () => { it("\(OK) BIP32-Ed25519 derive PUBLIC key m'/44'/283'/1'/0/1", async () => { await ready - const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 1, 1) + const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 1, 1, BIP32DerivationType.Khovratovich) const rooKey: Uint8Array = fromSeed(seed) @@ -111,7 +113,7 @@ describe("Contextual Derivation & Signing", () => { it("\(OK) BIP32-Ed25519 derive PUBLIC key m'/44'/0'/1'/0/2", async () => { await ready - const key: Uint8Array = await cryptoService.keyGen(KeyContext.Identity, 1, 2) + const key: Uint8Array = await cryptoService.keyGen(KeyContext.Identity, 1, 2, BIP32DerivationType.Khovratovich) const rooKey: Uint8Array = fromSeed(seed) @@ -146,66 +148,67 @@ describe("Contextual Derivation & Signing", () => { describe("Soft Derivations", () => { it("\(OK) Derive m'/44'/283'/0'/0/0 Algorand Address Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 0) - expect(key).toEqual(new Uint8Array(Buffer.from("62fe832b7ad10544be8337a670435e5064ae4a66e77bd78909765b46b576a6f3", "hex"))) + expect(key).toEqual(new Uint8Array(Buffer.from("8ad0bbc42326ac64eb4dbbe40a77518a7fc1d39504b618a4dc85f03b3a921a02", "hex"))) }) it("\(OK) Derive m'/44'/283'/0'/0/1 Algorand Address Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 1) - expect(key).toEqual(new Uint8Array(Buffer.from("530461002eaccec0c7b5795925aa104a7fb45f85ef0aa95bbb5be93b6f8537ad", "hex"))) + + expect(key).toEqual(new Uint8Array(Buffer.from("2d3f9e31232bd36e6c0f37597e19c4c0154e58c41bc2b737c7700b683e85d0af", "hex"))) }) - + it("\(OK) Derive m'/44'/283'/0'/0/2 Algorand Address Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 2) - expect(key).toEqual(new Uint8Array(Buffer.from("2281c81bee04ee039fa482c283541c6ab06c8324db6f1cc59c68252e1d58bcb3", "hex"))) + expect(key).toEqual(new Uint8Array(Buffer.from("96acc17f0c34f6c640d5466988ce59c4da5423b5ec233b7ad2e5c5a3b1b80782", "hex"))) }) - + }) - + describe("Hard Derivations", () => { it("\(OK) Derive m'/44'/283'/1'/0/0 Algorand Address Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 1, 0) - expect(key).toEqual(new Uint8Array(Buffer.from("9e12643f6c0068dcf53b04daced6f8c1a90ad21c954a66df4140d79303166a67", "hex"))) + expect(key).toEqual(new Uint8Array(Buffer.from("fd56577456794efb91e05dc947d26d4864b346d139dfa8fff9b0e1def84b9078", "hex"))) }) - + it("\(OK) Derive m'/44'/283'/2'/0/1 Algorand Address Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 2, 1) - expect(key).toEqual(new Uint8Array(Buffer.from("8a5ddf62d51a2c50e51dbad4634356cc72314a81edd917ac91da96477a9fb5b0", "hex"))) + expect(key).toEqual(new Uint8Array(Buffer.from("aa03d62057744f4422d70c3a421deae838d8f7546a15f2ada59287569911144c", "hex"))) }) - + it("\(OK) Derive m'/44'/283'/3'/0/0 Algorand Address Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 3, 0) - expect(key).toEqual(new Uint8Array(Buffer.from("2358e0f2b465ab3e8f55139d8316654d4be39ebb22367d36409fd02a20b0e017", "hex"))) + expect(key).toEqual(new Uint8Array(Buffer.from("303718c23846fdd0f7d9cded69d95c5a72fbb1ccbbea50c865c00c050bb0e68b", "hex"))) }) }) }) - + describe("Identities", () => { describe("Soft Derivations", () => { it("\(OK) Derive m'/44'/0'/0'/0/0 Identity Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Identity, 0, 0) - expect(key).toEqual(new Uint8Array(Buffer.from("b6d7eea5af0ad83edf4340659e72f0ea2b4566de1fc3b63a40a425aabebe5e49", "hex"))) + expect(key).toEqual(new Uint8Array(Buffer.from("844cda69c4ef7c212befaa6733f5e3c0317fc173cb9f14c6cf66a48263e722ec", "hex"))) }) - + it("\(OK) Derive m'/44'/0'/0'/0/1 Identity Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Identity, 0, 1) - expect(key).toEqual(new Uint8Array(Buffer.from("b5cec676c5a2129ed1be4223a2702439bbb2462fd77b43f27e2f79fd194a30a2", "hex"))) + expect(key).toEqual(new Uint8Array(Buffer.from("a8c6de4e6d2672ad5a804994cf6e481ea7c2c3b1cedc5f51c63b2d0819d503f0", "hex"))) }) - + it("\(OK) Derive m'/44'/0'/0'/0/2 Identity Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Identity, 0, 2) - expect(key).toEqual(new Uint8Array(Buffer.from("435e5e3446431d462572abee1b8badb88608906a6af27b8497bccfd503edb6fe", "hex"))) + expect(key).toEqual(new Uint8Array(Buffer.from("88e493675894f0ba8472037da40a61a7ed356fd0f24c312a1ec9bb7c052f5d8c", "hex"))) }) }) - + describe("Hard Derivations", () => { it("\(OK) Derive m'/44'/0'/1'/0/0 Identity Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Identity, 1, 0) - expect(key).toEqual(new Uint8Array(Buffer.from("bf63be83fff9bc9d0aebc231d50342110e5220247e50de376b47e154b5d32a3e", "hex"))) + expect(key).toEqual(new Uint8Array(Buffer.from("c120ac324985009294f2546463f922a55cd93fb1d209929828d2a952c4581760", "hex"))) }) - + it("\(OK) Derive m'/44'/0'/2'/0/1 Identity Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Identity, 2, 1) - expect(key).toEqual(new Uint8Array(Buffer.from("edb10fff24a4745df52f1a0ab1ae71b3752d019c8c2437d46ab8c8e634a74cd4", "hex"))) + expect(key).toEqual(new Uint8Array(Buffer.from("82ccd181c1d533828a77218bdff0329966dc506084d5b87a0f2eb6678e53cb21", "hex"))) }) }) }) @@ -386,7 +389,8 @@ describe("Contextual Derivation & Signing", () => { // encrypt const cipherText: Uint8Array = crypto_secretbox_easy(message, nonce, aliceSharedSecret) - expect(cipherText).toEqual(new Uint8Array([251,7,48,58,57,22,135,152,150,116,242,138,26,155,136,252,163,209,7,34,125,135,218,222,102,45,250,55,34])) + + expect(cipherText).toEqual(new Uint8Array([158, 107, 5, 77, 134, 215, 212, 90, 192, 34, 131, 39, 160, 252, 74, 194, 129, 54, 249, 113, 128, 241, 213, 244, 98, 55, 46, 233, 7])) // decrypt const plainText: Uint8Array = crypto_secretbox_open_easy(cipherText, nonce, bobSharedSecret) expect(plainText).toEqual(message) diff --git a/assets/arc-0052/contextual.api.crypto.ts b/assets/arc-0052/contextual.api.crypto.ts index 495f32588..87384c389 100644 --- a/assets/arc-0052/contextual.api.crypto.ts +++ b/assets/arc-0052/contextual.api.crypto.ts @@ -27,6 +27,13 @@ export enum KeyContext { TESTVECTOR_3 = 5 } +export enum BIP32DerivationType { + // standard Ed25519 bip32 derivations based of: https://acrobat.adobe.com/id/urn:aaid:sc:EU:04fe29b0-ea1a-478b-a886-9bb558a5242a + Khovratovich = 0, + // Derivations based on Peikert's ammendments to the original BIP32-Ed25519 + Peikert = 1 +} + export interface ChannelKeys { tx: Uint8Array rx: Uint8Array @@ -76,11 +83,15 @@ export class ContextualCryptoApi { * @param isPrivate - if true, return the private key, otherwise return the public key * @returns - The public key of 32 bytes. If isPrivate is true, returns the private key instead. */ - private async deriveKey(rootKey: Uint8Array, bip44Path: number[], isPrivate: boolean = true): Promise { - let derived: Uint8Array = deriveChildNodePrivate(Buffer.from(rootKey), bip44Path[0]) - derived = deriveChildNodePrivate(derived, bip44Path[1]) - derived = deriveChildNodePrivate(derived, bip44Path[2]) - derived = deriveChildNodePrivate(derived, bip44Path[3]) + private async deriveKey(rootKey: Uint8Array, bip44Path: number[], isPrivate: boolean = true, derivationType: BIP32DerivationType): Promise { + + // Pick `g`, which is amount of bits zeroed from each derived node + const g: number = derivationType === BIP32DerivationType.Peikert ? 9 : 32 + + let derived: Uint8Array = deriveChildNodePrivate(Buffer.from(rootKey), bip44Path[0], g) + derived = deriveChildNodePrivate(derived, bip44Path[1], g) + derived = deriveChildNodePrivate(derived, bip44Path[2], g) + derived = deriveChildNodePrivate(derived, bip44Path[3], g) // Public Key SOFT derivations are possible without using the private key of the parent node // Could be an implementation choice. @@ -93,7 +104,7 @@ export class ContextualCryptoApi { // const extPub: Uint8Array = new Uint8Array(Buffer.concat([nodePublic, nodeCC])) // const publicKey: Uint8Array = deriveChildNodePublic(extPub, bip44Path[4]).subarray(0, 32)) - derived = deriveChildNodePrivate(derived, bip44Path[4]) + derived = deriveChildNodePrivate(derived, bip44Path[4], g) // const scalar = derived.subarray(0, 32) // scalar == pvtKey return isPrivate ? derived : crypto_scalarmult_ed25519_base_noclamp(derived.subarray(0, 32)) @@ -107,13 +118,13 @@ export class ContextualCryptoApi { * @param keyIndex - key index. This value will be a SOFT derivation as part of BIP44. * @returns - public key 32 bytes */ - async keyGen(context: KeyContext, account:number, keyIndex: number): Promise { + async keyGen(context: KeyContext, account:number, keyIndex: number, derivationType: BIP32DerivationType = BIP32DerivationType.Peikert): Promise { await ready // libsodium const rootKey: Uint8Array = fromSeed(this.seed) const bip44Path: number[] = GetBIP44PathFromContext(context, account, keyIndex) - return await this.deriveKey(rootKey, bip44Path, false) + return await this.deriveKey(rootKey, bip44Path, false, derivationType) } /** @@ -129,7 +140,7 @@ export class ContextualCryptoApi { * * @returns - signature holding R and S, totally 64 bytes * */ - async signData(context: KeyContext, account: number, keyIndex: number, data: Uint8Array, metadata: SignMetadata): Promise { + async signData(context: KeyContext, account: number, keyIndex: number, data: Uint8Array, metadata: SignMetadata, derivationType: BIP32DerivationType = BIP32DerivationType.Peikert): Promise { // validate data const result: boolean | Error = this.validateData(data, metadata) @@ -145,7 +156,7 @@ export class ContextualCryptoApi { const rootKey: Uint8Array = fromSeed(this.seed) const bip44Path: number[] = GetBIP44PathFromContext(context, account, keyIndex) - const raw: Uint8Array = await this.deriveKey(rootKey, bip44Path, true) + const raw: Uint8Array = await this.deriveKey(rootKey, bip44Path, true, derivationType) const scalar: Uint8Array = raw.slice(0, 32); const c: Uint8Array = raw.slice(32, 64); @@ -267,13 +278,13 @@ export class ContextualCryptoApi { * @param meFirst - defines the order in which the keys will be considered for the shared secret. If true, our key will be used first, otherwise the other party's key will be used first * @returns - raw 32 bytes shared secret */ - async ECDH(context: KeyContext, account: number, keyIndex: number, otherPartyPub: Uint8Array, meFirst: boolean): Promise { + async ECDH(context: KeyContext, account: number, keyIndex: number, otherPartyPub: Uint8Array, meFirst: boolean, derivationType: BIP32DerivationType = BIP32DerivationType.Peikert): Promise { await ready const rootKey: Uint8Array = fromSeed(this.seed) const bip44Path: number[] = GetBIP44PathFromContext(context, account, keyIndex) - const childKey: Uint8Array = await this.deriveKey(rootKey, bip44Path, true) + const childKey: Uint8Array = await this.deriveKey(rootKey, bip44Path, true, derivationType) const scalar: Uint8Array = childKey.slice(0, 32) @@ -295,11 +306,6 @@ export class ContextualCryptoApi { } - // check for adversarial public keys - // scalar was being clamped? - // dedicated keys for ECDH? - - return crypto_generichash(32, new Uint8Array(concatenation)) } } \ No newline at end of file From 1b516876af977e5d10507204c8c04e03efa7b0ac Mon Sep 17 00:00:00 2001 From: ehanoc Date: Thu, 25 Apr 2024 10:58:41 +0100 Subject: [PATCH 39/47] doc: Update README with new test vectors and details Signed-off-by: ehanoc --- assets/arc-0052/README.md | 47 +++++++++++-------- assets/arc-0052/contextual.api.crypto.spec.ts | 6 +-- assets/arc-0052/contextual.api.crypto.ts | 2 + 3 files changed, 32 insertions(+), 23 deletions(-) diff --git a/assets/arc-0052/README.md b/assets/arc-0052/README.md index 3761084c9..9d315d78c 100644 --- a/assets/arc-0052/README.md +++ b/assets/arc-0052/README.md @@ -4,7 +4,14 @@ This implementation is not meant to be used in production. It is a reference imp It shows how the ARC52 specification can be implemented on top of a BIP39 / BIP32-ed25519 / BIP44 wallet. -note: Bip32-ed25519 is meant to be compatible with our Ledger implementation. Meaning that we only do hard-derivation at the level of the `account` as per BIP44 +The implementation is based on the [BIP32-ed25519](https://acrobat.adobe.com/id/urn:aaid:sc:EU:04fe29b0-ea1a-478b-a886-9bb558a5242a) specification. + +## Variants + +It offers 2 modes to derive keys. + +- Khovratovich; Standard mode according to the paper above. +- Peikert's: Ammendment to the standard mode to allow for a more secure derivation of keys by giving more entropy to `zL`. This is the **default** mode of this library ## Run @@ -70,38 +77,38 @@ Public key = kl * Ed25519GeneratorPoint - `BIP39 mnemonic`: _salon zoo engage submit smile frost later decide wing sight chaos renew lizard rely canal coral scene hobby scare step bus leaf tobacco slice_ -- `root key (hex)`: a8ba80028922d9fcfa055c78aede55b5c575bcd8d5a53168edf45f36d9ec8f4694592b4bc892907583e22669ecdf1b0409a9f3bd5549f2dd751b51360909cd05b4b67277d74d4ddb3688daeeb02075482ceb812db8a5757c9e792d14ec791554 +- `root key (hex)`: a8ba80028922d9fcfa055c78aede55b5c575bcd8d5a53168edf45f36d9ec8f4694592b4bc892907583e22669ecdf1b0409a9f3bd5549f2dd751b51360909cd05796b9206ec30e142e94b790a98805bf999042b55046963174ee6cee2d0375946 ### BIP44 paths #### Child Private Derivation -- `m'/44'/283'/0'/0/0`: 70db1fe0dd722955f44b57b26eee09ac282172559eb2bc6b408f69e2e9ec8f461b8fbc75f0e2a9f454d7de35a812c97fa08b984df342389f4d1f9aec5637e67468c11d826d16f7f505d27bd52c314caea79a7b9b6e87f5aa7e20f6cf06ac51f9 - - corresponding public key: 7915e7ecbaad1dc9bc22a9e496686687f1a8cb4895b7ca46f86d64dd56c6cd97 (kl * Ed25519GeneratorPoint) +- `m'/44'/283'/0'/0/0`: 70982049eeea743cbd4139fc198be4f277ece99188be5834aeb3a97ac2c53d5a79ef3bc0121991bc02eb52c99055dff273348b157ee21ab6c03d4632bd6ba2ff7755309210496c3415d40372d94abd8a831906a30f57247a8c4aa101b204ba94 + - corresponding public key: 8ad0bbc42326ac64eb4dbbe40a77518a7fc1d39504b618a4dc85f03b3a921a02 (kl * Ed25519GeneratorPoint) -- `m'/44'/283'/0'/0/1`: 58678408f4f88d7ec700be1090940bedd584c21dc7f8b00028b69e01edec8f4645779d1c90ecc5168945024e4201552a349a825518badb8d4b017bf1dcce4dac910cb8dbcd10680c7754ca101089f4c9da61a2dff79e0301d9a1fb7656fb8c73 - - public key: 054a6881d8809c348a402d67ba2feedcd8e3145f40f21a6bbd0de09c30c78d0a +- `m'/44'/283'/0'/0/1`: e8a8ca7ee58ddfcecaff18a2adb2fbe691bcd20b618c9fc32e8950d074ad3c59f7303f20f0054a91996bb5cec26e36a4cc1da352762a276a73e61843e97a5b24ad172bfd9435e6b0bb42bbe5fbded4220ccb14d733e9aa2c75346ded752f134f + - public key: 2d3f9e31232bd36e6c0f37597e19c4c0154e58c41bc2b737c7700b683e85d0af -- `m'/44'/283'/0'/0/2`: 88ce207945dfecea41acbb2a9b4563268d3bee03ea2e199af502c500e7ec8f467b95d890e8c7e5aa79d9905f3c6794e1fc3bb9478552b7f24c751b94dd3becd76699130de29273b2ae742991b7daa0adae6fb656038b25895d4af3c85769a64d - - public key: 8cea8052cfa1fd8cec0b4fad6241a91f2edbfe9f072586f243839174e40a25ef +- `m'/44'/283'/0'/0/2`: 1885ded7f457f85c6060f44140cca497863b644d0e1662cb650e9c506688ea59644b97313410ed41acdb106512ea6600083417c1d782e5a22f18094a623f3dacdcc6a5447a10b67e8fde5b0b36a7d011c7678de0b558af725292d114b7665383 + - public key: 96acc17f0c34f6c640d5466988ce59c4da5423b5ec233b7ad2e5c5a3b1b80782 -- `m'/44'/283'/1'/0/0`: 18ada9291ae27f7415e99f4485d81493e68952c8b1af4f335cc52962ecec8f464c106ef1f56fa64b2a13631a68a6d6c9902eec52e2c6226a3ed953ae94bc8a613303bbf403f94b5eb189eed703b3985e12c6726d6b06d4ed8aac3d5b258e3c8c - - public key: 04f3ba279aa781ab4f8f79aaf6cf91e3d7ff75429064dc757001a30e10c628df +- `m'/44'/283'/1'/0/0`: 883345270edf5bd2bfdd744acd2a318b16d01a4668d7b467c14c1597658c75516e286a0311e8098e548581f315d2ac67d51661ded951349aedbd649e003794860cdfdf296711c7f40531f9dc4f1dd099b784c9a92bfbb749c8a7fe71c6f395b0 + - public key: fd56577456794efb91e05dc947d26d4864b346d139dfa8fff9b0e1def84b9078 -- `m'/44'/283'/2'/0/0`: 209dc0b3458ee1f7484189bf584c02807f1a5726168aff5ea14fc117f3ec8f46b68ea14ca84c0da34aa4990416c6da01b14fbe30c5c225238515a93a0a28a33dbecbe8e64a514d44c8c0a3775844d5aa8e18ea2182321f1ff87061e49adc4a77 - - public key: 400d78302258dc7b3cb56d1a09f85b018e8100865ced0b5cda474c26bbc07c30 +- `m'/44'/283'/2'/0/1`: c06f6219dfe978ebcbe4a4834fa57af7a9ebb92cfe966be120e98778cd600f59f3f19fc39b32ef51bb3c7344b3484c5fdcbea206e24dac0cad5da022fb18cb394863b9e03e8d5b290b82453dc0bd6fce65eafdd455df642614b7c80fbb8b067e + - public key: aa03d62057744f4422d70c3a421deae838d8f7546a15f2ada59287569911144c -- `m'/44'/283'/3'/0/0`: 3008165b92e6b29d6a3f28b593f15dcf6ce4c9a5a1aadc3a43ca068ce7ec8f46b8251db83f68dedaee1054ec545257f95ba00e33566d99926bef8743b77b42b8867472d1f1886c88ec36991f14a333454003236b5375d4a8bf4f01b2ff85ec9d - - public key: cf8d28a3d41bc656acbfeadb64d06054142c97bee6a987c11d934f84853df866 +- `m'/44'/283'/3'/0/0`: 48a9ed4203303292926a208811a19fa3fbd6480e92c03327d2e43b2596015e5e0dc91a410e5b9ddd2bef2008a702b54ff1ba58c698bb0271f047dcd2617c35024b36efc42fc48c932a6eeff1625e58382f302f4b3f069675e5ca8efc88e2f176 + - public key: 303718c23846fdd0f7d9cded69d95c5a72fbb1ccbbea50c865c00c050bb0e68b -- `m'/44'/0'/0'/0/0`: 08b10dfcfc37a0cc998ec3305c6c32d4412de73f3f9633342248d14aeeec8f461997b1136524a5d3499d933c6f6e739d38fbf7404a183f4d835c6fe105cf44d016634694e4087b2d547c8b550a0f053d2fba990ba7e5e58186963393994d06a9 - - public key: 28804f08d8c145e172c998fe75058237b8181846ca763894ae3eefea6ab88352 +- `m'/44'/0'/0'/0/0`: 589478bedb7983b1de5926129223d21e1628f12ac018caff5942c9bb8b956557529ca2ed4e97b945ec5325d5f456ebaa537c557adb3767f2b749582a46dfc1ce7826ab1d98f7bb2ab9a6004f4c6aa1b1360bfd95a8748c38c90ec906bab9acb4 + - public key: 844cda69c4ef7c212befaa6733f5e3c0317fc173cb9f14c6cf66a48263e722ec -- `m'/44'/0'/0'/0/1`: 80d6dd1cefb42fc93187739ad102e5dfd533b54b847b0693661bfee9edec8f466cab918344a8e7e9ae196d94cce301aa1d7360f550b25f3dc7468a006e423e3de80e43cd2588a9996b8156d3dc233ca31470f7d49261edd2e13d8c4c0096163c - - public key: fc8d1c79edd406fa415cb0a76435eb83b6f8af72cd9bd673753471470205057a +- `m'/44'/0'/0'/0/1`: 68094a077a8e025766c4456f306f91fcada3098b09993e45b4eb0fde191b955c1d709e0875931b98cbd045972e6d38f76ca49295cee24385d3eee8e5350db31d0e84012a142d233514f178b55b5b6b63dafaae9ceebe7d2bcc8872740213bbe9 + - public key: a8c6de4e6d2672ad5a804994cf6e481ea7c2c3b1cedc5f51c63b2d0819d503f0 -- `m'/44'/0'/0'/0/2`: 7847139bee38330c809a56f3d2261bfbd7da7c1c88999c38bb12175cefec8f467617b7dba8330b644eae1c24a2d6b7211f5f36d9919d9a240777fa32d382feb3debfe21afa0de5021342f3bfafe18a91e11c441ab98ff5bcbba2dfba3190ce6f - - public key: f7e317899420454886fe79f24e25af0bbb1856c440b14829674015e5fc2ad28a +- `m'/44'/0'/0'/0/2`: 20c050bce9e69a37bbad8bf50ec7c8c54a3a34e9cdfec902d477a32a20b543572629e59fd3aa7d284396668e891d2d8476b43d370811aaf56194c36bdc8bc30e8211880e939e0cbe6a252b828fc7faf46eec236ef967ebdf115d380194a93bd3 + - public key: 88e493675894f0ba8472037da40a61a7ed356fd0f24c312a1ec9bb7c052f5d8c diff --git a/assets/arc-0052/contextual.api.crypto.spec.ts b/assets/arc-0052/contextual.api.crypto.spec.ts index a0fb3a31c..44103c52b 100644 --- a/assets/arc-0052/contextual.api.crypto.spec.ts +++ b/assets/arc-0052/contextual.api.crypto.spec.ts @@ -151,10 +151,9 @@ describe("Contextual Derivation & Signing", () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 0) expect(key).toEqual(new Uint8Array(Buffer.from("8ad0bbc42326ac64eb4dbbe40a77518a7fc1d39504b618a4dc85f03b3a921a02", "hex"))) }) - + it("\(OK) Derive m'/44'/283'/0'/0/1 Algorand Address Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 1) - expect(key).toEqual(new Uint8Array(Buffer.from("2d3f9e31232bd36e6c0f37597e19c4c0154e58c41bc2b737c7700b683e85d0af", "hex"))) }) @@ -195,8 +194,9 @@ describe("Contextual Derivation & Signing", () => { expect(key).toEqual(new Uint8Array(Buffer.from("a8c6de4e6d2672ad5a804994cf6e481ea7c2c3b1cedc5f51c63b2d0819d503f0", "hex"))) }) - it("\(OK) Derive m'/44'/0'/0'/0/2 Identity Key", async () => { + it.only("\(OK) Derive m'/44'/0'/0'/0/2 Identity Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Identity, 0, 2) + console.log("Key", Buffer.from(key).toString("hex")) expect(key).toEqual(new Uint8Array(Buffer.from("88e493675894f0ba8472037da40a61a7ed356fd0f24c312a1ec9bb7c052f5d8c", "hex"))) }) }) diff --git a/assets/arc-0052/contextual.api.crypto.ts b/assets/arc-0052/contextual.api.crypto.ts index 8d39bb013..5cc437e7a 100644 --- a/assets/arc-0052/contextual.api.crypto.ts +++ b/assets/arc-0052/contextual.api.crypto.ts @@ -104,6 +104,8 @@ export class ContextualCryptoApi { derived = deriveChildNodePrivate(derived, bip44Path[4], g) + console.log("derived", Buffer.from(derived).toString("hex")) + // const scalar = derived.subarray(0, 32) // scalar == pvtKey return isPrivate ? derived : crypto_scalarmult_ed25519_base_noclamp(derived.subarray(0, 32)) } From 333601ddf42ace23858c119a37661f27c9429e7e Mon Sep 17 00:00:00 2001 From: ehanoc Date: Thu, 25 Apr 2024 12:22:20 +0100 Subject: [PATCH 40/47] fix: package.json, logs, run all tests Signed-off-by: ehanoc --- assets/arc-0052/contextual.api.crypto.spec.ts | 2 +- assets/arc-0052/contextual.api.crypto.ts | 2 -- assets/arc-0052/package.json | 3 +-- assets/arc-0052/yarn.lock | 2 +- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/assets/arc-0052/contextual.api.crypto.spec.ts b/assets/arc-0052/contextual.api.crypto.spec.ts index 44103c52b..ca66bd1f8 100644 --- a/assets/arc-0052/contextual.api.crypto.spec.ts +++ b/assets/arc-0052/contextual.api.crypto.spec.ts @@ -194,7 +194,7 @@ describe("Contextual Derivation & Signing", () => { expect(key).toEqual(new Uint8Array(Buffer.from("a8c6de4e6d2672ad5a804994cf6e481ea7c2c3b1cedc5f51c63b2d0819d503f0", "hex"))) }) - it.only("\(OK) Derive m'/44'/0'/0'/0/2 Identity Key", async () => { + it("\(OK) Derive m'/44'/0'/0'/0/2 Identity Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Identity, 0, 2) console.log("Key", Buffer.from(key).toString("hex")) expect(key).toEqual(new Uint8Array(Buffer.from("88e493675894f0ba8472037da40a61a7ed356fd0f24c312a1ec9bb7c052f5d8c", "hex"))) diff --git a/assets/arc-0052/contextual.api.crypto.ts b/assets/arc-0052/contextual.api.crypto.ts index 5cc437e7a..8d39bb013 100644 --- a/assets/arc-0052/contextual.api.crypto.ts +++ b/assets/arc-0052/contextual.api.crypto.ts @@ -104,8 +104,6 @@ export class ContextualCryptoApi { derived = deriveChildNodePrivate(derived, bip44Path[4], g) - console.log("derived", Buffer.from(derived).toString("hex")) - // const scalar = derived.subarray(0, 32) // scalar == pvtKey return isPrivate ? derived : crypto_scalarmult_ed25519_base_noclamp(derived.subarray(0, 32)) } diff --git a/assets/arc-0052/package.json b/assets/arc-0052/package.json index 651b561cc..fd635513f 100644 --- a/assets/arc-0052/package.json +++ b/assets/arc-0052/package.json @@ -29,8 +29,7 @@ "js-sha512": "^0.8.0", "libsodium-wrappers-sumo": "^0.7.13", "ts-custom-error": "^3.2.0", - "ts-log": "^2.2.4", - "tweetnacl": "1.0.3" + "ts-log": "^2.2.4" }, "jest": { "moduleFileExtensions": [ diff --git a/assets/arc-0052/yarn.lock b/assets/arc-0052/yarn.lock index 8d99afd7a..56e1f4912 100644 --- a/assets/arc-0052/yarn.lock +++ b/assets/arc-0052/yarn.lock @@ -2234,7 +2234,7 @@ ts-log@^2.2.4: resolved "https://registry.yarnpkg.com/ts-log/-/ts-log-2.2.5.tgz#aef3252f1143d11047e2cb6f7cfaac7408d96623" integrity sha512-PGcnJoTBnVGy6yYNFxWVNkdcAuAMstvutN9MgDJIV6L0oG8fB+ZNNy1T+wJzah8RPGor1mZuPQkVfXNDpy9eHA== -tweetnacl@1.0.3: +tweetnacl@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596" integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw== From e008a3d4cb08804f822215add46b6e1b3d4a088a Mon Sep 17 00:00:00 2001 From: ehanoc Date: Thu, 25 Apr 2024 19:00:33 +0100 Subject: [PATCH 41/47] chore: Add public derivation tests and use cases. Check for correctness of public node derivations Signed-off-by: ehanoc --- assets/arc-0052/contextual.api.crypto.spec.ts | 57 +++++++++++++++++-- assets/arc-0052/contextual.api.crypto.ts | 43 ++++++-------- 2 files changed, 71 insertions(+), 29 deletions(-) diff --git a/assets/arc-0052/contextual.api.crypto.spec.ts b/assets/arc-0052/contextual.api.crypto.spec.ts index ca66bd1f8..80cfb2ec3 100644 --- a/assets/arc-0052/contextual.api.crypto.spec.ts +++ b/assets/arc-0052/contextual.api.crypto.spec.ts @@ -3,7 +3,7 @@ import * as bip39 from "bip39" import { randomBytes } from "crypto" import { BIP32DerivationType, ContextualCryptoApi, ERROR_TAGS_FOUND, Encoding, KeyContext, SignMetadata, harden } from "./contextual.api.crypto" import * as msgpack from "algo-msgpack-with-bigint" -import { fromSeed } from "./bip32-ed25519" +import { deriveChildNodePublic, fromSeed } from "./bip32-ed25519" import { sha512_256 } from "js-sha512" import base32 from "hi-base32" import { JSONSchemaType } from "ajv" @@ -50,8 +50,13 @@ describe("Contextual Derivation & Signing", () => { afterEach(() => {}) - // Skipping because we are not diverging from standard BIP32-Ed25519 implementation as we are doing improvements - // Keeping tests so we can always go back and check for correctness + /** + * Testing against other known bip32-ed25519 lib that complies with the BIP32-Ed25519 specification + * + * @see BIP32-ed25519 Hierarchical Deterministic Keys over a Non-linear Keyspace (https://acrobat.adobe.com/id/urn:aaid:sc:EU:04fe29b0-ea1a-478b-a886-9bb558a5242a) + * + * We call the traditional derivation Khovratovich + */ describe("\(JS Library) Reference Implementation alignment with known BIP32-Ed25519 JS LIB", () => { it("\(OK) BIP32-Ed25519 derive key m'/44'/283'/0'/0/0", async () => { await ready @@ -196,7 +201,6 @@ describe("Contextual Derivation & Signing", () => { it("\(OK) Derive m'/44'/0'/0'/0/2 Identity Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Identity, 0, 2) - console.log("Key", Buffer.from(key).toString("hex")) expect(key).toEqual(new Uint8Array(Buffer.from("88e493675894f0ba8472037da40a61a7ed356fd0f24c312a1ec9bb7c052f5d8c", "hex"))) }) }) @@ -466,4 +470,49 @@ describe("Contextual Derivation & Signing", () => { }) }) }) + + describe("\(deriveNodePublic", () => { + it("\(OK) From m'/44'/283'/0'/0 root level (excluding address index) derive N keys with only public information", async () => { + + // wallet level m'/44'/283'/0'/0 root; node derivation before address_index + const walletRoot: Uint8Array = await cryptoService.deriveKey(fromSeed(seed), [harden(44), harden(283), harden(0), 0], false, BIP32DerivationType.Peikert) + + // should be able to derive all public keys from this root without knowing private information + // since these are SOFTLY derived + const numPublicKeysToDerive: number = 10 + for (let i = 0; i < numPublicKeysToDerive; i++) { + // 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 + const derivedKey: Uint8Array = new Uint8Array(deriveChildNodePublic(walletRoot, i, BIP32DerivationType.Peikert)) // g == 9 + + // Deriving from my own wallet where i DO have private information + const myKey: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, i, BIP32DerivationType.Peikert) + + // they should match + // derivedKey.subarray(0, 32) == public key (excluding chaincode) + expect(derivedKey.slice(0, 32)).toEqual(myKey) + } + }) + + it("\(FAIL) From m'/44'/283'/0'/0' root level (excluding address index) should not be able to derive correct addresses from a hardened derivation", async () => { + + // wallet level m'/44'/283'/0'/0' root; node derivation before address_index + const walletRoot: Uint8Array = await cryptoService.deriveKey(fromSeed(seed), [harden(44), harden(283), harden(0), harden(0)], false, BIP32DerivationType.Peikert) + + + const numPublicKeysToDerive: number = 10 + for (let i = 0; i < numPublicKeysToDerive; i++) { + const derivedKey: Uint8Array = new Uint8Array(deriveChildNodePublic(walletRoot, i, BIP32DerivationType.Peikert)) // g == 9 + + // Deriving from my own wallet where i DO have private information + const myKey: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, i, BIP32DerivationType.Peikert) + + // they should NOT match since the `change` level (as part of BIP44) was hardened + // derivedKey.subarray(0, 32) == public key (excluding chaincode) + expect(derivedKey.slice(0, 32)).not.toEqual(myKey) + } + }) + }) }) diff --git a/assets/arc-0052/contextual.api.crypto.ts b/assets/arc-0052/contextual.api.crypto.ts index 8d39bb013..3c19020eb 100644 --- a/assets/arc-0052/contextual.api.crypto.ts +++ b/assets/arc-0052/contextual.api.crypto.ts @@ -28,9 +28,11 @@ export enum KeyContext { export enum BIP32DerivationType { // standard Ed25519 bip32 derivations based of: https://acrobat.adobe.com/id/urn:aaid:sc:EU:04fe29b0-ea1a-478b-a886-9bb558a5242a - Khovratovich = 0, + // Defines 32 bits to be zeroed from each derived zL + Khovratovich = 32, // Derivations based on Peikert's ammendments to the original BIP32-Ed25519 - Peikert = 1 + // Picking only 9 bits to be zeroed from each derived zL + Peikert = 9 } export interface ChannelKeys { @@ -79,33 +81,23 @@ export class ContextualCryptoApi { * @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 - if true, return the private key, otherwise return the public key - * @returns - The public key of 32 bytes. If isPrivate is true, returns the private key instead. + * @returns - The extended private key (kL, kR, chainCode) or the extended public key (pub, chainCode) */ - private async deriveKey(rootKey: Uint8Array, bip44Path: number[], isPrivate: boolean = true, derivationType: BIP32DerivationType): Promise { + async deriveKey(rootKey: Uint8Array, bip44Path: number[], isPrivate: boolean = true, derivationType: BIP32DerivationType): Promise { + await ready // libsodium // Pick `g`, which is amount of bits zeroed from each derived node const g: number = derivationType === BIP32DerivationType.Peikert ? 9 : 32 - let derived: Uint8Array = deriveChildNodePrivate(Buffer.from(rootKey), bip44Path[0], g) - derived = deriveChildNodePrivate(derived, bip44Path[1], g) - derived = deriveChildNodePrivate(derived, bip44Path[2], g) - derived = deriveChildNodePrivate(derived, bip44Path[3], g) - - // Public Key SOFT derivations are possible without using the private key of the parent node - // Could be an implementation choice. - // Example: - // const nodeScalar: Uint8Array = derived.subarray(0, 32) - // const nodePublic: Uint8Array = crypto_scalarmult_ed25519_base_noclamp(nodeScalar) - // const nodeCC: Uint8Array = derived.subarray(64, 96) - - // // [Public][ChainCode] - // const extPub: Uint8Array = new Uint8Array(Buffer.concat([nodePublic, nodeCC])) - // const publicKey: Uint8Array = deriveChildNodePublic(extPub, bip44Path[4]).subarray(0, 32)) + for (let i = 0; i < bip44Path.length; i++) { + rootKey = deriveChildNodePrivate(rootKey, bip44Path[i], g) + } - derived = deriveChildNodePrivate(derived, bip44Path[4], g) + if (isPrivate) return rootKey - // const scalar = derived.subarray(0, 32) // scalar == pvtKey - return isPrivate ? derived : crypto_scalarmult_ed25519_base_noclamp(derived.subarray(0, 32)) + // extended public key + // [public] [nodeCC] + return new Uint8Array(Buffer.concat([crypto_scalarmult_ed25519_base_noclamp(rootKey.subarray(0, 32)), rootKey.subarray(64, 96)])) } /** @@ -122,7 +114,8 @@ export class ContextualCryptoApi { const rootKey: Uint8Array = fromSeed(this.seed) const bip44Path: number[] = GetBIP44PathFromContext(context, account, keyIndex) - return await this.deriveKey(rootKey, bip44Path, false, derivationType) + const extendedKey: Uint8Array = await this.deriveKey(rootKey, bip44Path, false, derivationType) + return extendedKey.subarray(0, 32) // only public key } /** @@ -147,13 +140,13 @@ export class ContextualCryptoApi { const raw: Uint8Array = await this.deriveKey(rootKey, bip44Path, true, derivationType) const scalar: Uint8Array = raw.slice(0, 32); - const c: Uint8Array = raw.slice(32, 64); + const kR: Uint8Array = raw.slice(32, 64); // \(1): pubKey = scalar * G (base point, no clamp) const publicKey = crypto_scalarmult_ed25519_base_noclamp(scalar); // \(2): h = hash(c || msg) mod q - const r = crypto_core_ed25519_scalar_reduce(crypto_hash_sha512(Buffer.concat([c, data]))) + const r = crypto_core_ed25519_scalar_reduce(crypto_hash_sha512(Buffer.concat([kR, data]))) // \(4): R = r * G (base point, no clamp) const R = crypto_scalarmult_ed25519_base_noclamp(r) From 5b1a230a3ff829ffd9719ae048b711efc5b5a76e Mon Sep 17 00:00:00 2001 From: HashMapsData2Value <83883690+HashMapsData2Value@users.noreply.github.com> Date: Fri, 26 Apr 2024 11:27:03 +0200 Subject: [PATCH 42/47] chore: rearranged lines to align with steps in paper --- assets/arc-0052/bip32-ed25519.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/assets/arc-0052/bip32-ed25519.ts b/assets/arc-0052/bip32-ed25519.ts index 1fedaf035..d054f0962 100644 --- a/assets/arc-0052/bip32-ed25519.ts +++ b/assets/arc-0052/bip32-ed25519.ts @@ -164,31 +164,33 @@ export function deriveChildNodePublic(extendedKey: Uint8Array, index: number, g: data.writeUInt32LE(index, 1 + 32); pk.copy(data, 1); + + // Step 1: Compute Z data[0] = 0x02; - const z: Buffer = createHmac("sha512", cc).update(data).digest(); - data[0] = 0x03; - const i: Buffer = createHmac("sha512", cc).update(data).digest(); - - // Section V. BIP32-Ed25519: Specification; subsection D) Public Child Key Derivation - const chainCode: Buffer = i.subarray(32, 64); + // Step 2: Compute child public key const zL: Uint8Array = trunc_256_minus_g_bits(z.subarray(0, 32), g) - + // ###################################### // 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) const left = new BN(zL, 16, 'le').mul(new BN(8)).toArrayLike(Buffer, 'le', 32); - const p: Uint8Array = crypto_scalarmult_ed25519_base_noclamp(left); + + // Step 3: Compute child chain code + data[0] = 0x03; + const i: Buffer = createHmac("sha512", cc).update(data).digest(); + const chainCode: Buffer = i.subarray(32, 64); + return Buffer.concat([crypto_core_ed25519_add(p, pk), chainCode]); } From c068a44a22f531dd4c03d5f69622d38242b43f98 Mon Sep 17 00:00:00 2001 From: HashMapsData2Value <83883690+HashMapsData2Value@users.noreply.github.com> Date: Fri, 26 Apr 2024 11:35:13 +0200 Subject: [PATCH 43/47] chore: clarify childChainCode naming chore: clarify childChainCode naming --- assets/arc-0052/bip32-ed25519.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/assets/arc-0052/bip32-ed25519.ts b/assets/arc-0052/bip32-ed25519.ts index d054f0962..f53f5b571 100644 --- a/assets/arc-0052/bip32-ed25519.ts +++ b/assets/arc-0052/bip32-ed25519.ts @@ -95,9 +95,10 @@ export function deriveChildNodePrivate( const kR: Buffer = Buffer.from(extendedKey.subarray(32, 64)); const cc: Uint8Array = extendedKey.subarray(64, 96); + // Steps 1 & 3: Produce Z and child chain code, in accordance with hardening branching logic const { z, childChainCode } = index < 0x80000000 ? derivedNonHardened(kL, cc, index) : deriveHardened(kL, kR, cc, index); - const chainCode = childChainCode.subarray(32, 64); + // Step 2: compute child private key const zLeft = z.subarray(0, 32); // 32 bytes const zRight = z.subarray(32, 64); @@ -139,7 +140,7 @@ export function deriveChildNodePrivate( Buffer.from(right).copy(rightBuffer, 0, 0, right.length) // padding with zeros if needed // return (kL, kR, c) - return Buffer.concat([left, rightBuffer, chainCode]); + return Buffer.concat([left, rightBuffer, childChainCode]); } /** @@ -169,7 +170,6 @@ export function deriveChildNodePublic(extendedKey: Uint8Array, index: number, g: data[0] = 0x02; const z: Buffer = createHmac("sha512", cc).update(data).digest(); - // Step 2: Compute child public key const zL: Uint8Array = trunc_256_minus_g_bits(z.subarray(0, 32), g) @@ -188,10 +188,10 @@ export function deriveChildNodePublic(extendedKey: Uint8Array, index: number, g: // Step 3: Compute child chain code data[0] = 0x03; - const i: Buffer = createHmac("sha512", cc).update(data).digest(); - const chainCode: Buffer = i.subarray(32, 64); + const fullChildChainCode: Buffer = createHmac("sha512", cc).update(data).digest(); + const childChainCode: Buffer = fullChildChainCode.subarray(32, 64); - return Buffer.concat([crypto_core_ed25519_add(p, pk), chainCode]); + return Buffer.concat([crypto_core_ed25519_add(p, pk), childChainCode]); } /** @@ -218,7 +218,8 @@ function derivedNonHardened( const z: Buffer = createHmac("sha512", cc).update(data).digest(); data[0] = 0x03; - const childChainCode: Buffer = createHmac("sha512", cc).update(data).digest(); + const fullChildChainCode: Buffer = createHmac("sha512", cc).update(data).digest(); + const childChainCode: Buffer = fullChildChainCode.subarray(32, 64); return { z, childChainCode }; } @@ -247,7 +248,8 @@ function deriveHardened( data[0] = 0x00; const z: Buffer = createHmac("sha512", cc).update(data).digest(); data[0] = 0x01; - const childChainCode: Buffer = createHmac("sha512", cc).update(data).digest(); + const fullChildChainCode: Buffer = createHmac("sha512", cc).update(data).digest(); + const childChainCode: Buffer = fullChildChainCode.subarray(32, 64); return { z, childChainCode }; } From a6ffb6e13a7203e61e0106ea817b11d50ab8abb1 Mon Sep 17 00:00:00 2001 From: HashMapsData2Value <83883690+HashMapsData2Value@users.noreply.github.com> Date: Fri, 26 Apr 2024 11:50:47 +0200 Subject: [PATCH 44/47] chore: remove whitespace --- assets/arc-0052/bip32-ed25519.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/arc-0052/bip32-ed25519.ts b/assets/arc-0052/bip32-ed25519.ts index f53f5b571..4cc76562a 100644 --- a/assets/arc-0052/bip32-ed25519.ts +++ b/assets/arc-0052/bip32-ed25519.ts @@ -172,7 +172,7 @@ export function deriveChildNodePublic(extendedKey: Uint8Array, index: number, g: // Step 2: Compute child public key const zL: Uint8Array = trunc_256_minus_g_bits(z.subarray(0, 32), g) - + // ###################################### // Standard BIP32-ed25519 derivation // ####################################### @@ -182,7 +182,7 @@ export function deriveChildNodePublic(extendedKey: Uint8Array, index: number, g: // Chris Peikert's ammendment to BIP32-ed25519 derivation // ####################################### // zL = 8 * trunc_256_minus_g_bits (z_left_hand_side, g) - + const left = new BN(zL, 16, 'le').mul(new BN(8)).toArrayLike(Buffer, 'le', 32); const p: Uint8Array = crypto_scalarmult_ed25519_base_noclamp(left); From 6a02c3fa51a837b06542aaea1adb9def7918424a Mon Sep 17 00:00:00 2001 From: ehanoc Date: Mon, 6 May 2024 19:47:36 +0100 Subject: [PATCH 45/47] fix: zero out the most significant bits in a little endian array. Wrong bits were being zeroed Signed-off-by: ehanoc --- assets/arc-0052/bip32-ed25519.ts | 21 ++++++++------ assets/arc-0052/contextual.api.crypto.spec.ts | 29 ++++++++++--------- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/assets/arc-0052/bip32-ed25519.ts b/assets/arc-0052/bip32-ed25519.ts index 4cc76562a..79f43c008 100644 --- a/assets/arc-0052/bip32-ed25519.ts +++ b/assets/arc-0052/bip32-ed25519.ts @@ -54,22 +54,25 @@ export function trunc_256_minus_g_bits(array: Uint8Array, g: number): Uint8Array throw new Error("Number of bits to zero must be between 0 and 256."); } + // make a copy of array + const truncated = new Uint8Array(array); + let remainingBits = g; // Start from the last byte and move backward - for (let i = array.length - 1; i >= 0 && remainingBits > 0; i--) { + for (let i = truncated.length - 1; i >= 0 && remainingBits > 0; i--) { if (remainingBits >= 8) { // If more than 8 bits remain to be zeroed, zero the entire byte - array[i] = 0; + truncated[i] = 0; remainingBits -= 8; } else { - // Zero only the required bits in the last affected byte - array[i] &= ~(0xFF >> (8 - remainingBits)); // Mask to zero only the last remainingBits + // Zero out the most significant bits + truncated[i] &= (0xFF >> remainingBits) break; } } - return array + return truncated } /** @@ -128,13 +131,13 @@ export function deriveChildNodePrivate( // zL = kL + 8 * truncated(z_left_hand_side) // Big Integers + little Endianess - const klBigNum = new BN(kL, 16, "le") + const klBigNum = new BN(kL, 16, 'le') const big8 = new BN(8); - const zlBigNum = new BN(zL, 16, "le"); + const zlBigNum = new BN(zL, 16, 'le'); - const left = klBigNum.add(zlBigNum.mul(big8)).toArrayLike(Buffer, "le", 32); + const left = klBigNum.add(zlBigNum.mul(big8)).toArrayLike(Buffer, 'le', 32); - let right = new BN(kR, 16, "le").add(new BN(zRight, 16, "le")).toArrayLike(Buffer, "le").slice(0, 32); + let right = new BN(kR, 16, 'le').add(new BN(zRight, 16, 'le')).toArrayLike(Buffer, 'le').slice(0, 32); const rightBuffer = Buffer.alloc(32); Buffer.from(right).copy(rightBuffer, 0, 0, right.length) // padding with zeros if needed diff --git a/assets/arc-0052/contextual.api.crypto.spec.ts b/assets/arc-0052/contextual.api.crypto.spec.ts index 80cfb2ec3..bd3274457 100644 --- a/assets/arc-0052/contextual.api.crypto.spec.ts +++ b/assets/arc-0052/contextual.api.crypto.spec.ts @@ -154,17 +154,17 @@ describe("Contextual Derivation & Signing", () => { describe("Soft Derivations", () => { it("\(OK) Derive m'/44'/283'/0'/0/0 Algorand Address Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 0) - expect(key).toEqual(new Uint8Array(Buffer.from("8ad0bbc42326ac64eb4dbbe40a77518a7fc1d39504b618a4dc85f03b3a921a02", "hex"))) + expect(key).toEqual(new Uint8Array(Buffer.from("7bda7ac12627b2c259f1df6875d30c10b35f55b33ad2cc8ea2736eaa3ebcfab9", "hex"))) }) it("\(OK) Derive m'/44'/283'/0'/0/1 Algorand Address Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 1) - expect(key).toEqual(new Uint8Array(Buffer.from("2d3f9e31232bd36e6c0f37597e19c4c0154e58c41bc2b737c7700b683e85d0af", "hex"))) + expect(key).toEqual(new Uint8Array(Buffer.from("5bae8828f111064637ac5061bd63bc4fcfe4a833252305f25eeab9c64ecdf519", "hex"))) }) it("\(OK) Derive m'/44'/283'/0'/0/2 Algorand Address Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, 2) - expect(key).toEqual(new Uint8Array(Buffer.from("96acc17f0c34f6c640d5466988ce59c4da5423b5ec233b7ad2e5c5a3b1b80782", "hex"))) + expect(key).toEqual(new Uint8Array(Buffer.from("00a72635e97cba966529e9bfb4baf4a32d7b8cd2fcd8e2476ce5be1177848cb3", "hex"))) }) }) @@ -172,17 +172,17 @@ describe("Contextual Derivation & Signing", () => { describe("Hard Derivations", () => { it("\(OK) Derive m'/44'/283'/1'/0/0 Algorand Address Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 1, 0) - expect(key).toEqual(new Uint8Array(Buffer.from("fd56577456794efb91e05dc947d26d4864b346d139dfa8fff9b0e1def84b9078", "hex"))) + expect(key).toEqual(new Uint8Array(Buffer.from("358d8c4382992849a764438e02b1c45c2ca4e86bbcfe10fd5b963f3610012bc9", "hex"))) }) it("\(OK) Derive m'/44'/283'/2'/0/1 Algorand Address Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 2, 1) - expect(key).toEqual(new Uint8Array(Buffer.from("aa03d62057744f4422d70c3a421deae838d8f7546a15f2ada59287569911144c", "hex"))) + expect(key).toEqual(new Uint8Array(Buffer.from("1f0f75fbbca12b22523973191061b2f96522740e139a3420c730717ac5b0dfc0", "hex"))) }) it("\(OK) Derive m'/44'/283'/3'/0/0 Algorand Address Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 3, 0) - expect(key).toEqual(new Uint8Array(Buffer.from("303718c23846fdd0f7d9cded69d95c5a72fbb1ccbbea50c865c00c050bb0e68b", "hex"))) + expect(key).toEqual(new Uint8Array(Buffer.from("f035316f915b342ea5fe78dccb59d907b93805732219d436a1bd8488ff4e5b1b", "hex"))) }) }) }) @@ -191,29 +191,29 @@ describe("Contextual Derivation & Signing", () => { describe("Soft Derivations", () => { it("\(OK) Derive m'/44'/0'/0'/0/0 Identity Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Identity, 0, 0) - expect(key).toEqual(new Uint8Array(Buffer.from("844cda69c4ef7c212befaa6733f5e3c0317fc173cb9f14c6cf66a48263e722ec", "hex"))) + expect(key).toEqual(new Uint8Array(Buffer.from("ff8b1863ef5e40d0a48c245f26a6dbdf5da94dc75a1851f51d8a04e547bd5f5a", "hex"))) }) it("\(OK) Derive m'/44'/0'/0'/0/1 Identity Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Identity, 0, 1) - expect(key).toEqual(new Uint8Array(Buffer.from("a8c6de4e6d2672ad5a804994cf6e481ea7c2c3b1cedc5f51c63b2d0819d503f0", "hex"))) + expect(key).toEqual(new Uint8Array(Buffer.from("2b46c2af0890493e486049d456509a0199e565b41a5fb622f0ea4b9337bd2b97", "hex"))) }) it("\(OK) Derive m'/44'/0'/0'/0/2 Identity Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Identity, 0, 2) - expect(key).toEqual(new Uint8Array(Buffer.from("88e493675894f0ba8472037da40a61a7ed356fd0f24c312a1ec9bb7c052f5d8c", "hex"))) + expect(key).toEqual(new Uint8Array(Buffer.from("2713f135f19ef3dcfca73cb536b1e077b1165cd0b7bedbef709447319ff0016d", "hex"))) }) }) describe("Hard Derivations", () => { it("\(OK) Derive m'/44'/0'/1'/0/0 Identity Key", async () => { const key: Uint8Array = await cryptoService.keyGen(KeyContext.Identity, 1, 0) - expect(key).toEqual(new Uint8Array(Buffer.from("c120ac324985009294f2546463f922a55cd93fb1d209929828d2a952c4581760", "hex"))) + expect(key).toEqual(new Uint8Array(Buffer.from("232847ae1bb95babcaa50c8033fab98f59e4b4ad1d89ac523a90c830e4ceee4a", "hex"))) }) it("\(OK) Derive m'/44'/0'/2'/0/1 Identity Key", async () => { - const key: Uint8Array = await cryptoService.keyGen(KeyContext.Identity, 2, 1) - expect(key).toEqual(new Uint8Array(Buffer.from("82ccd181c1d533828a77218bdff0329966dc506084d5b87a0f2eb6678e53cb21", "hex"))) + const key: Uint8Array = await cryptoService.keyGen(KeyContext.Identity, 2, 1) + expect(key).toEqual(new Uint8Array(Buffer.from("8f68b6572860d84e8a41e38db1c8c692ded5eb291846f2e5bbfde774a9c6d16e", "hex"))) }) }) }) @@ -430,7 +430,10 @@ describe("Contextual Derivation & Signing", () => { // encrypt const cipherText: Uint8Array = crypto_secretbox_easy(message, nonce, aliceSharedSecret) - expect(cipherText).toEqual(new Uint8Array([158, 107, 5, 77, 134, 215, 212, 90, 192, 34, 131, 39, 160, 252, 74, 194, 129, 54, 249, 113, 128, 241, 213, 244, 98, 55, 46, 233, 7])) + // log cipherText uint8array + console.log("cipherText", cipherText) + + expect(cipherText).toEqual(new Uint8Array([20, 107, 126, 154, 152, 197, 252, 227, 148, 39, 245, 136, 233, 10, 199, 20, 219, 3, 53, 2, 113, 6, 190, 21, 193, 119, 43, 44, 230])) // decrypt const plainText: Uint8Array = crypto_secretbox_open_easy(cipherText, nonce, bobSharedSecret) expect(plainText).toEqual(message) From 4bdb38fcf4ea24f8807033affb8415831e408212 Mon Sep 17 00:00:00 2001 From: ehanoc Date: Mon, 20 May 2024 18:28:32 +0100 Subject: [PATCH 46/47] temp: tests for break levels derivation Signed-off-by: ehanoc --- assets/arc-0052/bip32-ed25519.spec.ts | 108 ++++++++++++++++++ assets/arc-0052/bip32-ed25519.ts | 24 ++-- assets/arc-0052/contextual.api.crypto.spec.ts | 4 +- assets/arc-0052/contextual.api.crypto.ts | 2 +- 4 files changed, 128 insertions(+), 10 deletions(-) create mode 100644 assets/arc-0052/bip32-ed25519.spec.ts diff --git a/assets/arc-0052/bip32-ed25519.spec.ts b/assets/arc-0052/bip32-ed25519.spec.ts new file mode 100644 index 000000000..1c2fd8bde --- /dev/null +++ b/assets/arc-0052/bip32-ed25519.spec.ts @@ -0,0 +1,108 @@ +import { crypto_scalarmult_ed25519_base_noclamp, ready } from "libsodium-wrappers-sumo" +import { deriveChildNodePrivate, deriveChildNodePublic, fromSeed, trunc_256_minus_g_bits } from "./bip32-ed25519" +import * as bip39 from "bip39" +import { harden } from "./contextual.api.crypto" + +// function that prints bits +const printBits = (arr: Uint8Array) => { + const bits = arr.reduce((acc, byte) => { + return acc + byte.toString(2).padStart(8, "0") + " "; + }, "") + + console.log("bits", bits) +} + + +describe("BIP32-Ed25519", () => { + describe("zero last number of bits", () => { + it("", () => { + // create 32 byte array with values + const input = new Uint8Array(32); // Create a 32-byte (256-bit) array + for (let i = 0; i < 32; i++) { + input[i] = 0xFF; // Initialize all bytes to 0xFF for demonstration + } + + // log bits binary with space every 8 bits + const inputBits = input.reduce((acc, byte) => { + return acc + byte.toString(2).padStart(8, "0") + " "; + }, ""); + + console.log("bits", inputBits) + + const extracted: Uint8Array = trunc_256_minus_g_bits(input, 9); + + // log bits binary with space every 8 bits + const bits = extracted.reduce((acc, byte) => { + return acc + byte.toString(2).padStart(8, "0") + " "; + }, ""); + + console.log("bits", bits) + }) + }) + + // describe group for N levels of derivation + describe.only("For N levels of derivation check that secret keys match public keys", () => { + let bip39Mnemonic: string = "salon zoo engage submit smile frost later decide wing sight chaos renew lizard rely canal coral scene hobby scare step bus leaf tobacco slice" + let seed: Buffer + + beforeAll(() => { + seed = bip39.mnemonicToSeedSync(bip39Mnemonic, "") + }) + + it("Derive 7 levels with g == Peikeirt == 9, private and public derivations, check matching", async () => { + await ready // libsodium ready + + const rootKey = fromSeed(seed) + // 44'/283'/0'/0 + const bip44Path = [harden(44), harden(283), harden(0), 0, 0] + + await ready // libsodium + + // Pick `g`, which is amount of bits zeroed from each derived node + const g: number = 9 + + let derivationNode: Uint8Array = rootKey + for (let i = 0; i < bip44Path.length + 85; i++) { + derivationNode = await deriveChildNodePrivate(derivationNode, bip44Path[i], g) + } + + // top end of the field Ed25519 + const maxValue: bigint = 2n ** 255n - 19n + + // derivationNode = await deriveChildNodePrivate(derivationNode, 0, g) + + // extendedPublicNodeFormat + const extendedPublicDerivationNode = new Uint8Array(Buffer.concat([crypto_scalarmult_ed25519_base_noclamp(derivationNode.subarray(0, 32)), derivationNode.subarray(64, 96)])) + + let derivationPublicNode = await deriveChildNodePublic(extendedPublicDerivationNode, 0, g) + derivationNode = await deriveChildNodePrivate(derivationNode, 0, g) + + // 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 + const derivedKey: Uint8Array = derivationPublicNode.subarray(0, 32) + + // print bits + printBits(derivedKey) + + const scalar: Uint8Array = derivationNode.subarray(0, 32) + + // scalar print bits + printBits(scalar) + + // check if scalar is smaller than the top end of the field + expect(BigInt(`0x${Buffer.from(scalar).toString("hex")}`)).toBeLessThan(maxValue) + + const myKey: Uint8Array = crypto_scalarmult_ed25519_base_noclamp(scalar) + + // print bits + printBits(myKey) + + // they should match + // derivedKey.subarray(0, 32) == public key (excluding chaincode) + expect(derivedKey).toEqual(myKey) + + }) + }) +}) \ No newline at end of file diff --git a/assets/arc-0052/bip32-ed25519.ts b/assets/arc-0052/bip32-ed25519.ts index 79f43c008..1b302d5cc 100644 --- a/assets/arc-0052/bip32-ed25519.ts +++ b/assets/arc-0052/bip32-ed25519.ts @@ -1,9 +1,12 @@ import { createHash, createHmac } from "crypto"; +import { read } from "fs"; import { crypto_core_ed25519_add, - crypto_scalarmult_ed25519_base_noclamp + crypto_scalarmult_ed25519_base_noclamp, + ready } from "libsodium-wrappers-sumo"; var BN = require("bn.js"); +import * as util from 'util' /** * @@ -89,11 +92,13 @@ export function trunc_256_minus_g_bits(array: Uint8Array, g: number): Uint8Array * @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 - (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 */ -export function deriveChildNodePrivate( +export async function deriveChildNodePrivate( extendedKey: Uint8Array, index: number, g: number = 9 -): Uint8Array { +): Promise { + await ready // wait for libsodium to be ready + const kL: Buffer = Buffer.from(extendedKey.subarray(0, 32)); const kR: Buffer = Buffer.from(extendedKey.subarray(32, 64)); const cc: Uint8Array = extendedKey.subarray(64, 96); @@ -133,7 +138,12 @@ export function deriveChildNodePrivate( // Big Integers + little Endianess const klBigNum = new BN(kL, 16, 'le') const big8 = new BN(8); - const zlBigNum = new BN(zL, 16, 'le'); + const zlBigNum = new BN(zL, 16, 'le') + + const zlBigNumMul8 = klBigNum.add(zlBigNum.mul(big8)) + + // check size of zlBigNumMul8 + if (zlBigNumMul8.toArrayLike(Buffer, 'le', 32).length != 32) console.log(util.inspect(zlBigNumMul8), { colors: true, depth: null })) const left = klBigNum.add(zlBigNum.mul(big8)).toArrayLike(Buffer, 'le', 32); @@ -143,7 +153,7 @@ export function deriveChildNodePrivate( Buffer.from(right).copy(rightBuffer, 0, 0, right.length) // padding with zeros if needed // return (kL, kR, c) - return Buffer.concat([left, rightBuffer, childChainCode]); + return new Uint8Array(Buffer.concat([left, rightBuffer, childChainCode])) } /** @@ -158,7 +168,7 @@ export function deriveChildNodePrivate( * @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 */ -export function deriveChildNodePublic(extendedKey: Uint8Array, index: number, g: number = 9): Uint8Array { +export async function deriveChildNodePublic(extendedKey: Uint8Array, index: number, g: number = 9): Promise { if (index > 0x80000000) throw new Error('can not derive public key with harden') const pk: Buffer = Buffer.from(extendedKey.subarray(0, 32)) @@ -194,7 +204,7 @@ export function deriveChildNodePublic(extendedKey: Uint8Array, index: number, g: const fullChildChainCode: Buffer = createHmac("sha512", cc).update(data).digest(); const childChainCode: Buffer = fullChildChainCode.subarray(32, 64); - return Buffer.concat([crypto_core_ed25519_add(p, pk), childChainCode]); + return new Uint8Array(Buffer.concat([crypto_core_ed25519_add(p, pk), childChainCode])) } /** diff --git a/assets/arc-0052/contextual.api.crypto.spec.ts b/assets/arc-0052/contextual.api.crypto.spec.ts index bd3274457..b700ef55e 100644 --- a/assets/arc-0052/contextual.api.crypto.spec.ts +++ b/assets/arc-0052/contextual.api.crypto.spec.ts @@ -488,7 +488,7 @@ describe("Contextual Derivation & Signing", () => { // 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 - const derivedKey: Uint8Array = new Uint8Array(deriveChildNodePublic(walletRoot, i, BIP32DerivationType.Peikert)) // g == 9 + const derivedKey: Uint8Array = new Uint8Array(await deriveChildNodePublic(walletRoot, i, BIP32DerivationType.Peikert)) // g == 9 // Deriving from my own wallet where i DO have private information const myKey: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, i, BIP32DerivationType.Peikert) @@ -507,7 +507,7 @@ describe("Contextual Derivation & Signing", () => { const numPublicKeysToDerive: number = 10 for (let i = 0; i < numPublicKeysToDerive; i++) { - const derivedKey: Uint8Array = new Uint8Array(deriveChildNodePublic(walletRoot, i, BIP32DerivationType.Peikert)) // g == 9 + const derivedKey: Uint8Array = new Uint8Array(await deriveChildNodePublic(walletRoot, i, BIP32DerivationType.Peikert)) // g == 9 // Deriving from my own wallet where i DO have private information const myKey: Uint8Array = await cryptoService.keyGen(KeyContext.Address, 0, i, BIP32DerivationType.Peikert) diff --git a/assets/arc-0052/contextual.api.crypto.ts b/assets/arc-0052/contextual.api.crypto.ts index 3c19020eb..f895c8fd6 100644 --- a/assets/arc-0052/contextual.api.crypto.ts +++ b/assets/arc-0052/contextual.api.crypto.ts @@ -90,7 +90,7 @@ export class ContextualCryptoApi { const g: number = derivationType === BIP32DerivationType.Peikert ? 9 : 32 for (let i = 0; i < bip44Path.length; i++) { - rootKey = deriveChildNodePrivate(rootKey, bip44Path[i], g) + rootKey = await deriveChildNodePrivate(rootKey, bip44Path[i], g) } if (isPrivate) return rootKey From 92a8b4ed4f9d0441b9b15a179905d0044d024799 Mon Sep 17 00:00:00 2001 From: ehanoc Date: Thu, 13 Jun 2024 19:13:15 +0100 Subject: [PATCH 47/47] Detect when scalar >= 2^255, stop derivation Signed-off-by: ehanoc --- assets/arc-0052/bip32-ed25519.ts | 7 ++-- assets/arc-0052/contextual.api.crypto.spec.ts | 36 ++++++++++++++++++- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/assets/arc-0052/bip32-ed25519.ts b/assets/arc-0052/bip32-ed25519.ts index 1b302d5cc..f243c85ce 100644 --- a/assets/arc-0052/bip32-ed25519.ts +++ b/assets/arc-0052/bip32-ed25519.ts @@ -142,8 +142,11 @@ export async function deriveChildNodePrivate( const zlBigNumMul8 = klBigNum.add(zlBigNum.mul(big8)) - // check size of zlBigNumMul8 - if (zlBigNumMul8.toArrayLike(Buffer, 'le', 32).length != 32) console.log(util.inspect(zlBigNumMul8), { colors: true, depth: null })) + // check if zlBigNumMul8 is equal or larger than 2^255 + if (zlBigNumMul8.cmp(new BN(2).pow(new BN(255))) >= 0) { + console.log(util.inspect(zlBigNumMul8), { colors: true, depth: null }) + throw new Error('zL * 8 is larger than 2^255, which is not safe') + } const left = klBigNum.add(zlBigNum.mul(big8)).toArrayLike(Buffer, 'le', 32); diff --git a/assets/arc-0052/contextual.api.crypto.spec.ts b/assets/arc-0052/contextual.api.crypto.spec.ts index b700ef55e..472dfccf9 100644 --- a/assets/arc-0052/contextual.api.crypto.spec.ts +++ b/assets/arc-0052/contextual.api.crypto.spec.ts @@ -3,7 +3,7 @@ import * as bip39 from "bip39" import { randomBytes } from "crypto" import { BIP32DerivationType, ContextualCryptoApi, ERROR_TAGS_FOUND, Encoding, KeyContext, SignMetadata, harden } from "./contextual.api.crypto" import * as msgpack from "algo-msgpack-with-bigint" -import { deriveChildNodePublic, fromSeed } from "./bip32-ed25519" +import { deriveChildNodePrivate, deriveChildNodePublic, fromSeed } from "./bip32-ed25519" import { sha512_256 } from "js-sha512" import base32 from "hi-base32" import { JSONSchemaType } from "ajv" @@ -474,6 +474,40 @@ describe("Contextual Derivation & Signing", () => { }) }) + describe("\(deriveNodePrivate)", () => { + it("\(FAIL) Should fail if during derivation scalar >= 2^255", async () => { + await ready // libsodium ready + + const rootKey = fromSeed(seed) + // 44'/283'/0'/0 + const bip44Path = [harden(44), harden(283), harden(0), 0, 0] + + await ready // libsodium + + // Pick `g`, which is amount of bits zeroed from each derived node + const g: number = 9 + + // 44' + let derivationNode: Uint8Array = await deriveChildNodePrivate(rootKey, bip44Path[0], g) + // 283' + derivationNode = await deriveChildNodePrivate(derivationNode, bip44Path[1], g) + // 0' + derivationNode = await deriveChildNodePrivate(derivationNode, bip44Path[2], g) + // 0 + derivationNode = await deriveChildNodePrivate(derivationNode, bip44Path[3], g) + // 0 + derivationNode = await deriveChildNodePrivate(derivationNode, bip44Path[4], g) + + for (let i = 0; i < 19; i++) { + derivationNode = await deriveChildNodePrivate(derivationNode, 0, g) + } + + // for the seed in this test, we know where the scalar breaks (>= 2 ^ 255) + // expect error at the 20th level for this known seed + expect(deriveChildNodePrivate(derivationNode, 0, g)).rejects.toThrow("zL * 8 is larger than 2^255, which is not safe") + }) + }) + describe("\(deriveNodePublic", () => { it("\(OK) From m'/44'/283'/0'/0 root level (excluding address index) derive N keys with only public information", async () => {