From cef89f8174bcaa050b31c56b06c6e667237a1270 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Mon, 11 Dec 2023 12:36:37 +0200 Subject: [PATCH 01/10] Additional explanations. --- sdk-wallet/keystores/encrypted_keystore.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/sdk-wallet/keystores/encrypted_keystore.md b/sdk-wallet/keystores/encrypted_keystore.md index 8238e8e..ca833e8 100644 --- a/sdk-wallet/keystores/encrypted_keystore.md +++ b/sdk-wallet/keystores/encrypted_keystore.md @@ -1,21 +1,31 @@ ## EncryptedKeystore +`EncryptedKeystore` + ``` class EncryptedKeystore: - // The constructor is not captured by the specs; it's up to the implementing library to define it. + // The constructor is not captured by the specs; it's up to the implementing library to define it. Suggestion below. + // If kind == 'secretKey', `secret_key`` must be provided, while `mnemonic` must be nil. + // If kind == 'mnemonic', `secret_key` must be nil, while `mnemonic` must be provided. + // In the implementation, all the parameters would be held as instance state (private fields). + private constructor(wallet_provider: IWalletProvider, kind: string, secret_key?: ISecretKey, mnemonic?: Mnemonic) // Named constructor - static new_from_secret_key(secret_key: ISecretKey): EncryptedKeystore + // This should have a trivial implementation (e.g. a wrapper around the private constructor). + static new_from_secret_key(wallet_provider: IWalletProvider, secret_key: ISecretKey): EncryptedKeystore // Named constructor - // Below, "wallet_provider" should implement "derive_secret_key_from_mnemonic()". - // Advice: in the implementation all the parameters will be held as instance state (private fields). + // This should have a trivial implementation (e.g. a wrapper around the private constructor). static new_from_mnemonic(wallet_provider: IWalletProvider, mnemonic: Mnemonic): EncryptedKeystore // Importing "constructor" + // This should decrypt the encrypted data, then call the (private) constructor. + // "password" should not be retained. static import_from_object(wallet_provider: IWalletProvider, object: KeyfileObject, password: string): EncryptedKeystore // Importing "constructor" + // This should load the file content, decrypt the encrypted data, then call the (private) constructor. + // "password" should not be retained. static import_from_file(wallet_provider: IWalletProvider, path: Path, password: string): EncryptedKeystore // When kind == 'secretKey', only index == 0 and passphrase == "" is supported. From 8a31f7df7c6536420bd971298803f86856319176 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Mon, 11 Dec 2023 15:27:33 +0200 Subject: [PATCH 02/10] Add extra explanations. Segregate IWalletProvider interface. --- sdk-wallet/interfaces.md | 16 +++++++++---- sdk-wallet/keystores/encrypted_keystore.md | 27 ++++++++++++---------- sdk-wallet/keystores/pem_keystore.md | 14 +++++++---- sdk-wallet/user_wallet_provider.md | 2 +- sdk-wallet/validator_wallet_provider.md | 2 +- 5 files changed, 38 insertions(+), 23 deletions(-) diff --git a/sdk-wallet/interfaces.md b/sdk-wallet/interfaces.md index 38427b4..289cb2e 100644 --- a/sdk-wallet/interfaces.md +++ b/sdk-wallet/interfaces.md @@ -5,12 +5,20 @@ For languages that support **structural typing** (e.g. Go, Python, TypeScript), For languages that only support **nominal typing** (e.g. C#), these interfaces can be _exported_. ``` -interface IWalletProvider: - generate_keypair(): (ISecretKey, IPublicKey) +interface ISigner: sign(data: bytes, secret_key: ISecretKey): bytes +``` + +``` +interface IVerifier: verify(data: bytes, signature: bytes, public_key: IPublicKey): bool - create_secret_key_from_bytes(data: bytes): ISecretKey - create_public_key_from_bytes(data: bytes): IPublicKey +``` + +``` +interface IKeysComputer: + generate_keypair(): (ISecretKey, IPublicKey) + compute_secret_key_from_bytes(data: bytes): ISecretKey + compute_public_key_from_bytes(data: bytes): IPublicKey compute_public_key_from_secret_key(secret_key: ISecretKey): IPublicKey ``` diff --git a/sdk-wallet/keystores/encrypted_keystore.md b/sdk-wallet/keystores/encrypted_keystore.md index ca833e8..ad21d19 100644 --- a/sdk-wallet/keystores/encrypted_keystore.md +++ b/sdk-wallet/keystores/encrypted_keystore.md @@ -4,40 +4,43 @@ ``` class EncryptedKeystore: - // The constructor is not captured by the specs; it's up to the implementing library to define it. Suggestion below. - // If kind == 'secretKey', `secret_key`` must be provided, while `mnemonic` must be nil. + // The constructor is not strictly captured by the specs; it's up to the implementing library to define it. Suggestion below. + // If kind == 'secretKey', `secret_key` must be provided, while `mnemonic` must be nil. // If kind == 'mnemonic', `secret_key` must be nil, while `mnemonic` must be provided. // In the implementation, all the parameters would be held as instance state (private fields). - private constructor(wallet_provider: IWalletProvider, kind: string, secret_key?: ISecretKey, mnemonic?: Mnemonic) + private constructor(keys_computer: IKeysComputer, kind: string, secret_key?: ISecretKey, mnemonic?: Mnemonic) // Named constructor // This should have a trivial implementation (e.g. a wrapper around the private constructor). - static new_from_secret_key(wallet_provider: IWalletProvider, secret_key: ISecretKey): EncryptedKeystore + static new_from_secret_key(keys_computer: IKeysComputer, secret_key: ISecretKey): EncryptedKeystore // Named constructor // This should have a trivial implementation (e.g. a wrapper around the private constructor). - static new_from_mnemonic(wallet_provider: IWalletProvider, mnemonic: Mnemonic): EncryptedKeystore + static new_from_mnemonic(keys_computer: IKeysComputer, mnemonic: Mnemonic): EncryptedKeystore // Importing "constructor" // This should decrypt the encrypted data, then call the (private) constructor. // "password" should not be retained. - static import_from_object(wallet_provider: IWalletProvider, object: KeyfileObject, password: string): EncryptedKeystore + static import_from_object(keys_computer: IKeysComputer, object: KeyfileObject, password: string): EncryptedKeystore // Importing "constructor" // This should load the file content, decrypt the encrypted data, then call the (private) constructor. // "password" should not be retained. - static import_from_file(wallet_provider: IWalletProvider, path: Path, password: string): EncryptedKeystore + static import_from_file(keys_computer: IKeysComputer, path: Path, password: string): EncryptedKeystore - // When kind == 'secretKey', only index == 0 and passphrase == "" is supported. - // When kind == 'mnemonic', secret key derivation happens under the hood. - // Below, "passphrase" is the bip39 passphrase required to derive a secret key from a mnemonic (by default, it should be an empty string). - get_secret_key(index: int, passphrase: string): ISecretKey + get_kind(): "secretKey|mnemonic" + + // Can throw: + // - ErrSecretKeyNotAvailable + // + // Returns the secretKey (if available, i.e. if kind == 'secretKey'). + get_secret_key(): ISecretKey // Can throw: // - ErrMnemonicNotAvailable // // Returns the mnemonic used to create the keystore (if available, i.e. if kind == 'mnemonic'). - // This function is useful for UX flows where the application has to display the mnemonic etc. + // The caller of this can then derive secret keys, as needed. get_mnemonic(): Mnemonic export_to_object(password: string, address_hrp: string): KeyfileObject diff --git a/sdk-wallet/keystores/pem_keystore.md b/sdk-wallet/keystores/pem_keystore.md index 71a1c3b..889b8bf 100644 --- a/sdk-wallet/keystores/pem_keystore.md +++ b/sdk-wallet/keystores/pem_keystore.md @@ -2,19 +2,23 @@ ``` class PEMKeystore: - // The constructor is not captured by the specs; it's up to the implementing library to define it. + // The constructor is not strictly captured by the specs; it's up to the implementing library to define it. Suggestion below. + // In the implementation, all the parameters would be held as instance state (private fields). + private constructor(keys_computer: IKeysComputer, secret_keys: ISecretKey[]) // Named constructor - static new_from_secret_key(wallet_provider: IWalletProvider, secret_key: ISecretKey): PEMKeystore + // This should have a trivial implementation (e.g. a wrapper around the private constructor). + static new_from_secret_key(keys_computer: IKeysComputer, secret_key: ISecretKey): PEMKeystore // Named constructor - static new_from_secret_keys(wallet_provider: IWalletProvider, secret_keys: ISecretKey[]): PEMKeystore + // This should have a trivial implementation (e.g. a wrapper around the private constructor). + static new_from_secret_keys(keys_computer: IKeysComputer, secret_keys: ISecretKey[]): PEMKeystore // Importing "constructor" - static import_from_text(wallet_provider: IWalletProvider, text: string): PEMKeystore + static import_from_text(keys_computer: IKeysComputer, text: string): PEMKeystore // Importing "constructor" - static import_from_file(wallet_provider: IWalletProvider, path: Path): PEMKeystore + static import_from_file(keys_computer: IKeysComputer, path: Path): PEMKeystore get_secret_key(index: int): ISecretKey diff --git a/sdk-wallet/user_wallet_provider.md b/sdk-wallet/user_wallet_provider.md index be0cd34..d77092e 100644 --- a/sdk-wallet/user_wallet_provider.md +++ b/sdk-wallet/user_wallet_provider.md @@ -1,7 +1,7 @@ ## UserWalletProvider ``` -class UserWalletProvider implements IWalletProvider: +class UserWalletProvider implements ISigner, IVerifier, IKeysGenerator: // The constructor is not captured by the specs; it's up to the implementing library to define it. // For example, the constructor can be parametrized with underlying, more low-level crypto components, if applicable. diff --git a/sdk-wallet/validator_wallet_provider.md b/sdk-wallet/validator_wallet_provider.md index 9b52937..8e796dd 100644 --- a/sdk-wallet/validator_wallet_provider.md +++ b/sdk-wallet/validator_wallet_provider.md @@ -1,7 +1,7 @@ ## ValidatorWalletProvider ``` -class ValidatorWalletProvider implements IWalletProvider: +class ValidatorWalletProvider implements ISigner, IVerifier, IKeysGenerator: // The constructor is not captured by the specs; it's up to the implementing library to define it. // For example, the constructor can be parametrized with underlying, more low-level crypto components, if applicable. From 3185f3a19010fb83e8ea9551c8dedcfe675358cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Tue, 12 Dec 2023 10:32:20 +0200 Subject: [PATCH 03/10] Additional explanations. --- sdk-wallet/crypto/keypair_based_encryptor_decryptor.md | 7 +++++++ sdk-wallet/crypto/public_key_encrypted_data.md | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/sdk-wallet/crypto/keypair_based_encryptor_decryptor.md b/sdk-wallet/crypto/keypair_based_encryptor_decryptor.md index 5766d1a..cae1e55 100644 --- a/sdk-wallet/crypto/keypair_based_encryptor_decryptor.md +++ b/sdk-wallet/crypto/keypair_based_encryptor_decryptor.md @@ -1,5 +1,12 @@ ## KeyPairBasedEncryptorDecryptor +Optional, can be omitted by implementing libraries. + +Implementation suggestions: + - https://github.com/multiversx/mx-sdk-js-wallet/blob/v4.2.1/src/crypto/pubkeyEncryptor.ts + - https://github.com/multiversx/mx-sdk-js-wallet/blob/main/src/crypto/pubkeyDecryptor.ts + - + ``` class KeyPairBasedEncryptorDecryptor: encrypt(data: bytes, recipient_public_key: IPublicKey, auth_secret_key: ISecretKey): PublicKeyEncryptedData; diff --git a/sdk-wallet/crypto/public_key_encrypted_data.md b/sdk-wallet/crypto/public_key_encrypted_data.md index 29495ed..1f4175e 100644 --- a/sdk-wallet/crypto/public_key_encrypted_data.md +++ b/sdk-wallet/crypto/public_key_encrypted_data.md @@ -1,5 +1,10 @@ ## PublicKeyEncryptedData +Optional, can be omitted by implementing libraries. + +Implementation suggestions: + - https://github.com/multiversx/mx-sdk-js-wallet/blob/main/src/crypto/x25519EncryptedData.ts + ``` dto PublicKeyEncryptedData: nonce: string; From 85b7ad617171ebd68e73807bc4f9ff717ef82375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Tue, 12 Dec 2023 15:20:35 +0200 Subject: [PATCH 04/10] More explanations, some refactoring. --- sdk-wallet/cookbook.md | 79 ++++++++++++++++++++++ sdk-wallet/keystores/encrypted_keystore.md | 32 --------- sdk-wallet/mnemonic.md | 38 ----------- sdk-wallet/user_wallet_provider.md | 18 ++++- 4 files changed, 96 insertions(+), 71 deletions(-) create mode 100644 sdk-wallet/cookbook.md diff --git a/sdk-wallet/cookbook.md b/sdk-wallet/cookbook.md new file mode 100644 index 0000000..76d2c48 --- /dev/null +++ b/sdk-wallet/cookbook.md @@ -0,0 +1,79 @@ + +## Cookbook (examples of usage) + +Create a new mnemonic and derive the first secret key. + +``` +provider = new UserWalletProvider() +mnemonic = provider.generate_mnemonic() +print(mnemonic.toString()) + +sk = provider.derive_secret_key_from_mnemonic(mnemonic, address_index=0 , passphrase="") +pk = provider.compute_public_key_from_secret_key(sk) +address = new Address(public_key, "erd") +print(address.bech32()) +``` + +Generate a secret key and export it to a PEM keystore. + +``` +provider = new UserWalletProvider() +sk, _ = provider.generate_keypair() +keystore = PEMKeystore.new_from_secret_key(provider, sk) +keystore.export_to_file("wallet.pem", "erd") +``` + +Generate a mnemonic and export it to a JSON keystore. + +``` +provider = new UserWalletProvider() +mnemonic = provider.generate_mnemonic() +keystore = EncryptedKeystore.new_from_mnemonic(provider, mnemonic) +keystore.export_to_file("wallet.json", "password", "erd") +``` + +Loading a JSON keystore which holds the encrypted mnemonic, then iterate over the first 3 accounts: + +``` +provider = new UserWalletProvider() +keystore = EncryptedKeystore.import_from_file(provider, "wallet.json", "password") +mnemonic = keystore.get_mnemonic() + +for i in [0, 1, 2]: + secret_key = provider.derive_secret_key_from_mnemonic(mnemonic, address_index=i, passphrase="") + public_key = provider.compute_public_key_from_secret_key(secret_key) + address = new Address(public_key, "erd") + print("Address", i, address.bech32()) +``` + +Changing the password of an existing keystore: + +``` +provider = new UserWalletProvider() +keystore = EncryptedKeystore.import_from_file(provider, "wallet.json", "password") +keystore.export_to_file("wallet.json", "new_password", "erd") +``` + +Loading a PEM keystore and sign a piece of data: + +``` +provider = new UserWalletProvider() +keystore = PEMKeystore.import_from_file(provider, "wallet.pem") +first_secret_key = keystore.get_secret_key(0) +signature = provider.sign(data, first_secret_key) +``` + +Loading a JSON keystore and sign data: + +``` +provider = new UserWalletProvider() +keystore = EncryptedKeystore.import_from_file(provider, "wallet.json", "password") + +if keystore.get_kind() == "secretKey": + first_secret_key = keystore.get_secret_key() +else: + mnemonic = keystore.get_mnemonic() + first_secret_key = provider.derive_secret_key_from_mnemonic(mnemonic, address_index=0, passphrase="") + +signature = provider.sign(data, first_secret_key) +``` diff --git a/sdk-wallet/keystores/encrypted_keystore.md b/sdk-wallet/keystores/encrypted_keystore.md index ad21d19..b9264cf 100644 --- a/sdk-wallet/keystores/encrypted_keystore.md +++ b/sdk-wallet/keystores/encrypted_keystore.md @@ -99,35 +99,3 @@ dto KeyfileObject: mac: string; }; ``` - -## Examples of usage - -Create a new JSON keystore using a new mnemonic: - -``` -provider = new UserWalletProvider() -mnemonic = provider.generate_mnemonic() -keystore = EncryptedKeystore.new_from_mnemonic(provider, mnemonic) -keystore.export_to_file("file.json", "password", "erd") -``` - -Iterating over the first 3 accounts: - -``` -provider = new UserWalletProvider() -keystore = EncryptedKeystore.import_from_file(provider, "file.json", "password") - -for i in [0, 1, 2]: - secret_key = keystore.get_secret_key(i, "") - public_key = provider.compute_public_key_from_secret_key(secret_key) - address = new Address(public_key, "erd") - print("Address", i, address.bech32()) -``` - -Changing the password of an existing keystore: - -``` -provider = new UserWalletProvider() -keystore = EncryptedKeystore.import_from_file(provider, "file.json", "password") -keystore.export_to_file("file.json", "new_password", "erd") -``` diff --git a/sdk-wallet/mnemonic.md b/sdk-wallet/mnemonic.md index 84e55ed..835e48c 100644 --- a/sdk-wallet/mnemonic.md +++ b/sdk-wallet/mnemonic.md @@ -20,41 +20,3 @@ class Mnemonic: toString(): string; } ``` - -## MnemonicComputer - -Encapsulates logic for generating and validating mnemonics and for deriving secret keys from mnemonics (i.e. BIP39). - -``` -class MnemonicComputer implements IMnemonicComputer: - // The constructor is not captured by the specs; it's up to the implementing library to define it. - - // Should not throw. - generate_mnemonic(): Mnemonic - - // Should not throw. - validate_mnemonic(mnemonic: Mnemonic): bool - - // Can throw: - // - ErrInvalidMnemonic - // Below, "passphrase" is the optional bip39 passphrase used to derive a secret key from a mnemonic. - // Reference: https://en.bitcoin.it/wiki/Seed_phrase#Two-factor_seed_phrases - derive_secret_key_from_mnemonic(mnemonic: Mnemonic, address_index: int, passphrase: string = ""): ISecretKey -``` - -## Examples of usage - -Creating a new mnemonic and deriving the first secret key. - -``` -computer = new MnemonicComputer() -provider = new UserWalletProvider() -mnemonic = computer.generate_mnemonic() -print(mnemonic.toString()) - -mnemonic = Mnemonic.newfromText("...") -sk = computer.derive_secret_key_from_mnemonic(mnemonic, address_index=0 , passphrase="") -pk = provider.compute_public_key_from_secret_key(sk) -address = new Address(public_key, "erd") -print(address.bech32()) -``` diff --git a/sdk-wallet/user_wallet_provider.md b/sdk-wallet/user_wallet_provider.md index d77092e..36f8929 100644 --- a/sdk-wallet/user_wallet_provider.md +++ b/sdk-wallet/user_wallet_provider.md @@ -1,7 +1,11 @@ ## UserWalletProvider +Provides functionality for generating secret keys, derivating public keys, signing & verifying data. + +Additionally, allows one to generate and validate mnemonics, and to deriving secret keys from mnemonics (i.e. BIP39). + ``` -class UserWalletProvider implements ISigner, IVerifier, IKeysGenerator: +class UserWalletProvider implements ISigner, IVerifier, IKeysGenerator, IMnemonicComputer: // The constructor is not captured by the specs; it's up to the implementing library to define it. // For example, the constructor can be parametrized with underlying, more low-level crypto components, if applicable. @@ -28,4 +32,16 @@ class UserWalletProvider implements ISigner, IVerifier, IKeysGenerator: // Can throw: // - ErrInvalidSecretKey compute_public_key_from_secret_key(secret_key: ISecretKey): IPublicKey + + // Should not throw. + generate_mnemonic(): Mnemonic + + // Should not throw. + validate_mnemonic(mnemonic: Mnemonic): bool + + // Can throw: + // - ErrInvalidMnemonic + // Below, "passphrase" is the optional bip39 passphrase used to derive a secret key from a mnemonic. + // Reference: https://en.bitcoin.it/wiki/Seed_phrase#Two-factor_seed_phrases + derive_secret_key_from_mnemonic(mnemonic: Mnemonic, address_index: int, passphrase: string = ""): ISecretKey ``` From f8f3b0252b151bd709df2b42e4c179a72d7689b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Tue, 12 Dec 2023 16:56:21 +0200 Subject: [PATCH 05/10] Add extra note. --- sdk-wallet/keystores/encrypted_keystore.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sdk-wallet/keystores/encrypted_keystore.md b/sdk-wallet/keystores/encrypted_keystore.md index b9264cf..6ad6c6e 100644 --- a/sdk-wallet/keystores/encrypted_keystore.md +++ b/sdk-wallet/keystores/encrypted_keystore.md @@ -1,6 +1,8 @@ ## EncryptedKeystore -`EncryptedKeystore` +`EncryptedKeystore` handles encrypted JSON files. + +Note: current design is suboptimal. `EncryptedKeystore` handles both legacy keystore files (that hold encrypted secret keys) and new keystore files (that hold encrypted mnemonics). `get_kind()` can be used as a differentiator. A future design might involve two separate classes. ``` class EncryptedKeystore: From 81fd122ac184a504e894fdeeba8a1553d9f302d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Tue, 12 Dec 2023 21:43:22 +0200 Subject: [PATCH 06/10] Adjust cookbook. --- sdk-wallet/cookbook.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/sdk-wallet/cookbook.md b/sdk-wallet/cookbook.md index 76d2c48..baf904a 100644 --- a/sdk-wallet/cookbook.md +++ b/sdk-wallet/cookbook.md @@ -1,7 +1,7 @@ ## Cookbook (examples of usage) -Create a new mnemonic and derive the first secret key. +Generate a mnemonic and derive the first secret key: ``` provider = new UserWalletProvider() @@ -10,11 +10,11 @@ print(mnemonic.toString()) sk = provider.derive_secret_key_from_mnemonic(mnemonic, address_index=0 , passphrase="") pk = provider.compute_public_key_from_secret_key(sk) -address = new Address(public_key, "erd") +address = new Address(pk, "erd") print(address.bech32()) ``` -Generate a secret key and export it to a PEM keystore. +Generate a secret key and export it to a PEM keystore: ``` provider = new UserWalletProvider() @@ -23,7 +23,7 @@ keystore = PEMKeystore.new_from_secret_key(provider, sk) keystore.export_to_file("wallet.pem", "erd") ``` -Generate a mnemonic and export it to a JSON keystore. +Generate a mnemonic and export it to a JSON keystore: ``` provider = new UserWalletProvider() @@ -32,7 +32,7 @@ keystore = EncryptedKeystore.new_from_mnemonic(provider, mnemonic) keystore.export_to_file("wallet.json", "password", "erd") ``` -Loading a JSON keystore which holds the encrypted mnemonic, then iterate over the first 3 accounts: +Load a JSON keystore which holds an encrypted mnemonic, then iterate over the first 3 accounts: ``` provider = new UserWalletProvider() @@ -40,13 +40,13 @@ keystore = EncryptedKeystore.import_from_file(provider, "wallet.json", "password mnemonic = keystore.get_mnemonic() for i in [0, 1, 2]: - secret_key = provider.derive_secret_key_from_mnemonic(mnemonic, address_index=i, passphrase="") - public_key = provider.compute_public_key_from_secret_key(secret_key) - address = new Address(public_key, "erd") + sk = provider.derive_secret_key_from_mnemonic(mnemonic, address_index=i, passphrase="") + pk = provider.compute_public_key_from_secret_key(sk) + address = new Address(pk, "erd") print("Address", i, address.bech32()) ``` -Changing the password of an existing keystore: +Change the password of a JSON keystore: ``` provider = new UserWalletProvider() @@ -54,26 +54,26 @@ keystore = EncryptedKeystore.import_from_file(provider, "wallet.json", "password keystore.export_to_file("wallet.json", "new_password", "erd") ``` -Loading a PEM keystore and sign a piece of data: +Load a PEM keystore and sign a piece of data: ``` provider = new UserWalletProvider() keystore = PEMKeystore.import_from_file(provider, "wallet.pem") -first_secret_key = keystore.get_secret_key(0) -signature = provider.sign(data, first_secret_key) +sk = keystore.get_secret_key(0) +signature = provider.sign(data, sk) ``` -Loading a JSON keystore and sign data: +Load a JSON keystore and sign data: ``` provider = new UserWalletProvider() keystore = EncryptedKeystore.import_from_file(provider, "wallet.json", "password") if keystore.get_kind() == "secretKey": - first_secret_key = keystore.get_secret_key() + sk = keystore.get_secret_key() else: mnemonic = keystore.get_mnemonic() - first_secret_key = provider.derive_secret_key_from_mnemonic(mnemonic, address_index=0, passphrase="") + sk = provider.derive_secret_key_from_mnemonic(mnemonic, address_index=0, passphrase="") -signature = provider.sign(data, first_secret_key) +signature = provider.sign(data, sk) ``` From b607a8a96b91bf5354abaecd2de998f558b02e5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Thu, 14 Dec 2023 14:54:52 +0200 Subject: [PATCH 07/10] Fix after review. --- sdk-wallet/cookbook.md | 15 +++++++++++++++ sdk-wallet/keystores/encrypted_keystore.md | 15 ++++++++------- sdk-wallet/user_wallet_provider.md | 2 +- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/sdk-wallet/cookbook.md b/sdk-wallet/cookbook.md index baf904a..67ad1b4 100644 --- a/sdk-wallet/cookbook.md +++ b/sdk-wallet/cookbook.md @@ -1,6 +1,8 @@ ## Cookbook (examples of usage) +Note: error handling is out of scope for this cookbook (omitted for brevity). + Generate a mnemonic and derive the first secret key: ``` @@ -46,6 +48,19 @@ for i in [0, 1, 2]: print("Address", i, address.bech32()) ``` +Load a PEM keystore, then iterate over the first 3 accounts: + +``` +provider = new UserWalletProvider() +keystore = PEMKeystore.import_from_file(provider, "wallet.pem") + +for i in [0, 1, 2]: + sk = keystore.get_secret_key(i) + pk = provider.compute_public_key_from_secret_key(sk) + address = new Address(pk, "erd") + print("Address", i, address.bech32()) +``` + Change the password of a JSON keystore: ``` diff --git a/sdk-wallet/keystores/encrypted_keystore.md b/sdk-wallet/keystores/encrypted_keystore.md index 6ad6c6e..5276087 100644 --- a/sdk-wallet/keystores/encrypted_keystore.md +++ b/sdk-wallet/keystores/encrypted_keystore.md @@ -6,18 +6,19 @@ Note: current design is suboptimal. `EncryptedKeystore` handles both legacy keys ``` class EncryptedKeystore: - // The constructor is not strictly captured by the specs; it's up to the implementing library to define it. Suggestion below. - // If kind == 'secretKey', `secret_key` must be provided, while `mnemonic` must be nil. - // If kind == 'mnemonic', `secret_key` must be nil, while `mnemonic` must be provided. - // In the implementation, all the parameters would be held as instance state (private fields). - private constructor(keys_computer: IKeysComputer, kind: string, secret_key?: ISecretKey, mnemonic?: Mnemonic) + // The constructor is not captured by the specs; it's up to the implementing library to define it. + // Instance fields to be held (suggestion): + // - keys_computer: IKeysComputer + // - kind: "secretKey|mnemonic" + // - secretKey: ISecretKey (will be nil if kind == 'mnemonic') + // - mnemonic: Mnemonic (will be nil if kind == 'secretKey') // Named constructor - // This should have a trivial implementation (e.g. a wrapper around the private constructor). + // This should have a trivial implementation. static new_from_secret_key(keys_computer: IKeysComputer, secret_key: ISecretKey): EncryptedKeystore // Named constructor - // This should have a trivial implementation (e.g. a wrapper around the private constructor). + // This should have a trivial implementation. static new_from_mnemonic(keys_computer: IKeysComputer, mnemonic: Mnemonic): EncryptedKeystore // Importing "constructor" diff --git a/sdk-wallet/user_wallet_provider.md b/sdk-wallet/user_wallet_provider.md index 36f8929..962f622 100644 --- a/sdk-wallet/user_wallet_provider.md +++ b/sdk-wallet/user_wallet_provider.md @@ -2,7 +2,7 @@ Provides functionality for generating secret keys, derivating public keys, signing & verifying data. -Additionally, allows one to generate and validate mnemonics, and to deriving secret keys from mnemonics (i.e. BIP39). +Additionally, allows one to generate and validate mnemonics, and to derive secret keys from mnemonics (i.e. BIP39). ``` class UserWalletProvider implements ISigner, IVerifier, IKeysGenerator, IMnemonicComputer: From de13d6c7eecc2b06f12d337524c0911cfd0f6d50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Fri, 15 Dec 2023 22:42:43 +0200 Subject: [PATCH 08/10] Move (and rename) " KeyfileObject" -> "EncryptedKeystoreContent". Add extra explanations and notes. --- .../password_based_encryptor_decryptor.md | 6 ++ sdk-wallet/crypto/password_encrypted_data.md | 11 ++- sdk-wallet/keystores/encrypted_keystore.md | 52 -------------- .../keystores/encrypted_keystore_content.md | 71 +++++++++++++++++++ 4 files changed, 87 insertions(+), 53 deletions(-) create mode 100644 sdk-wallet/keystores/encrypted_keystore_content.md diff --git a/sdk-wallet/crypto/password_based_encryptor_decryptor.md b/sdk-wallet/crypto/password_based_encryptor_decryptor.md index eb2989f..0cbeffe 100644 --- a/sdk-wallet/crypto/password_based_encryptor_decryptor.md +++ b/sdk-wallet/crypto/password_based_encryptor_decryptor.md @@ -1,5 +1,11 @@ ## PasswordBasedEncryptorDecryptor +Implementation suggestions: + - https://github.com/multiversx/mx-sdk-py-wallet/blob/v0.8.3/multiversx_sdk_wallet/crypto/encryptor.py + - https://github.com/multiversx/mx-sdk-py-wallet/blob/v0.8.3/multiversx_sdk_wallet/crypto/decryptor.py + - https://github.com/multiversx/mx-sdk-js-wallet/blob/v4.2.1/src/crypto/encryptor.ts + - https://github.com/multiversx/mx-sdk-js-wallet/blob/v4.2.1/src/crypto/decryptor.ts + ``` class PasswordBasedEncryptorDecryptor: encrypt(data: bytes, password: string): PasswordEncryptedData; diff --git a/sdk-wallet/crypto/password_encrypted_data.md b/sdk-wallet/crypto/password_encrypted_data.md index f184b14..5bd6840 100644 --- a/sdk-wallet/crypto/password_encrypted_data.md +++ b/sdk-wallet/crypto/password_encrypted_data.md @@ -1,5 +1,14 @@ ## PasswordEncryptedData +Implementation suggestions: + - `EncryptedData`: https://github.com/multiversx/mx-sdk-js-wallet/blob/v4.2.1/src/crypto/encryptedData.ts + - `EncryptedData`: https://github.com/multiversx/mx-sdk-py-wallet/blob/v0.8.3/multiversx_sdk_wallet/crypto/encrypted_data.py + +**Note:** this DTO is not a representation of the JSON keystore itself. Instead, it defines a data structure that is **agnostic to the type of file it might be stored in**. This abstraction was introduced July 2021: + - https://github.com/multiversx/mx-sdk-js-core/pull/11 + +In practice, such a data structure is, indeed, stored in a JSON keystore file, whose schema does resemble the DTO below - but it's not identical. Any similarities between `PasswordEncryptedData` and `EncryptedKeystoreContent` **should not be considered duplication**. + ``` dto PasswordEncryptedData: id: string; @@ -13,7 +22,7 @@ dto PasswordEncryptedData: mac: string; // Example of class compatible with "PasswordEncryptedData.kdfparams". -// Can be anything, and it's specific to a "PasswordEncryptedData.kdf" (e.g. "scrypt"). +// Can be anything, and it's correlated to a "PasswordEncryptedData.kdf" (e.g. "scrypt"). // EncryptorDecryptor.encrypt() puts this into "PasswordEncryptedData.kdfparams". // EncryptorDecryptor.decrypt() interprets (possibly validates) it. dto ScryptKeyDerivationParams: diff --git a/sdk-wallet/keystores/encrypted_keystore.md b/sdk-wallet/keystores/encrypted_keystore.md index 5276087..1f9d3b2 100644 --- a/sdk-wallet/keystores/encrypted_keystore.md +++ b/sdk-wallet/keystores/encrypted_keystore.md @@ -50,55 +50,3 @@ class EncryptedKeystore: export_to_file(path: Path, password: string, address_hrp: string) ``` - -``` -dto KeyfileObject: - version: number; - - // "secretKey|mnemonic" - kind: string; - - // a GUID - id: string - - // hex representation of the address - address: string - - // bech32 representation of the address - bech32: string - - crypto: { - // PasswordEncryptedData.ciphertext - ciphertext: string; - - cipherparams: { - // PasswordEncryptedData.iv - iv: string; - }; - - // PasswordEncryptedData.cipher - cipher: string; - - // PasswordEncryptedData.kdf - kdf: string; - kdfparams: { - // PasswordEncryptedData.kdfparams.n - n: number; - - // PasswordEncryptedData.kdfparams.r - r: number; - - // PasswordEncryptedData.kdfparams.p - p: number; - - // PasswordEncryptedData.kdfparams.dklen - dklen: number; - - // PasswordEncryptedData.salt - salt: string; - }; - - // PasswordEncryptedData.mac - mac: string; - }; -``` diff --git a/sdk-wallet/keystores/encrypted_keystore_content.md b/sdk-wallet/keystores/encrypted_keystore_content.md new file mode 100644 index 0000000..4c68444 --- /dev/null +++ b/sdk-wallet/keystores/encrypted_keystore_content.md @@ -0,0 +1,71 @@ +## EncryptedKeystoreContent + +Also see (mere references, not implementation suggestions): + - `encryptedKeyJSONV4`: https://github.com/multiversx/mx-sdk-go/blob/v1.3.8/interactors/wallet.go + - Transforming the keystore content into an `PasswordEncryptedData`: https://github.com/multiversx/mx-sdk-js-wallet/blob/v4.2.1/src/userWallet.ts#L127 + +`EncryptedKeystoreContent` DTO is a representation of the JSON keystore file. + +Generally speaking, decrypting the payload of a keystore file happens as follows (high-level): + - the content of the keystore file is loaded into a `EncryptedKeystoreContent`. + - the `EncryptedKeystoreContent` is mapped (converted) to a `PasswordEncryptedData` + - the `PasswordEncryptedData` is fed to an `IPasswordBasedDecryptor` (e.g. `PasswordBasedEncryptorDecryptor`) for decryption. + - the decrypted payload is interpreted downstream (e.g. as a secret key, mnemonic etc.). + + +Generally speaking, saving a confidential payload into a keystore file happens as follows (high-level): + - the payload is encrypted using an `IPasswordBasedEncryptor` (e.g. `PasswordBasedEncryptorDecryptor`). What results is a `PasswordEncryptedData`. + - the resulted `PasswordEncryptedData` is mapped (converted) to an `EncryptedKeystoreContent`. + - the `EncryptedKeystoreContent` is written to a file. + +``` +dto EncryptedKeystoreContent: + version: number; + + // "secretKey|mnemonic" + kind: string; + + // a GUID + id: string + + // hex representation of the address + address: string + + // bech32 representation of the address + bech32: string + + crypto: { + // PasswordEncryptedData.ciphertext + ciphertext: string; + + cipherparams: { + // PasswordEncryptedData.iv + iv: string; + }; + + // PasswordEncryptedData.cipher + cipher: string; + + // PasswordEncryptedData.kdf + kdf: string; + kdfparams: { + // PasswordEncryptedData.kdfparams.n + n: number; + + // PasswordEncryptedData.kdfparams.r + r: number; + + // PasswordEncryptedData.kdfparams.p + p: number; + + // PasswordEncryptedData.kdfparams.dklen + dklen: number; + + // PasswordEncryptedData.salt + salt: string; + }; + + // PasswordEncryptedData.mac + mac: string; + }; +``` From baa0066bec255225033a4e5cd48e977d9002a2fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Wed, 20 Dec 2023 16:00:39 +0200 Subject: [PATCH 09/10] Define (sketch) wallet controllers. --- .../controllers/ledger_wallet_controller.md | 35 +++++++++++++++++++ .../controllers/mnemonic_wallet_controller.md | 27 ++++++++++++++ .../secret_key_wallet_controller.md | 27 ++++++++++++++ 3 files changed, 89 insertions(+) create mode 100644 sdk-wallet/controllers/ledger_wallet_controller.md create mode 100644 sdk-wallet/controllers/mnemonic_wallet_controller.md create mode 100644 sdk-wallet/controllers/secret_key_wallet_controller.md diff --git a/sdk-wallet/controllers/ledger_wallet_controller.md b/sdk-wallet/controllers/ledger_wallet_controller.md new file mode 100644 index 0000000..3bfcaf4 --- /dev/null +++ b/sdk-wallet/controllers/ledger_wallet_controller.md @@ -0,0 +1,35 @@ +## LedgerWalletController + +Note: though the Ledger device supports `account_index`, as well, this is not used in the context of MultiversX. For all purposes, `account_index` is `0`, while `address_index` is allowed to vary. + +References (not implementation suggestions): +- https://github.com/multiversx/mx-sdk-js-hw-provider/blob/v6.4.0/src/ledgerApp.ts +- https://github.com/multiversx/mx-sdk-py-cli/blob/main/multiversx_sdk_cli/ledger/ledger_app_handler.py + +``` +class LedgerWalletController: + // The constructor is not strictly captured by the specs; it's up to the implementing library to define it. + // Generally speaking, the constructor would take as input: a "IKeysComputer", an "IAddressComputer". + + // Returns the configuration of the MultiversX App. + get_configuration(): LedgerAppConfiguration; + + get_address(address_index: number): Address; + + get_current_address(): Address; + + select_current_address(address_index: number); + + // Part of the Native Authentication flow. + sign_auth_token(auth_token: bytes): bytes; + + sign_transaction(serialized_transaction: bytes): bytes; + sign_message(serialized_message: bytes): bytes; +``` + +``` +dto LedgerAppConfiguration: + is_transaction_data_allowed: boolean; + address_index: number; + version: string; +``` diff --git a/sdk-wallet/controllers/mnemonic_wallet_controller.md b/sdk-wallet/controllers/mnemonic_wallet_controller.md new file mode 100644 index 0000000..bc9363c --- /dev/null +++ b/sdk-wallet/controllers/mnemonic_wallet_controller.md @@ -0,0 +1,27 @@ +## MnemonicWalletController + +``` +class MnemonicWalletController implements IWalletController: + // The constructor is not strictly captured by the specs; it's up to the implementing library to define it. + // Generally speaking, the constructor would take as input: a "Mnemonic", an "IMnemonicComputer", a "IKeysComputer", an "IAddressComputer". + // Generally speaking, upon construction, the selected address would be the one at index 0. + + // Generally speaking, this would use the underlying mnemonic computer to derive the secret key, + // then compute the public key and, ultimately, the address. + get_address(address_index: number): Address; + + get_current_address(): Address; + + // Generally speaking, this would use the underlying mnemonic computer to derive the secret key. + select_current_address(address_index: number); + + // Part of the Native Authentication flow. + sign_auth_token(auth_token: bytes): bytes; + + // Kept separate from "sign_message", for clarity, and also because some wallet controllers require separate signing flows (e.g. Ledger). + sign_transaction(serialized_transaction: bytes): bytes; + // Kept separate from "sign_transaction", for clarity, and also because some wallet controllers require separate signing flows (e.g. Ledger). + sign_message(serialized_message: bytes): bytes; +``` + +**Note:** mnemonics cannot be retrieved from the wallet controller - at least, not directly; this use case is not supported by the specs. Wallet-like applications are responsible to enable support for retrieving the mnemonic, if they wish to do so (e.g. for display). diff --git a/sdk-wallet/controllers/secret_key_wallet_controller.md b/sdk-wallet/controllers/secret_key_wallet_controller.md new file mode 100644 index 0000000..6e90dee --- /dev/null +++ b/sdk-wallet/controllers/secret_key_wallet_controller.md @@ -0,0 +1,27 @@ +## SecretKeyWalletController + +``` +class SecretKeyWalletController implements IWalletController: + // The constructor is not strictly captured by the specs; it's up to the implementing library to define it. + // Generally speaking, the constructor would take as input: an "ISecretKey", a "IKeysComputer", an "IAddressComputer". + // If desired, the implementing library can support more than one secret key, but this is not required. + // Generally speaking, upon construction, the selected address would be the one at index 0. + + // Generally speaking, this throws if "address_index" is not 0. Though, implementations can choose to support more than one secret key, if desired. + get_address(address_index: number): Address; + + get_current_address(): Address; + + // Generally speaking, this throws if "address_index" is not 0. Though, implementations can choose to support more than one secret key, if desired. + select_current_address(address_index: number); + + // Part of the Native Authentication flow. + sign_auth_token(auth_token: bytes): bytes; + + // Kept separate from "sign_message", for clarity, and also because some wallet controllers require separate signing flows (e.g. Ledger). + sign_transaction(serialized_transaction: bytes): bytes; + // Kept separate from "sign_transaction", for clarity, and also because some wallet controllers require separate signing flows (e.g. Ledger). + sign_message(serialized_message: bytes): bytes; +``` + +**Note:** secret keys cannot be retrieved from the wallet controller - at least, not directly; this use case is not supported by the specs. Wallet-like applications are responsible to enable support for retrieving the secret key, if they wish to do so (e.g. for display). From 360f545de07417d77b4d31c9236262e518be5882 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrei=20B=C4=83ncioiu?= Date: Wed, 20 Dec 2023 19:59:22 +0200 Subject: [PATCH 10/10] Define interface, mention errors etc. --- sdk-wallet/README.md | 37 +++++++++++++++---- .../secret_key_wallet_controller.md | 4 ++ sdk-wallet/interfaces.md | 10 +++++ 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/sdk-wallet/README.md b/sdk-wallet/README.md index 63c05a2..c5a3e13 100644 --- a/sdk-wallet/README.md +++ b/sdk-wallet/README.md @@ -1,13 +1,36 @@ -## Keystores +## Package `controllers` -Components in `sdk-wallet/keystore`, even if they do share a common purpose - storing secret keys - are not meant to be used interchangeably. Generally speaking, they do not (are not required to) share a common interface. If needed (for exotic use-cases), client code can design customized adapters over these components in order to unify their interfaces (this is not covered by specs). +This package holds the most high-level components of **sdk-wallet**, the ones generally used to sign transactions and messages. -**Implementation detail:** an instance of `EncryptedKeystore` (which is a wrapper over the well-known JSON wallet) holds decrypted data within its state. +```mermaid +classDiagram +IWalletProvider <|-- LedgerWalletController : implements +IWalletProvider <|-- SecretKeyWalletController : implements +IWalletProvider <|-- MnemonicWalletController : implements +<> IWalletProvider +IWalletProvider : get_address(address_index) +IWalletProvider : get_current_address() +IWalletProvider : select_current_address(address_index) +IWalletProvider : sign_auth_token(auth_token) +IWalletProvider : sign_transaction(serialized_transaction) +IWalletProvider : sign_message(serialized_message) +``` -**Design detail:** components in `sdk-wallet/keystore` should not depend on `Address` within their public interface (though they are allowed to depend on it within their implementation). For example, the `export` functionality of `EncryptedKeystore` requires the functionality provided by `Address` (conversion from public key bytes to bech32 representation). +## Package `crypto` + +This package must be agnostic to all the other ones. Though, for languages that do no support **structural typing** (e.g. C#), the package is allowed to depend on the interfaces `ISecretKey` and `IPublicKey` (which are externally defined). In all other cases, the package must be completely self-contained and independent. + +## Package `keystores` + +TBD + +**Implementation detail:** an instance of `EncryptedKeystoreWithMnemonic` or `EncryptedKeystoreWithSecretKey` (wrappers over the well-known JSON wallet) hold decrypted data within their state. + +**Design detail:** components in `sdk-wallet/keystore` should not reference `Address` within their public interface (though they are allowed to depend on it within their implementation). + +### Useful links -### References: - https://github.com/ethereumjs/keythereum - - https://github.com/ethereumjs/ethereumjs-wallet/blob/master/docs/classes/wallet.md - - https://github.com/multiversx/mx-sdk-js-wallet/blob/main/src/userWallet.ts + - https://github.com/ethereumjs/ethereumjs-wallet/blob/v1.0.2/docs/classes/wallet.md + - https://github.com/multiversx/mx-sdk-js-wallet/blob/v4.2.1/src/userWallet.ts - https://github.com/multiversx/mx-sdk-py-wallet/blob/main/multiversx_sdk_wallet/user_wallet.py diff --git a/sdk-wallet/controllers/secret_key_wallet_controller.md b/sdk-wallet/controllers/secret_key_wallet_controller.md index 6e90dee..9accc6b 100644 --- a/sdk-wallet/controllers/secret_key_wallet_controller.md +++ b/sdk-wallet/controllers/secret_key_wallet_controller.md @@ -8,11 +8,15 @@ class SecretKeyWalletController implements IWalletController: // Generally speaking, upon construction, the selected address would be the one at index 0. // Generally speaking, this throws if "address_index" is not 0. Though, implementations can choose to support more than one secret key, if desired. + // Can throw: + // - ErrAddressNotAvailable get_address(address_index: number): Address; get_current_address(): Address; // Generally speaking, this throws if "address_index" is not 0. Though, implementations can choose to support more than one secret key, if desired. + // Can throw: + // - ErrAddressNotAvailable select_current_address(address_index: number); // Part of the Native Authentication flow. diff --git a/sdk-wallet/interfaces.md b/sdk-wallet/interfaces.md index 289cb2e..4258fe1 100644 --- a/sdk-wallet/interfaces.md +++ b/sdk-wallet/interfaces.md @@ -4,6 +4,16 @@ For languages that support **structural typing** (e.g. Go, Python, TypeScript), For languages that only support **nominal typing** (e.g. C#), these interfaces can be _exported_. +``` +interface IWalletController: + get_address(address_index: number): Address; + get_current_address(): Address; + select_current_address(address_index: number); + sign_auth_token(auth_token: bytes): bytes; + sign_transaction(serialized_transaction: bytes): bytes; + sign_message(serialized_message: bytes): bytes; +``` + ``` interface ISigner: sign(data: bytes, secret_key: ISecretKey): bytes