Draft: Added support for the different SecretFormat within the Vault
- Adapted vault items filename to "{address}-{secret_format}" for new entries
- Kept backward compatibility to still read "substrate" vault items from disk (without the SecretFormat in the filename)
- Refactored methods in keys.rs and vault.rs to accommodate for the changes without duplicating code (i.e. for the secret prompts)
- for "vault import" command, kept SecretFormat.Substrate as default so it's not mandatory to give the "-S" parameter
Performed manual tests using accounts:
- 5Funkng8dL397H9ZEJ9sXDjFhStCqRYUj1a62ngcrEMy7ivn
- Identity: Nicolas80
- Kept as "old" vault entry
~/.local/share/gcli/5Funkng8dL397H9ZEJ9sXDjFhStCqRYUj1a62ngcrEMy7ivn
- 5HE6gH87fVuGj6akXneNceBtgEiaUZua43TJbVBRQTT77gp6-substrate
- Identity: Nicolas80-GDev
- 5GhAZBagx87sTGfMppfPPcWhxCKGWE8zDi1oHp7YiKosD9KZ-cesium
- (old G1v1 for which I moved identity to 5Funkng8dL397H9ZEJ9sXDjFhStCqRYUj1a62ngcrEMy7ivn)
- 5GNadia4kktgt3Zg3QgTKL8Lse78XXMcisnRhkPKA4ombYDm-seed
- New test account without Identity and created from seed only
- 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
- predefined Alice key
Saving the different key types in the vault
gcli vault list
available keys:
5Funkng8dL397H9ZEJ9sXDjFhStCqRYUj1a62ngcrEMy7ivn
gcli vault import --help
Import key from (substrate)mnemonic or other format with interactive prompt
Usage: gcli vault import [OPTIONS]
Options:
-S, --secret-format <SECRET_FORMAT> Secret key format (substrate, seed, cesium) [default: substrate]
-h, --help Print help
# Using default "substrate" secret format
gcli vault import
Mnemonic:
Enter password to protect the key
Password:
Stored secret for 5HE6gH87fVuGj6akXneNceBtgEiaUZua43TJbVBRQTT77gp6
gcli vault import -S cesium
Cesium id:
Cesium password:
Enter password to protect the key
Password:
Stored secret for 5GhAZBagx87sTGfMppfPPcWhxCKGWE8zDi1oHp7YiKosD9KZ
gcli vault import -S seed
Seed:
Enter password to protect the key
Password:
Stored secret for 5GNadia4kktgt3Zg3QgTKL8Lse78XXMcisnRhkPKA4ombYDm
# Just for reference, also work for predefined key - but probably not useful
# Using "Alice" as derivation here
gcli vault import -S predefined
Enter derivation path:
Enter password to protect the key
Password:
Stored secret for 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
gcli vault list
available keys:
5Funkng8dL397H9ZEJ9sXDjFhStCqRYUj1a62ngcrEMy7ivn
5GNadia4kktgt3Zg3QgTKL8Lse78XXMcisnRhkPKA4ombYDm-seed
5GhAZBagx87sTGfMppfPPcWhxCKGWE8zDi1oHp7YiKosD9KZ-cesium
5HE6gH87fVuGj6akXneNceBtgEiaUZua43TJbVBRQTT77gp6-substrate
5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY-predefined
Using the different vaults for transfers
# Using -a to select the source address for all commands
gcli -a 5Funkng8dL397H9ZEJ9sXDjFhStCqRYUj1a62ngcrEMy7ivn account transfer 101 5HE6gH87fVuGj6akXneNceBtgEiaUZua43TJbVBRQTT77gp6
Enter password to unlock account 5Funkng8dL397H9ZEJ9sXDjFhStCqRYUj1a62ngcrEMy7ivn
Password:
transaction submitted to the network, waiting 6 seconds...
transfered 1.01 ĞD (5Funkng8dL397H9ZEJ9sXDjFhStCqRYUj1a62ngcrEMy7ivn → 5HE6gH87fVuGj6akXneNceBtgEiaUZua43TJbVBRQTT77gp6)
gcli -a 5GNadia4kktgt3Zg3QgTKL8Lse78XXMcisnRhkPKA4ombYDm account transfer 101 5HE6gH87fVuGj6akXneNceBtgEiaUZua43TJbVBRQTT77gp6
Enter password to unlock account 5GNadia4kktgt3Zg3QgTKL8Lse78XXMcisnRhkPKA4ombYDm
Password:
transaction submitted to the network, waiting 6 seconds...
transfered 1.01 ĞD (5GNadia4kktgt3Zg3QgTKL8Lse78XXMcisnRhkPKA4ombYDm → 5HE6gH87fVuGj6akXneNceBtgEiaUZua43TJbVBRQTT77gp6)
gcli -a 5GhAZBagx87sTGfMppfPPcWhxCKGWE8zDi1oHp7YiKosD9KZ account transfer 101 5HE6gH87fVuGj6akXneNceBtgEiaUZua43TJbVBRQTT77gp6
Enter password to unlock account 5GhAZBagx87sTGfMppfPPcWhxCKGWE8zDi1oHp7YiKosD9KZ
Password:
transaction submitted to the network, waiting 6 seconds...
transfered 1.01 ĞD (5GhAZBagx87sTGfMppfPPcWhxCKGWE8zDi1oHp7YiKosD9KZ → 5HE6gH87fVuGj6akXneNceBtgEiaUZua43TJbVBRQTT77gp6)
gcli -a 5HE6gH87fVuGj6akXneNceBtgEiaUZua43TJbVBRQTT77gp6 account transfer 101 5GNadia4kktgt3Zg3QgTKL8Lse78XXMcisnRhkPKA4ombYDm
Enter password to unlock account 5HE6gH87fVuGj6akXneNceBtgEiaUZua43TJbVBRQTT77gp6
Password:
transaction submitted to the network, waiting 6 seconds...
transfered 1.01 ĞD (5HE6gH87fVuGj6akXneNceBtgEiaUZua43TJbVBRQTT77gp6 → 5GNadia4kktgt3Zg3QgTKL8Lse78XXMcisnRhkPKA4ombYDm)
# Using the address of the predefined key actually doesn't request the password (it's not even searching in the vault)
gcli -a 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY account transfer 101 5HE6gH87fVuGj6akXneNceBtgEiaUZua43TJbVBRQTT77gp6
transaction submitted to the network, waiting 6 seconds...
transfered 1.01 ĞD (5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY → 5HE6gH87fVuGj6akXneNceBtgEiaUZua43TJbVBRQTT77gp6)
I'm still new to rust, so don't hesitate to give suggestions
Merge request reports
Activity
Good idea to use the filename to store the format.
An other option would be to add a proper format (eventually by adding a
.<format>
to the name) and have an internal format with more options like:- a name
- crypto type
- ...
This way we could have something like
alice.keyformat
which is encrypted file of a json containing:{ "name": "alice", "crypto_type": "ed25519", "secret_type": "mnemonic", "secret": "<mnemonic>", "more": 123 }
Which would allow to make some command like:
# suggestion of command $ vault use alice account 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY selected
(similar to what was suggested in #36)
I wonder if such format already exists so that we do not have to re-develop one.
Just to avoid confusion in this discussion:
-
Address
- SS58 Substrate Public Key
-
Identity
- Optional username linked to an Address
-
VaultItemName
- Optional vault item name linked to an Address
I read #36 and for that one; I wonder how to avoid potential confusion between a VaultItemName and an optional Identity name - In my test cases above, I have several Address that don't have any Identity.
But if all the commands using that VaultItemName explicitly mention something like "<vault_item_name> or <Address>" for a command such as
gcli vault use ...
I guess it could be fine.And having something more standardised/structured for the vault entries would be a good idea as well; we just have to think of what can work with all current usages + the ones we want to add.
For now, a vault entry is selected through the current Address - which is the only thing that should always exist for any kind of vault entry.
1. completely encrypted Vault entry
Let's say we now use vault filenames named one of
<VaultItemName>.keyformat <Address>.keyformat
With encrypted content
{ #If it's encrypted; that one is useless to retrieve the correct Vault entry #I expect this one to be VaultItemName - not Identity which can change independently "name": "Alice", "crypto_type": "ed25519", "secret_type": "mnemonic", "secret": "<mnemonic>", "more": 123 }
1.1 Using VaultItemName
In the case we had
<VaultItemName>.keyformat
for sayAlice.keyformat
Commands using
-a <Address>
will not be able to find a Vault entry even if it was for the proper Address of AliceUnless we keep a part of the vault entry un-enctrypted and with an extra Address property (point 2)
Or we need to format file names more strictly - say one of
<Address>.keyformat <Address>_<VaultItemName>.keyformat
Which means if searching for VaultItemName we need to parse all file names until we have a match
Also tricky to prevent using the same VaultItemName more than once
2. Partially encrypted Vault entry
Content
{ # Un-encrypted value of VaultItemName "name": "Alice", # Un-encrypted value of Address "address": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY", # value could be encrypted "crypto_type": "ed25519", # value could be encrypted "secret_type": "mnemonic", # value MUST be encrypted - and still need to support dual secret "secret": "<mnemonic>", # value could be encrypted "more": 123 }
In this case I don't even think the file name is still relevant, because unless we put both the Address and the VaultItemName in it (filename), we won't be able to find it back with the other one.
So I would keep the original naming + extension
<Address>.keyformat
And we would have to parse all existing vault entries at the start of the gcli command and keep maps of
- VaultItemName => Address
- Address => vault_item_filepath
Which would also allow us to prevent naming a vault entry with a name already used in the map
... I'll stop here for those first thoughts; maybe you can check those and give me your feedback
-
Extra idea for the VaultItemName to Address mapping & filename.
Still use the base file names
<Address>.keyformat
And one extra special file - say
vault_item_name.mapping
That could have a json structure like
{ "Alice": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY", "Nicolas": "5...", }
So only one file to parse at startup of every
gcli
command - and we can keep the "keyformat" files fully encrypted...Edited by Nicolas80The single "
.mapping
" file could be a.sqlite
file with the same structure as Tikka's. This would allow to manage derivations and so on. The scope is larger than this MR but it's nice to think ahead to know the direction we are going!You're right that
-a
would not be able to find account if the name of the encrypted file is simplyname.xxx
. But this option is not very intuitive anyway, so we might prefer to have some kind of-v <name>
option to tell which vault to use.This moves from stateless commands to stateful commands where the state contains some context (like the account to target / to use) within the config file.
I pushed a new commit with support for sqlite (I didn't change anything about the vault key files for now).
I added support of mapping Address <=> VaultName and added some extra commands in Vault.
It's normally also possible to use
-a <Address>
or-v VaultName
interchangeablyI also left 2 points for review in there (//TODO & //FIXME).
Since it's my first time using databases with rust; I looked a bit at creates and went with sea-orm as I wanted to have support of entity mapping (not being forced to write the table creation sql).
Can you (or some other dev on the project) review that; and then we can re-discuss the best approach for the vault key files themselves. Since we would now have the database anyway; do we want to store that in there or still on the file system ?
I checked in Tikka for the way they mapped the keys and found 3 entities for it:
Although I don't clearly understand why we would need to have 2 persistence of key in "password" and "wallet"... (apparently, a password entry is only ever created when it's for root account...)
Edit: In fact "password" entity persists the password itself and encrypts it with the keypair (for when user adds derivation keys without a password - fetching the one of the root key) I don't think we want that for GCli - also I don't see the point of persisting several wallets neither - we only actually need the root mnemonic/seed(SR25519)/seed(ED25519)
Also, I have a question regarding non-mnemonic keys; it is possible to use derivation path when working with cesium key (2 secrets) or from a private seed directly ?
Edited by Nicolas80If I understand this part correctly; derivations can also be created from the private seed (called mini-secret)
Actually, it seems it's possible to make derivations for a cesium v1 key (I made a test for it as well as all SecretFormat derivations):
/// Test which verifies that it's possible to derive a key coming from a cesium v1 id & password /// /// Using subkey command with **ed25519** scheme to show we can derive a key from a seed /// and to retrieve expected values. /// /// ##### Without derivation (using seed from the test) /// ``` /// subkey inspect --scheme ed25519 /// URI: /// Secret Key URI `0x2101d2bc68de9ad149c06293bfe489c8608de576c88927aa5439a81be17aae84` is account: /// Network ID: substrate /// Secret seed: 0x2101d2bc68de9ad149c06293bfe489c8608de576c88927aa5439a81be17aae84 /// Public key (hex): 0x697f6bd16ddebf142384e503fd3f3efc39fe5c7be7c693bd98d982403bb6eb74 /// Account ID: 0x697f6bd16ddebf142384e503fd3f3efc39fe5c7be7c693bd98d982403bb6eb74 /// Public key (SS58): 5ET2jhgJFoNQUpgfdSkdwftK8DKWdqZ1FKm5GKWdPfMWhPr4 /// SS58 Address: 5ET2jhgJFoNQUpgfdSkdwftK8DKWdqZ1FKm5GKWdPfMWhPr4 /// ``` /// /// ##### With derivation '//0' (using seed from the test) /// ``` /// subkey inspect --scheme ed25519 /// URI: /// Secret Key URI `0x2101d2bc68de9ad149c06293bfe489c8608de576c88927aa5439a81be17aae84//0` is account: /// Network ID: substrate /// Secret seed: 0x916e95359a49c82e3d84269b90551433352c433eb9bb270fb8cb86e8a6c9ec85 /// Public key (hex): 0x1658ce32f039dff26d83b5282f611cfed8c71296a311417ef737db4e016194de /// Account ID: 0x1658ce32f039dff26d83b5282f611cfed8c71296a311417ef737db4e016194de /// Public key (SS58): 5Ca1HrNxQ4hiekd92Z99fzhfdSAqPy2rUkLBmwLsgLCjeSQf /// SS58 Address: 5Ca1HrNxQ4hiekd92Z99fzhfdSAqPy2rUkLBmwLsgLCjeSQf /// ``` #[test] fn test_cesium_v1_key_derivation() { let cesium_id = "test_cesium_id".to_string(); let cesium_pwd = "test_cesium_pwd".to_string(); let cesium_keypair = pair_from_cesium(cesium_id, cesium_pwd); println!("Nacl keypair: pkey:'0x{}' skey:'0x{}'", hex::encode(cesium_keypair.pkey), hex::encode(cesium_keypair.skey)); let cesium_v1_pubkey = bs58::encode(cesium_keypair.pkey).into_string(); println!("Cesium v1 Pubkey: '{}'", cesium_v1_pubkey); let cesium_v1_address: AccountId = cesium_keypair.pkey.into(); println!("SS58 Address: '{}'", cesium_v1_address); //ed25519 seed **seems** to be the first 32 bytes of the secret key from nacl keypair let mut seed: [u8; 32] = [0; 32]; seed.copy_from_slice(&cesium_keypair.skey[0..32]); println!(); println!("ed25519 seed: '0x{}'", hex::encode(seed)); let ed25519_pair = sp_core::ed25519::Pair::from_seed(&seed); println!(); println!("ed25519 keypair: public:'0x{}' raw_vec:'0x{}'", hex::encode(ed25519_pair.public().0), hex::encode(ed25519_pair.to_raw_vec().as_slice())); let ed25519_address: AccountId = ed25519_pair.public().into(); println!("ed25519 keypair: public SS58 Address:'{}'", ed25519_address); assert_eq!(cesium_v1_address, ed25519_address); // Tested derivation manually with `subkey` command using adapted suri: `0x2101d2bc68de9ad149c06293bfe489c8608de576c88927aa5439a81be17aae84//0` let expected_ss58_address_derivation_0 = "5Ca1HrNxQ4hiekd92Z99fzhfdSAqPy2rUkLBmwLsgLCjeSQf".to_string(); let (derived_ed25519_pair, _seed) = ed25519_pair.derive(Some(sp_core::DeriveJunction::hard(0)).into_iter(), None).unwrap(); println!(); println!("ed25519 derived keypair: public:'0x{}' raw_vec:'0x{}'", hex::encode(derived_ed25519_pair.public().0), hex::encode(derived_ed25519_pair.to_raw_vec().as_slice())); let account_id32 = subxt::utils::AccountId32::from(derived_ed25519_pair.public()); println!("ed25519 derived keypair: public SS58 Address:'{}'", account_id32); assert_eq!(expected_ss58_address_derivation_0, account_id32.to_string()); }
But this requires to use
sp_core::ed25519::Pair
instead ofnacl::sign::Keypair
(unless it's also supported in nacl; but I didn't find it).So my question is: would it be ok to change the code for cesium so that it uses
sp_core::ed25519::Pair
everywhere (except for retrieving the seed from cesium id & secret usingnacl::sign::Keypair
) ?That would also mean:
- removing custom
CesiumSigner
to use what is provided bysp_core::ed25519::Pair
- in the Vault, we could only encrypt the
seed
for cesium accounts instead of both id & secret - ...
Edited by Nicolas80- removing custom
the best approach for the vault key files themselves
The reason I wanted age-encrypted secrets is for portability. Example using rage (https://github.com/str4d/rage):
> gcli vault where /home/hugo/.local/share/gcli > rage --decrypt ~/.local/share/gcli/5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY bottom drive obey lake curtain smoke basket hold race lonely fit walk//Alice
I checked in Tikka for the way they mapped the keys
You can discuss Tikka with vit on the forum.
password entry is only ever created when it's for root account
This can be discussed here: https://forum.duniter.org/t/utilisation-dune-derivation-avec-gcli/12761. The idea of HD wallet is that with known derivation scheme, we can use a single secret to generate multiple keys. The choice of encrypting only one secret or every secret with derivation independantly depends on what we want to achieve. Since gcli does not have persistent memory secret between two runs, I think it does not make sense here as you write below.
So my question is: would it be ok to change the code for cesium so that it uses sp_core::ed25519::Pair everywhere (except for retrieving the seed from cesium id & secret using nacl::sign::Keypair) ?
The point of "Cesium secret" is to be compatible with legacy software. Using something else as nacl would break this, so there is no point doing this. Think of "non-mnemonic keys" as "non secure" since the entropy level is so low that a computer can quickly break this just by brute-forcing all accounts pubkeys.
In the Vault, we could only encrypt the seed
That is an other feature which could be useful. But with each seed you have to know the key derivation function to use if you want to produce the same keypair. And the standard way of producing a high entropy seed is to use standard implementations that usually expose... a mnemonic! (instead of the bare seed or "mini secret").
- Resolved by Hugo Trentesaux
- Resolved by Hugo Trentesaux
- Resolved by Hugo Trentesaux
133 keys::Secret::DualSecret(id, pwd) => { 134 //Making a simple separation of the 2 parts with a newline character 135 let secret = format!("{id}\n{pwd}"); 136 file.write_all(&encrypt(secret.as_bytes(), password).map_err(|e| anyhow!(e))?[..])?; 137 } 138 } 139 140 Ok(address) 91 141 } 92 142 93 /// try get secret in keystore 94 pub fn try_fetch_secret(data: &Data, address: AccountId) -> Result<Option<String>, GcliError> { 95 let path = data.project_dir.data_dir().join(address.to_string()); 143 fn get_vault_key_path(data: &Data, secret_format: SecretFormat, address: String) -> PathBuf { 144 let format_str: &'static str = From::from(secret_format); 145 data.project_dir.data_dir().join(format!("{}-{}", address, format_str)) Having the format inside the file would prevent from having to pass the format here as an argument. And note that "address" is a string, so you could simply wrap this function by giving
format!("{}-{}", address, format_str)
as an argument toget_vault_key_path
function, allowing to use the same function if the filename is something likealice.json
or so.changed this line in version 2 of the diff
157 secret_format = SecretFormat::Seed; 158 path = get_vault_key_path(data, secret_format, address.to_string()); 159 } 160 if !path.exists() { 161 secret_format = SecretFormat::Cesium; 162 path = get_vault_key_path(data, secret_format, address.to_string()); 163 } 164 if !path.exists() { 165 secret_format = SecretFormat::Predefined; 166 path = get_vault_key_path(data, secret_format, address.to_string()); 167 } 96 168 if path.exists() { 169 Ok(Some((secret_format, path))) 170 } else { 171 Ok(None) 172 } Indeed; but I'll let it on the side until we decide on the vault item filename & content in the first comment as it will be impacted anyway.
Edited by Nicolas80
183 let secret_vec = decrypt(&cypher, password).map_err(|e| GcliError::Input(e.to_string()))?; 184 let secret = String::from_utf8(secret_vec).map_err(|e| anyhow!(e))?; 185 186 //Still need to handle different secret formats 187 match secret_format { 188 SecretFormat::Substrate => { 189 Ok(Some(pair_from_str(&secret)?.into())) 190 } 191 SecretFormat::Seed => { 192 Ok(Some(pair_from_seed(&secret)?.into())) 193 } 194 SecretFormat::Cesium => { 195 let mut lines = secret.lines(); 196 //Un-wrapping the 2 secrets from each line 197 let id = lines.next().unwrap(); 198 let pwd = lines.next().unwrap(); changed this line in version 6 of the diff
added 1 commit
- 97abd338 - * Modified VaultItem to only contain relevant data necessary to create the vault entry.
added 1 commit
- 9f712d7b - * Added sea-orm dependency to allow having DB entity mappings and use a local sqlite file database
I'll check later to create a post on the forum, referencing the new (cleaner) MR !41 (merged)
Some questions for that, is it better to do it in English or French? (I'm not sure what languages the different developers speak)?
Added post in forum asking for review: https://forum.duniter.org/t/request-for-review-adding-db-persistence-for-all-secretformat-of-vault-keys-as-well-as-supporting-derivations/12829
I almost have something testable now; still need to do some cleanup (and some extra manual testing might be useful as well).
For now; I kept the same encrypt & decrypt functions; but only persists "substrate uris" for the root vault accounts.
This is in the form of
- sr25519 mnemonic: "bottom drive obey lake curtain smoke basket hold race lonely fit walk"
- sr25519 seed: "0xfac7959dbfe72f052e5a0c3c8d6530f202b02fd8f9f5ca3580ec8deb7797479e" (seed of the mnemonic above)
- ed25519 seed: "0x916e95359a49c82e3d84269b90551433352c433eb9bb270fb8cb86e8a6c9ec85" (seed retrieved from nacl key, which is retrieved from cesium id & secret)
Edited by Nicolas80added 1 commit
- e5dbb383 - * Lot of changes to add vault_account and vault_derivation to persist everything.
I retrieved a patch of this branch and re-phrased that in one commit on a new branch that is directly on the original repository so I could also rebase with latest changes from
master
(I didn't have developer access before):We should probably close this MR
Edited by Nicolas80mentioned in merge request !41 (merged)
Because !40 (closed) is merged, we can close this one.
mentioned in issue #36