Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sdk-wallet: some refactoring, additional explanations #32

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
79 changes: 79 additions & 0 deletions sdk-wallet/cookbook.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@

## Cookbook (examples of usage)

Generate a mnemonic and derive the first secret key:
iulianpascalau marked this conversation as resolved.
Show resolved Hide resolved

```
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(pk, "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")
```

Load a JSON keystore which holds an 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]:
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())
```

Change the password of a JSON keystore:

```
provider = new UserWalletProvider()
keystore = EncryptedKeystore.import_from_file(provider, "wallet.json", "password")
keystore.export_to_file("wallet.json", "new_password", "erd")
```

Load a PEM keystore and sign a piece of data:

```
provider = new UserWalletProvider()
keystore = PEMKeystore.import_from_file(provider, "wallet.pem")
sk = keystore.get_secret_key(0)
iulianpascalau marked this conversation as resolved.
Show resolved Hide resolved
signature = provider.sign(data, sk)
```

Load a JSON keystore and sign data:

```
provider = new UserWalletProvider()
keystore = EncryptedKeystore.import_from_file(provider, "wallet.json", "password")

if keystore.get_kind() == "secretKey":
sk = keystore.get_secret_key()
else:
mnemonic = keystore.get_mnemonic()
sk = provider.derive_secret_key_from_mnemonic(mnemonic, address_index=0, passphrase="")

signature = provider.sign(data, sk)
```
7 changes: 7 additions & 0 deletions sdk-wallet/crypto/keypair_based_encryptor_decryptor.md
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
5 changes: 5 additions & 0 deletions sdk-wallet/crypto/public_key_encrypted_data.md
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
16 changes: 12 additions & 4 deletions sdk-wallet/interfaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Copy link
Contributor Author

@andreibancioiu andreibancioiu Dec 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IWalletProvider was segregated.

See: #31 (comment).

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:
Copy link

@axenteoctavian axenteoctavian Dec 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In User/Validator wallet provider this is called IKeysGenerator. Which name is correct?

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
```

Expand Down
71 changes: 27 additions & 44 deletions sdk-wallet/keystores/encrypted_keystore.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,48 @@
## 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.
Copy link
Contributor Author

@andreibancioiu andreibancioiu Dec 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question for review: all right for the moment? 🙏

Also, see the example(s) in cookbook.md.


```
class EncryptedKeystore:
// 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.
Copy link

@iulianpascalau iulianpascalau Dec 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove this private constructor, it's out of the scope of the provided specs. We should have only those 2 named constructors as example
+ providing a type on which the constructor will do a switch is not a very good example (clean code wise)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed phrasing, removed private constructor. No switch should be made in the implementation.

// 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)

// 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(keys_computer: IKeysComputer, 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).
static new_from_mnemonic(wallet_provider: IWalletProvider, mnemonic: Mnemonic): EncryptedKeystore
// This should have a trivial implementation (e.g. a wrapper around the private constructor).
static new_from_mnemonic(keys_computer: IKeysComputer, mnemonic: Mnemonic): EncryptedKeystore

// Importing "constructor"
static import_from_object(wallet_provider: IWalletProvider, object: KeyfileObject, password: string): EncryptedKeystore
// This should decrypt the encrypted data, then call the (private) constructor.
// "password" should not be retained.
static import_from_object(keys_computer: IKeysComputer, object: KeyfileObject, password: string): EncryptedKeystore

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

object: KeyfileObject should be something like keystore_content: EncryptedKeystoreContent


// Importing "constructor"
static import_from_file(wallet_provider: IWalletProvider, path: Path, password: string): EncryptedKeystore
// This should load the file content, decrypt the encrypted data, then call the (private) constructor.
// "password" should not be retained.
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
Expand Down Expand Up @@ -86,35 +101,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")
```
14 changes: 9 additions & 5 deletions sdk-wallet/keystores/pem_keystore.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
38 changes: 0 additions & 38 deletions sdk-wallet/mnemonic.md
Original file line number Diff line number Diff line change
Expand Up @@ -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())
```
18 changes: 17 additions & 1 deletion sdk-wallet/user_wallet_provider.md
Original file line number Diff line number Diff line change
@@ -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).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deriving => derive

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.


```
class UserWalletProvider implements IWalletProvider:
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.

Expand All @@ -28,4 +32,16 @@ class UserWalletProvider implements IWalletProvider:
// 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
Comment on lines +35 to +46
Copy link
Contributor Author

@andreibancioiu andreibancioiu Dec 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MnemonicComputer has been merged into this.

```
2 changes: 1 addition & 1 deletion sdk-wallet/validator_wallet_provider.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down