Skip to content
Snippets Groups Projects
Commit 4166a7b4 authored by Hugo Trentesaux's avatar Hugo Trentesaux
Browse files

add vault (!22)

* move into function

* remove unnecessary complexity

* wip add phrase generation

* prepare 0.2.7 release

* remove dep

* add predefined keys and update doc

* wip add save from cmd line

* improve error management

* allow to store password-protected secret

* wip use rage for password-encrypted keys

* wip poc for keystore
parent 7e4a4a0e
Branches
Tags 0.2.7
1 merge request!22add vault
Pipeline #36257 passed
...@@ -46,6 +46,49 @@ dependencies = [ ...@@ -46,6 +46,49 @@ dependencies = [
"generic-array 0.14.7", "generic-array 0.14.7",
] ]
[[package]]
name = "age"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edeef7d7b199195a2d7d7a8155d2d04aee736e60c5c7bdd7097d115369a8817d"
dependencies = [
"age-core",
"base64 0.21.7",
"bech32",
"chacha20poly1305",
"cookie-factory",
"hmac 0.12.1",
"i18n-embed",
"i18n-embed-fl",
"lazy_static",
"nom",
"pin-project",
"rand",
"rust-embed",
"scrypt",
"sha2 0.10.8",
"subtle",
"x25519-dalek",
"zeroize",
]
[[package]]
name = "age-core"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5f11899bc2bbddd135edbc30c36b1924fa59d0746bb45beb5933fafe3fe509b"
dependencies = [
"base64 0.21.7",
"chacha20poly1305",
"cookie-factory",
"hkdf",
"io_tee",
"nom",
"rand",
"secrecy",
"sha2 0.10.8",
]
[[package]] [[package]]
name = "ahash" name = "ahash"
version = "0.7.7" version = "0.7.7"
...@@ -163,6 +206,12 @@ version = "1.0.79" ...@@ -163,6 +206,12 @@ version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
[[package]]
name = "arc-swap"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6"
[[package]] [[package]]
name = "ark-bls12-377" name = "ark-bls12-377"
version = "0.4.0" version = "0.4.0"
...@@ -710,6 +759,12 @@ version = "1.6.0" ...@@ -710,6 +759,12 @@ version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "bech32"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445"
[[package]] [[package]]
name = "beef" name = "beef"
version = "0.5.2" version = "0.5.2"
...@@ -933,6 +988,19 @@ dependencies = [ ...@@ -933,6 +988,19 @@ dependencies = [
"cpufeatures", "cpufeatures",
] ]
[[package]]
name = "chacha20poly1305"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
dependencies = [
"aead",
"chacha20",
"cipher",
"poly1305",
"zeroize",
]
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.33" version = "0.4.33"
...@@ -953,6 +1021,7 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" ...@@ -953,6 +1021,7 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [ dependencies = [
"crypto-common", "crypto-common",
"inout", "inout",
"zeroize",
] ]
[[package]] [[package]]
...@@ -1051,7 +1120,7 @@ version = "0.5.1" ...@@ -1051,7 +1120,7 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e37668cb35145dcfaa1931a5f37fde375eeae8068b4c0d2f289da28a270b2d2c" checksum = "e37668cb35145dcfaa1931a5f37fde375eeae8068b4c0d2f289da28a270b2d2c"
dependencies = [ dependencies = [
"directories", "directories 4.0.1",
"serde", "serde",
"thiserror", "thiserror",
"toml 0.5.11", "toml 0.5.11",
...@@ -1087,6 +1156,12 @@ version = "0.4.0" ...@@ -1087,6 +1156,12 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
[[package]]
name = "cookie-factory"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "396de984970346b0d9e93d1415082923c679e5ae5c3ee3dcbd104f5610af126b"
[[package]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.9.4" version = "0.9.4"
...@@ -1359,6 +1434,19 @@ dependencies = [ ...@@ -1359,6 +1434,19 @@ dependencies = [
"syn 2.0.48", "syn 2.0.48",
] ]
[[package]]
name = "dashmap"
version = "5.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
dependencies = [
"cfg-if",
"hashbrown 0.14.3",
"lock_api",
"once_cell",
"parking_lot_core",
]
[[package]] [[package]]
name = "der" name = "der"
version = "0.7.8" version = "0.7.8"
...@@ -1439,7 +1527,16 @@ version = "4.0.1" ...@@ -1439,7 +1527,16 @@ version = "4.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f51c5d4ddabd36886dd3e1438cb358cdcb0d7c499cb99cb4ac2e38e18b5cb210" checksum = "f51c5d4ddabd36886dd3e1438cb358cdcb0d7c499cb99cb4ac2e38e18b5cb210"
dependencies = [ dependencies = [
"dirs-sys", "dirs-sys 0.3.7",
]
[[package]]
name = "directories"
version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35"
dependencies = [
"dirs-sys 0.4.1",
] ]
[[package]] [[package]]
...@@ -1453,6 +1550,29 @@ dependencies = [ ...@@ -1453,6 +1550,29 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "dirs-sys"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.48.0",
]
[[package]]
name = "displaydoc"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
]
[[package]] [[package]]
name = "dleq_vrf" name = "dleq_vrf"
version = "0.0.2" version = "0.0.2"
...@@ -1709,6 +1829,15 @@ version = "0.2.6" ...@@ -1709,6 +1829,15 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1676f435fc1dadde4d03e43f5d62b259e1ce5f40bd4ffb21db2b42ebe59c1382" checksum = "1676f435fc1dadde4d03e43f5d62b259e1ce5f40bd4ffb21db2b42ebe59c1382"
[[package]]
name = "find-crate"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2"
dependencies = [
"toml 0.5.11",
]
[[package]] [[package]]
name = "fixed-hash" name = "fixed-hash"
version = "0.8.0" version = "0.8.0"
...@@ -1721,6 +1850,50 @@ dependencies = [ ...@@ -1721,6 +1850,50 @@ dependencies = [
"static_assertions", "static_assertions",
] ]
[[package]]
name = "fluent"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61f69378194459db76abd2ce3952b790db103ceb003008d3d50d97c41ff847a7"
dependencies = [
"fluent-bundle",
"unic-langid",
]
[[package]]
name = "fluent-bundle"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e242c601dec9711505f6d5bbff5bedd4b61b2469f2e8bb8e57ee7c9747a87ffd"
dependencies = [
"fluent-langneg",
"fluent-syntax",
"intl-memoizer",
"intl_pluralrules",
"rustc-hash",
"self_cell 0.10.3",
"smallvec",
"unic-langid",
]
[[package]]
name = "fluent-langneg"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c4ad0989667548f06ccd0e306ed56b61bd4d35458d54df5ec7587c0e8ed5e94"
dependencies = [
"unic-langid",
]
[[package]]
name = "fluent-syntax"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0abed97648395c902868fee9026de96483933faa54ea3b40d652f7dfe61ca78"
dependencies = [
"thiserror",
]
[[package]] [[package]]
name = "fnv" name = "fnv"
version = "1.0.7" version = "1.0.7"
...@@ -1900,12 +2073,15 @@ dependencies = [ ...@@ -1900,12 +2073,15 @@ dependencies = [
[[package]] [[package]]
name = "gcli" name = "gcli"
version = "0.2.6" version = "0.2.7"
dependencies = [ dependencies = [
"age",
"anyhow", "anyhow",
"bip39",
"bs58", "bs58",
"clap", "clap",
"confy", "confy",
"directories 5.0.1",
"env_logger", "env_logger",
"futures", "futures",
"graphql_client", "graphql_client",
...@@ -2122,6 +2298,15 @@ version = "0.4.3" ...@@ -2122,6 +2298,15 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hkdf"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
dependencies = [
"hmac 0.12.1",
]
[[package]] [[package]]
name = "hmac" name = "hmac"
version = "0.8.1" version = "0.8.1"
...@@ -2255,6 +2440,75 @@ dependencies = [ ...@@ -2255,6 +2440,75 @@ dependencies = [
"tokio-native-tls", "tokio-native-tls",
] ]
[[package]]
name = "i18n-config"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c9ce3c48cbc21fd5b22b9331f32b5b51f6ad85d969b99e793427332e76e7640"
dependencies = [
"log",
"serde",
"serde_derive",
"thiserror",
"toml 0.8.10",
"unic-langid",
]
[[package]]
name = "i18n-embed"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94205d95764f5bb9db9ea98fa77f89653365ca748e27161f5bbea2ffd50e459c"
dependencies = [
"arc-swap",
"fluent",
"fluent-langneg",
"fluent-syntax",
"i18n-embed-impl",
"intl-memoizer",
"lazy_static",
"log",
"parking_lot",
"rust-embed",
"thiserror",
"unic-langid",
"walkdir",
]
[[package]]
name = "i18n-embed-fl"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fc1f8715195dffc4caddcf1cf3128da15fe5d8a137606ea8856c9300047d5a2"
dependencies = [
"dashmap",
"find-crate",
"fluent",
"fluent-syntax",
"i18n-config",
"i18n-embed",
"lazy_static",
"proc-macro-error",
"proc-macro2",
"quote",
"strsim 0.10.0",
"syn 2.0.48",
"unic-langid",
]
[[package]]
name = "i18n-embed-impl"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81093c4701672f59416582fe3145676126fd23ba5db910acad0793c1108aaa58"
dependencies = [
"find-crate",
"i18n-config",
"proc-macro2",
"quote",
"syn 2.0.48",
]
[[package]] [[package]]
name = "iana-time-zone" name = "iana-time-zone"
version = "0.1.60" version = "0.1.60"
...@@ -2393,6 +2647,25 @@ dependencies = [ ...@@ -2393,6 +2647,25 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "intl-memoizer"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c310433e4a310918d6ed9243542a6b83ec1183df95dff8f23f87bb88a264a66f"
dependencies = [
"type-map",
"unic-langid",
]
[[package]]
name = "intl_pluralrules"
version = "7.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972"
dependencies = [
"unic-langid",
]
[[package]] [[package]]
name = "io-lifetimes" name = "io-lifetimes"
version = "1.0.11" version = "1.0.11"
...@@ -2404,6 +2677,12 @@ dependencies = [ ...@@ -2404,6 +2677,12 @@ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
[[package]]
name = "io_tee"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b3f7cef34251886990511df1c61443aa928499d598a9473929ab5a90a527304"
[[package]] [[package]]
name = "ipnet" name = "ipnet"
version = "2.9.0" version = "2.9.0"
...@@ -2981,6 +3260,12 @@ dependencies = [ ...@@ -2981,6 +3260,12 @@ dependencies = [
"vcpkg", "vcpkg",
] ]
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]] [[package]]
name = "parity-scale-codec" name = "parity-scale-codec"
version = "3.6.9" version = "3.6.9"
...@@ -3530,6 +3815,40 @@ dependencies = [ ...@@ -3530,6 +3815,40 @@ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
[[package]]
name = "rust-embed"
version = "8.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a82c0bbc10308ed323529fd3c1dce8badda635aa319a5ff0e6466f33b8101e3f"
dependencies = [
"rust-embed-impl",
"rust-embed-utils",
"walkdir",
]
[[package]]
name = "rust-embed-impl"
version = "8.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6227c01b1783cdfee1bcf844eb44594cd16ec71c35305bf1c9fb5aade2735e16"
dependencies = [
"proc-macro2",
"quote",
"rust-embed-utils",
"syn 2.0.48",
"walkdir",
]
[[package]]
name = "rust-embed-utils"
version = "8.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cb0a25bfbb2d4b4402179c2cf030387d9990857ce08a32592c6238db9fa8665"
dependencies = [
"sha2 0.10.8",
"walkdir",
]
[[package]] [[package]]
name = "rustc-demangle" name = "rustc-demangle"
version = "0.1.23" version = "0.1.23"
...@@ -3980,6 +4299,21 @@ dependencies = [ ...@@ -3980,6 +4299,21 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "self_cell"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e14e4d63b804dc0c7ec4a1e52bcb63f02c7ac94476755aa579edac21e01f915d"
dependencies = [
"self_cell 1.0.3",
]
[[package]]
name = "self_cell"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58bf37232d3bb9a2c4e641ca2a11d83b5062066f88df7fed36c28772046d65ba"
[[package]] [[package]]
name = "semver" name = "semver"
version = "1.0.21" version = "1.0.21"
...@@ -5044,6 +5378,15 @@ dependencies = [ ...@@ -5044,6 +5378,15 @@ dependencies = [
"once_cell", "once_cell",
] ]
[[package]]
name = "tinystr"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83c02bf3c538ab32ba913408224323915f4ef9a6d61c0e85d493f355921c0ece"
dependencies = [
"displaydoc",
]
[[package]] [[package]]
name = "tinyvec" name = "tinyvec"
version = "1.6.0" version = "1.6.0"
...@@ -5363,6 +5706,15 @@ dependencies = [ ...@@ -5363,6 +5706,15 @@ dependencies = [
"static_assertions", "static_assertions",
] ]
[[package]]
name = "type-map"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6d3364c5e96cb2ad1603037ab253ddd34d7fb72a58bdddf4b7350760fc69a46"
dependencies = [
"rustc-hash",
]
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.17.0" version = "1.17.0"
...@@ -5381,6 +5733,25 @@ dependencies = [ ...@@ -5381,6 +5733,25 @@ dependencies = [
"static_assertions", "static_assertions",
] ]
[[package]]
name = "unic-langid"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "238722e6d794ed130f91f4ea33e01fcff4f188d92337a21297892521c72df516"
dependencies = [
"unic-langid-impl",
]
[[package]]
name = "unic-langid-impl"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bd55a2063fdea4ef1f8633243a7b0524cbeef1905ae04c31a1c9b9775c55bc6"
dependencies = [
"serde",
"tinystr",
]
[[package]] [[package]]
name = "unicode-bidi" name = "unicode-bidi"
version = "0.3.15" version = "0.3.15"
......
...@@ -9,7 +9,7 @@ rust-version = "1.75.0" ...@@ -9,7 +9,7 @@ rust-version = "1.75.0"
license = "AGPL-3.0-only" license = "AGPL-3.0-only"
name = "gcli" name = "gcli"
repository = "https://git.duniter.org/clients/rust/gcli-v2s" repository = "https://git.duniter.org/clients/rust/gcli-v2s"
version = "0.2.6" version = "0.2.7"
[dependencies] [dependencies]
# subxt is main dependency # subxt is main dependency
...@@ -18,9 +18,11 @@ subxt = { git = 'https://github.com/duniter/subxt', branch = 'subxt-v0.34.0-duni ...@@ -18,9 +18,11 @@ subxt = { git = 'https://github.com/duniter/subxt', branch = 'subxt-v0.34.0-duni
"native", "native",
"jsonrpsee", "jsonrpsee",
] } ] }
# substrate primitives dependencies # substrate primitives dependencies
sp-core = { git = "https://github.com/duniter/duniter-polkadot-sdk.git", branch = "duniter-substrate-v1.6.0" } sp-core = { git = "https://github.com/duniter/duniter-polkadot-sdk.git", branch = "duniter-substrate-v1.6.0" }
sp-runtime = { git = "https://github.com/duniter/duniter-polkadot-sdk.git", branch = "duniter-substrate-v1.6.0" } sp-runtime = { git = "https://github.com/duniter/duniter-polkadot-sdk.git", branch = "duniter-substrate-v1.6.0" }
# crates.io dependencies # crates.io dependencies
anyhow = "^1.0" anyhow = "^1.0"
clap = { version = "^4.5.0", features = ["derive"] } clap = { version = "^4.5.0", features = ["derive"] }
...@@ -36,10 +38,16 @@ serde = { version = "^1.0", features = ["derive"] } ...@@ -36,10 +38,16 @@ serde = { version = "^1.0", features = ["derive"] }
serde_json = "^1.0.113" serde_json = "^1.0.113"
tokio = { version = "^1.36.0", features = ["macros"] } tokio = { version = "^1.36.0", features = ["macros"] }
confy = "^0.5.1" confy = "^0.5.1"
scrypt = { version = "^0.11", default-features = false } # for old-style key generation
nacl = { version = "^0.5.3" } # for old-style key generation
bs58 = "^0.5.0" bs58 = "^0.5.0"
inquire = "^0.6.2" inquire = "^0.6.2"
directories = "^5.0.1"
# crypto
scrypt = { version = "^0.11", default-features = false } # for old-style key generation
nacl = { version = "^0.5.3" } # for old-style key generation
# this is beta crate for password-encrypted files
age = { default-features = false, version = "^0.10.0", features = ["armor"] }
bip39 = { version = "^2.0.0", features = ["rand"] } # mnemonic
# allows to build gcli for different runtimes and with different predefined networks # allows to build gcli for different runtimes and with different predefined networks
[features] [features]
......
...@@ -22,7 +22,7 @@ List certifications and session keys that will expire within one month: ...@@ -22,7 +22,7 @@ List certifications and session keys that will expire within one month:
cargo run -- --url wss://gdev.p2p.legal:443/ws smith expire --blocks 432000 cargo run -- --url wss://gdev.p2p.legal:443/ws smith expire --blocks 432000
For more examples see [in the example file](./doc/example.md). For more examples see [in the doc](./doc/). `cargo run --` is replaced by `gcli` as if the binary was added to your path.
#### Log level #### Log level
......
# Ğcli config # Ğcli config
Some Ğcli commands require to have an address configured (for example to get account balance), some require to have a secret configured (to sign extrinsics). Some Ğcli commands require to have an address configured (for example to get account balance), some require to have a secret configured (to sign extrinsics).
Ğcli allows to save what you want in a config file and to overwrite parts in command line arguments. Example: Ğcli allows to save the address you want in a config file and to overwrite parts in command line arguments. Example:
```sh ```sh
# save Alice secret to config file # save Alice address to config file
cargo run -- -S predefined -s Alice config save gcli -S predefined -s Alice config save
# show config # show config
cargo run -- config show gcli config show
# [stdout] # [stdout]
# Ğcli config # Ğcli config
# duniter endpoint ws://localhost:9944 # duniter endpoint ws://localhost:9944
# indexer endpoint http://localhost:4350/graphql # indexer endpoint http://localhost:4350/graphql
# address 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY (secret defined) # address 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
# use different address in command line # use different address in command line
cargo run -- --address 5Fxune7f71ZbpP2FoY3mhYcmM596Erhv1gRue4nsPwkxMR4n config show gcli --address 5Fxune7f71ZbpP2FoY3mhYcmM596Erhv1gRue4nsPwkxMR4n config show
# [stdout] # [stdout]
# Ğcli config # Ğcli config
# duniter endpoint ws://localhost:9944 # duniter endpoint ws://localhost:9944
# indexer endpoint http://localhost:4350/graphql # indexer endpoint http://localhost:4350/graphql
# address 5Fxune7f71ZbpP2FoY3mhYcmM596Erhv1gRue4nsPwkxMR4n (no secret) # address 5Fxune7f71ZbpP2FoY3mhYcmM596Erhv1gRue4nsPwkxMR4n
``` ```
You can see that if a secret is defined, the associated address is used, but if an other address is given, the secret is silenced. This also applies to rpc endpoint config (`--url`) or indexer config (`--indexer`).
\ No newline at end of file
## Using password encrypted vault
For convenience, default accounts are hardcoded in Ğcli without needing a password:
```sh
# when Alice address is stored in config file
gcli account transfer 1 5Fxune7f71ZbpP2FoY3mhYcmM596Erhv1gRue4nsPwkxMR4n
# no need for password to sign transaction
```
but in general usage, you want to store your secret in the local vault. This goes like this:
```sh
# list available keys in the vault
gcli vault list
# [stdout]
# available keys:
# 5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV
# add a new secret to the vault
gcli vault import
# [stdout]
# Mnemonic: <enter mnemonic>
# Password: <enter password>
```
After saving your secret to the vault, you will be able to unlock it with the password:
```sh
gcli account transfer 123 5Fxune7f71ZbpP2FoY3mhYcmM596Erhv1gRue4nsPwkxMR4n
# [stdout]
# Enter password to unlock account 5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV
# Password: <enter password>
# transaction submitted to the network, waiting 6 seconds...
# transfered 1.23 ĞD (5DfhGyQdFobKM8NsWvEeAKk5EQQgYe9AydgJ7rMB6E1EqRzV → 5Fxune7f71ZbpP2FoY3mhYcmM596Erhv1gRue4nsPwkxMR4n)
```
You can display the secret files location:
```sh
gcli vault where
# [stdout]
# /home/hugo/.local/share/gcli
```
\ No newline at end of file
# Examples of gcli commands for copy-paste # Examples of gcli commands for copy-paste
Useful when developing: replace `gcli` by `cargo run --` to build in debug mode and launch gcli.
## Configuration ## Configuration
It can be handful to use Gcli with a configuration file to avoid passing arguments on every command. It can be handful to use Ǧcli with a configuration file to avoid passing arguments on every command.
```sh ```sh
# show config commands # show config commands
...@@ -13,11 +11,11 @@ gcli config ...@@ -13,11 +11,11 @@ gcli config
gcli config where gcli config where
# save config to use gdev network for next commands # save config to use gdev network for next commands
gcli --network gdev config save gcli --network gdev config save
# save config to use Alice predefined secret # save config to use Alice predefined account
gcli -S predefined -s Alice config save gcli -S predefined -s Alice config save
# the arguments above can be combined # the arguments above can be combined
# command below sets local network and predefined secret # command below sets local network and predefined secret
gcli --network local -S predefined -s test1 config save gcli --network local -S predefined -s Alice config save
``` ```
In the following, we assume this last command was run. More about the config in [config.md](./config.md). In the following, we assume this last command was run. More about the config in [config.md](./config.md).
...@@ -33,8 +31,8 @@ gcli blockchain current-block ...@@ -33,8 +31,8 @@ gcli blockchain current-block
gcli account balance gcli account balance
# get identity information without indexer # get identity information without indexer
gcli --no-indexer identity get -a 5Hn2LeMZXPFitMwrmrGucwtAPSLEiP4o5zTF7kHzMBtEkJUr gcli --no-indexer identity get -a 5Hn2LeMZXPFitMwrmrGucwtAPSLEiP4o5zTF7kHzMBtEkJUr
# get information about test1 identity (needs indexer) # get information about Alice identity (needs indexer)
gcli identity get --username test1 gcli identity get --username Alice
# claim universal dividends # claim universal dividends
gcli ud claim gcli ud claim
# transfer 5000 units # transfer 5000 units
......
...@@ -6,6 +6,7 @@ pub mod collective; ...@@ -6,6 +6,7 @@ pub mod collective;
pub mod distance; pub mod distance;
pub mod expire; pub mod expire;
pub mod identity; pub mod identity;
pub mod vault;
pub mod net_test; pub mod net_test;
pub mod oneshot; pub mod oneshot;
pub mod publish; pub mod publish;
......
use crate::*;
use age::secrecy::Secret;
use std::io::{Read, Write};
/// define universal dividends subcommands
#[derive(Clone, Default, Debug, clap::Parser)]
pub enum Subcommand {
#[default]
/// List available keys
List,
/// Show where vault stores secret
Where,
/// Generate a mnemonic
Generate,
/// Import mnemonic with interactive prompt
Import,
}
// encrypt input with passphrase
fn encrypt(input: &[u8], passphrase: String) -> Result<Vec<u8>, age::EncryptError> {
let encryptor = age::Encryptor::with_user_passphrase(Secret::new(passphrase));
let mut encrypted = vec![];
let mut writer = encryptor.wrap_output(age::armor::ArmoredWriter::wrap_output(
&mut encrypted,
age::armor::Format::AsciiArmor,
)?)?;
writer.write_all(input)?;
writer.finish().and_then(|armor| armor.finish())?;
Ok(encrypted)
}
// decrypt cypher with passphrase
fn decrypt(input: &[u8], passphrase: String) -> Result<Vec<u8>, age::DecryptError> {
let age::Decryptor::Passphrase(decryptor) =
age::Decryptor::new(age::armor::ArmoredReader::new(input))?
else {
unimplemented!()
};
let mut decrypted = vec![];
let mut reader = decryptor.decrypt(&Secret::new(passphrase.to_owned()), None)?;
reader.read_to_end(&mut decrypted)?;
Ok(decrypted)
}
/// handle ud commands
pub fn handle_command(data: Data, command: Subcommand) -> Result<(), GcliError> {
// match subcommand
match command {
Subcommand::List => {
if let Ok(entries) = std::fs::read_dir(data.project_dir.data_dir()) {
println!("available keys:");
entries.for_each(|e| println!("{}", e.unwrap().file_name().to_str().unwrap()));
} else {
println!("could not read project dir");
}
}
Subcommand::Where => {
println!("{}", data.project_dir.data_dir().to_str().unwrap());
}
Subcommand::Generate => {
// TODO allow custom word count
let mnemonic = bip39::Mnemonic::generate(12).unwrap();
println!("{mnemonic}");
}
Subcommand::Import => {
let mnemonic = rpassword::prompt_password("Mnemonic: ")?;
println!("Enter password to protect the key");
let password = rpassword::prompt_password("Password: ")?;
let address = store_mnemonic(&data, &mnemonic, password)?;
println!("Stored secret for {address}");
}
};
Ok(())
}
/// store mnemonic protected with password
pub fn store_mnemonic(
data: &Data,
mnemonic: &str,
password: String,
) -> Result<AccountId, GcliError> {
// check validity by deriving keypair
let keypair = pair_from_str(&mnemonic)?;
let address = keypair.public();
// write encrypted mnemonic in file identified by pubkey
let path = data.project_dir.data_dir().join(address.to_string());
let mut file = std::fs::File::create(path)?;
file.write_all(&encrypt(mnemonic.as_bytes(), password).map_err(|e| anyhow!(e))?[..])?;
Ok(keypair.public().into())
}
/// try get secret in keystore
pub fn try_fetch_secret(data: &Data, address: AccountId) -> Result<Option<String>, GcliError> {
let path = data.project_dir.data_dir().join(address.to_string());
if path.exists() {
println!("Enter password to unlock account {address}");
let password = rpassword::prompt_password("Password: ")?;
let mut file = std::fs::OpenOptions::new().read(true).open(path)?;
let mut cypher = vec![];
file.read_to_end(&mut cypher)?;
let secret = decrypt(&cypher, password).map_err(|e| GcliError::Input(e.to_string()))?;
let secretstr = String::from_utf8(secret).map_err(|e| anyhow!(e))?;
Ok(Some(secretstr))
} else {
Ok(None)
}
}
// test that armored encryption/decryption work as intended
#[test]
fn test_encrypt_decrypt() {
let plaintext = b"Hello world!";
let passphrase = "this is not a good passphrase".to_string();
let encrypted = encrypt(plaintext, passphrase.clone()).unwrap();
let decrypted = decrypt(&encrypted, passphrase).unwrap();
assert_eq!(decrypted, plaintext);
}
...@@ -6,14 +6,13 @@ const APP_NAME: &str = "gcli"; ...@@ -6,14 +6,13 @@ const APP_NAME: &str = "gcli";
/// defines structure of config file /// defines structure of config file
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct Config { pub struct Config {
// duniter endpoint /// duniter endpoint
pub duniter_endpoint: String, pub duniter_endpoint: String,
// indexer endpoint /// indexer endpoint
pub indexer_endpoint: String, pub indexer_endpoint: String,
// user address /// user address
/// to perform actions, user must provide secret
pub address: Option<AccountId>, pub address: Option<AccountId>,
// user secret (substrate format)
pub secret: Option<String>,
} }
impl std::default::Default for Config { impl std::default::Default for Config {
...@@ -22,18 +21,12 @@ impl std::default::Default for Config { ...@@ -22,18 +21,12 @@ impl std::default::Default for Config {
duniter_endpoint: String::from(data::LOCAL_DUNITER_ENDPOINT), duniter_endpoint: String::from(data::LOCAL_DUNITER_ENDPOINT),
indexer_endpoint: String::from(data::LOCAL_INDEXER_ENDPOINT), indexer_endpoint: String::from(data::LOCAL_INDEXER_ENDPOINT),
address: None, address: None,
secret: None,
} }
} }
} }
impl std::fmt::Display for Config { impl std::fmt::Display for Config {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let secret = if self.secret.is_some() {
"(secret defined)"
} else {
"(no secret)"
};
let address = if let Some(address) = &self.address { let address = if let Some(address) = &self.address {
format!("{}", address) format!("{}", address)
} else { } else {
...@@ -42,7 +35,7 @@ impl std::fmt::Display for Config { ...@@ -42,7 +35,7 @@ impl std::fmt::Display for Config {
writeln!(f, "Ğcli config")?; writeln!(f, "Ğcli config")?;
writeln!(f, "duniter endpoint {}", self.duniter_endpoint)?; writeln!(f, "duniter endpoint {}", self.duniter_endpoint)?;
writeln!(f, "indexer endpoint {}", self.indexer_endpoint)?; writeln!(f, "indexer endpoint {}", self.indexer_endpoint)?;
write!(f, "address {address} {secret}") write!(f, "address {address}")
} }
} }
......
...@@ -25,7 +25,6 @@ pub const GDEV_INDEXER_ENDPOINTS: [&str; 2] = [ ...@@ -25,7 +25,6 @@ pub const GDEV_INDEXER_ENDPOINTS: [&str; 2] = [
/// Data of current command /// Data of current command
/// can also include fetched information /// can also include fetched information
#[derive(Default)]
pub struct Data { pub struct Data {
// command line arguments // command line arguments
pub args: Args, pub args: Args,
...@@ -47,6 +46,8 @@ pub struct Data { ...@@ -47,6 +46,8 @@ pub struct Data {
pub genesis_hash: Hash, pub genesis_hash: Hash,
// indexer genesis hash // indexer genesis hash
pub indexer_genesis_hash: Hash, pub indexer_genesis_hash: Hash,
// gcli base path
pub project_dir: directories::ProjectDirs,
} }
/// system properties defined in client specs /// system properties defined in client specs
...@@ -57,10 +58,32 @@ struct SystemProperties { ...@@ -57,10 +58,32 @@ struct SystemProperties {
token_symbol: String, token_symbol: String,
} }
impl Default for Data {
fn default() -> Self {
let project_dir = directories::ProjectDirs::from("org", "duniter", "gcli").unwrap();
if !project_dir.data_dir().exists() {
std::fs::create_dir(project_dir.data_dir()).expect("could not create data dir");
};
Self {
project_dir,
args: Default::default(),
cfg: Default::default(),
client: Default::default(),
indexer: Default::default(),
keypair: Default::default(),
idty_index: Default::default(),
token_decimals: Default::default(),
token_symbol: Default::default(),
genesis_hash: Default::default(),
indexer_genesis_hash: Default::default(),
}
}
}
// implement helper functions for Data // implement helper functions for Data
impl Data { impl Data {
/// --- constructor --- /// --- constructor ---
pub fn new(args: Args) -> Self { pub fn new(args: Args) -> Result<Self, GcliError> {
Self { Self {
args, args,
cfg: conf::load_conf(), cfg: conf::load_conf(),
...@@ -69,7 +92,6 @@ impl Data { ...@@ -69,7 +92,6 @@ impl Data {
..Default::default() ..Default::default()
} }
.overwrite_from_args() .overwrite_from_args()
.build_from_config()
} }
// --- getters --- // --- getters ---
// the "unwrap" should not fail if data is well prepared // the "unwrap" should not fail if data is well prepared
...@@ -85,7 +107,12 @@ impl Data { ...@@ -85,7 +107,12 @@ impl Data {
pub fn keypair(&self) -> KeyPair { pub fn keypair(&self) -> KeyPair {
match self.keypair.clone() { match self.keypair.clone() {
Some(keypair) => keypair, Some(keypair) => keypair,
None => prompt_secret(self.args.secret_format), None => loop {
match fetch_or_get_keypair(self, self.cfg.address.clone()) {
Ok(pair) => return pair,
Err(e) => println!("{e:?} → retry"),
}
},
} }
} }
pub fn idty_index(&self) -> IdtyId { pub fn idty_index(&self) -> IdtyId {
...@@ -102,7 +129,7 @@ impl Data { ...@@ -102,7 +129,7 @@ impl Data {
} }
// --- mutators --- // --- mutators ---
/// use arguments to overwrite config /// use arguments to overwrite config
pub fn overwrite_from_args(mut self) -> Self { pub fn overwrite_from_args(mut self) -> Result<Self, GcliError> {
// network // network
if let Some(network) = self.args.network.clone() { if let Some(network) = self.args.network.clone() {
// a network was provided as arugment // a network was provided as arugment
...@@ -145,49 +172,18 @@ impl Data { ...@@ -145,49 +172,18 @@ impl Data {
self.cfg.indexer_endpoint = indexer_endpoint self.cfg.indexer_endpoint = indexer_endpoint
} }
// secret format and value // secret format and value
if self.args.secret_format == SecretFormat::Predefined { if let Some(secret_format) = self.args.secret_format {
// predefined secret format overwrites secret with mnemonic let keypair = get_keypair(secret_format, self.args.secret.as_deref())?;
match self.args.secret.clone() { self.cfg.address = Some(keypair.address());
None => {} self.keypair = Some(keypair);
Some(derivation) => {
self.cfg.secret = Some(predefined_mnemonic(&derivation));
}
};
} else if self.args.secret_format == SecretFormat::Cesium {
// cesium secret format also overwrites, to force valid prompt
self.cfg.secret = None
} else if let Some(secret) = self.args.secret.clone() {
// other secret type
self.cfg.secret = Some(secret);
} }
// address // address
if let Some(address) = self.args.address.clone() { if let Some(address) = self.args.address.clone() {
self.cfg.address = Some(AccountId::from_str(&address).expect("invalid address")); self.cfg.address = Some(AccountId::from_str(&address).expect("invalid address"));
// if giving address, cancel secret // if giving address, cancel secret
self.cfg.secret = None self.keypair = None
}
self
} }
/// build from config Ok(self)
pub fn build_from_config(mut self) -> Self {
let secret_format = self.args.secret_format;
// prevent incoherent state
if secret_format == SecretFormat::Cesium && self.cfg.secret.is_some() {
panic!("incompatible input: secret arg with cesium format");
}
// if secret format is cesium, force a prompt now and record keypair
if secret_format == SecretFormat::Cesium {
let keypair = prompt_secret(SecretFormat::Cesium);
self.cfg.address = Some(keypair.address());
self.keypair = Some(keypair);
}
// if a secret is defined (format should not be cesium), build keypair and silently overwrite address
if let Some(secret) = self.cfg.secret.clone() {
let keypair = pair_from_secret(secret_format, &secret).expect("invalid secret");
self.cfg.address = Some(keypair.public().into());
self.keypair = Some(keypair.into());
}
self
} }
/// build a client from url /// build a client from url
pub async fn build_client(mut self) -> Result<Self, GcliError> { pub async fn build_client(mut self) -> Result<Self, GcliError> {
......
...@@ -93,12 +93,9 @@ pub fn get_keypair( ...@@ -93,12 +93,9 @@ pub fn get_keypair(
secret: Option<&str>, secret: Option<&str>,
) -> Result<KeyPair, GcliError> { ) -> Result<KeyPair, GcliError> {
match (secret_format, secret) { match (secret_format, secret) {
(SecretFormat::Cesium, None) => Ok(prompt_secret(SecretFormat::Cesium)),
(SecretFormat::Predefined, Some(deriv)) => pair_from_predefined(deriv).map(|v| v.into()), (SecretFormat::Predefined, Some(deriv)) => pair_from_predefined(deriv).map(|v| v.into()),
(secret_format, None) => Ok(prompt_secret(secret_format)),
(_, Some(secret)) => Ok(pair_from_secret(secret_format, secret)?.into()), (_, Some(secret)) => Ok(pair_from_secret(secret_format, secret)?.into()),
_ => Err(GcliError::Logic(
"can not get keypair from available options".to_string(),
)),
} }
} }
...@@ -125,6 +122,8 @@ pub fn pair_from_str(secret: &str) -> Result<Sr25519Pair, GcliError> { ...@@ -125,6 +122,8 @@ pub fn pair_from_str(secret: &str) -> Result<Sr25519Pair, GcliError> {
} }
/// get keypair from given seed /// get keypair from given seed
// note: Sr25519Pair::from_string does exactly that when seed is 0x prefixed
// (see from_string_with_seed method in crypto core)
pub fn pair_from_seed(secret: &str) -> Result<Sr25519Pair, GcliError> { pub fn pair_from_seed(secret: &str) -> Result<Sr25519Pair, GcliError> {
let mut seed = [0; 32]; let mut seed = [0; 32];
hex::decode_to_slice(secret, &mut seed) hex::decode_to_slice(secret, &mut seed)
...@@ -195,3 +194,34 @@ pub fn prompt_secret(secret_format: SecretFormat) -> KeyPair { ...@@ -195,3 +194,34 @@ pub fn prompt_secret(secret_format: SecretFormat) -> KeyPair {
SecretFormat::Predefined => prompt_predefined().into(), SecretFormat::Predefined => prompt_predefined().into(),
} }
} }
/// get the secret from user, trying first keystore then input
pub fn fetch_or_get_keypair(data: &Data, address: Option<AccountId>) -> Result<KeyPair, GcliError> {
if let Some(address) = address {
// if address corresponds to predefined, (for example saved to config)
// keypair is already known (useful for dev mode)
if let Some(d) = catch_known(&address.to_string()) {
return Ok(pair_from_predefined(d).unwrap().into());
};
// look for corresponding secret in keystore
if let Some(secret) = commands::vault::try_fetch_secret(data, address)? {
return get_keypair(SecretFormat::Substrate, Some(&secret));
};
}
// at the moment, there is no way to confg gcli to use an other kind of secret
// without telling explicitly each time
Ok(prompt_secret(SecretFormat::Substrate))
}
// catch known addresses
fn catch_known(address: &str) -> Option<&str> {
match address {
"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" => Some("Alice"),
"5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" => Some("Bob"),
"5FLSigC9HGRKVhB9FiEo4Y3koPsNmBmLJbpXg2mp1hXcS59Y" => Some("Charlie"),
"5DAAnrj7VHTznn2AWBemMuyBwZWs6FNFjdyVXUeYum3PTXFy" => Some("Dave"),
"5HGjWAeFDfFCWPsjFQdVV2Msvz2XtMktvgocEZcCj68kUMaw" => Some("Eve"),
_ => None,
}
}
...@@ -40,13 +40,13 @@ pub struct Args { ...@@ -40,13 +40,13 @@ pub struct Args {
/// Do not use indexer /// Do not use indexer
#[clap(long)] #[clap(long)]
no_indexer: bool, no_indexer: bool,
/// Secret key or BIP39 mnemonic /// Secret key or BIP39 mnemonic (only used when secret format is compatible)
/// (eventually followed by derivation path) /// (eventually followed by derivation path)
#[clap(short, long)] #[clap(short, long)]
secret: Option<String>, secret: Option<String>,
/// Secret key format (seed, substrate, cesium) /// Secret key format (seed, substrate, cesium)
#[clap(short = 'S', long, default_value = SecretFormat::Substrate)] #[clap(short = 'S', long)]
secret_format: SecretFormat, secret_format: Option<SecretFormat>,
/// Address /// Address
#[clap(short, long)] #[clap(short, long)]
address: Option<String>, address: Option<String>,
...@@ -137,6 +137,9 @@ pub enum Subcommand { ...@@ -137,6 +137,9 @@ pub enum Subcommand {
/// Config (show, save...) /// Config (show, save...)
#[clap(subcommand)] #[clap(subcommand)]
Config(conf::Subcommand), Config(conf::Subcommand),
/// Key management (import, generate, list...)
#[clap(subcommand)]
Vault(commands::vault::Subcommand),
/// Cesium /// Cesium
#[clap(subcommand, hide = true)] #[clap(subcommand, hide = true)]
Cesium(commands::cesium::Subcommand), Cesium(commands::cesium::Subcommand),
...@@ -152,7 +155,7 @@ async fn main() -> Result<(), GcliError> { ...@@ -152,7 +155,7 @@ async fn main() -> Result<(), GcliError> {
env_logger::init(); env_logger::init();
// parse argument and initialize data // parse argument and initialize data
let data = Data::new(Args::parse()); let data = Data::new(Args::parse())?;
// match subcommands // match subcommands
let result = match data.args.subcommand.clone() { let result = match data.args.subcommand.clone() {
...@@ -177,6 +180,7 @@ async fn main() -> Result<(), GcliError> { ...@@ -177,6 +180,7 @@ async fn main() -> Result<(), GcliError> {
} }
Subcommand::Indexer(subcommand) => indexer::handle_command(data, subcommand).await, Subcommand::Indexer(subcommand) => indexer::handle_command(data, subcommand).await,
Subcommand::Config(subcommand) => conf::handle_command(data, subcommand), Subcommand::Config(subcommand) => conf::handle_command(data, subcommand),
Subcommand::Vault(subcommand) => commands::vault::handle_command(data, subcommand),
Subcommand::Cesium(subcommand) => commands::cesium::handle_command(data, subcommand).await, Subcommand::Cesium(subcommand) => commands::cesium::handle_command(data, subcommand).await,
Subcommand::Publish => commands::publish::handle_command().await, Subcommand::Publish => commands::publish::handle_command().await,
}; };
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment