diff --git a/Cargo.lock b/Cargo.lock
index ae6df0a9238d6e11769df01611dd97784fba0718..1e61c3201c938d15c1979b510a5aa429ebee05e0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -46,6 +46,49 @@ dependencies = [
  "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]]
 name = "ahash"
 version = "0.7.7"
@@ -163,6 +206,12 @@ version = "1.0.79"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
 
+[[package]]
+name = "arc-swap"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6"
+
 [[package]]
 name = "ark-bls12-377"
 version = "0.4.0"
@@ -710,6 +759,12 @@ version = "1.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
 
+[[package]]
+name = "bech32"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445"
+
 [[package]]
 name = "beef"
 version = "0.5.2"
@@ -933,6 +988,19 @@ dependencies = [
  "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]]
 name = "chrono"
 version = "0.4.33"
@@ -953,6 +1021,7 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
 dependencies = [
  "crypto-common",
  "inout",
+ "zeroize",
 ]
 
 [[package]]
@@ -1051,7 +1120,7 @@ version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e37668cb35145dcfaa1931a5f37fde375eeae8068b4c0d2f289da28a270b2d2c"
 dependencies = [
- "directories",
+ "directories 4.0.1",
  "serde",
  "thiserror",
  "toml 0.5.11",
@@ -1087,6 +1156,12 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
 
+[[package]]
+name = "cookie-factory"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "396de984970346b0d9e93d1415082923c679e5ae5c3ee3dcbd104f5610af126b"
+
 [[package]]
 name = "core-foundation"
 version = "0.9.4"
@@ -1359,6 +1434,19 @@ dependencies = [
  "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]]
 name = "der"
 version = "0.7.8"
@@ -1439,7 +1527,16 @@ version = "4.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f51c5d4ddabd36886dd3e1438cb358cdcb0d7c499cb99cb4ac2e38e18b5cb210"
 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]]
@@ -1453,6 +1550,29 @@ dependencies = [
  "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]]
 name = "dleq_vrf"
 version = "0.0.2"
@@ -1709,6 +1829,15 @@ version = "0.2.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 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]]
 name = "fixed-hash"
 version = "0.8.0"
@@ -1721,6 +1850,50 @@ dependencies = [
  "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]]
 name = "fnv"
 version = "1.0.7"
@@ -1900,12 +2073,15 @@ dependencies = [
 
 [[package]]
 name = "gcli"
-version = "0.2.6"
+version = "0.2.7"
 dependencies = [
+ "age",
  "anyhow",
+ "bip39",
  "bs58",
  "clap",
  "confy",
+ "directories 5.0.1",
  "env_logger",
  "futures",
  "graphql_client",
@@ -2122,6 +2298,15 @@ version = "0.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 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]]
 name = "hmac"
 version = "0.8.1"
@@ -2255,6 +2440,75 @@ dependencies = [
  "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]]
 name = "iana-time-zone"
 version = "0.1.60"
@@ -2393,6 +2647,25 @@ dependencies = [
  "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]]
 name = "io-lifetimes"
 version = "1.0.11"
@@ -2404,6 +2677,12 @@ dependencies = [
  "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]]
 name = "ipnet"
 version = "2.9.0"
@@ -2981,6 +3260,12 @@ dependencies = [
  "vcpkg",
 ]
 
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
 [[package]]
 name = "parity-scale-codec"
 version = "3.6.9"
@@ -3530,6 +3815,40 @@ dependencies = [
  "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]]
 name = "rustc-demangle"
 version = "0.1.23"
@@ -3980,6 +4299,21 @@ dependencies = [
  "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]]
 name = "semver"
 version = "1.0.21"
@@ -5044,6 +5378,15 @@ dependencies = [
  "once_cell",
 ]
 
+[[package]]
+name = "tinystr"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83c02bf3c538ab32ba913408224323915f4ef9a6d61c0e85d493f355921c0ece"
+dependencies = [
+ "displaydoc",
+]
+
 [[package]]
 name = "tinyvec"
 version = "1.6.0"
@@ -5363,6 +5706,15 @@ dependencies = [
  "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]]
 name = "typenum"
 version = "1.17.0"
@@ -5381,6 +5733,25 @@ dependencies = [
  "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]]
 name = "unicode-bidi"
 version = "0.3.15"
diff --git a/Cargo.toml b/Cargo.toml
index 16e9344cabc821a48957d3bbe691a70d90445b66..bd3166d885f09950c7b92f36a5b8195a8a67a6af 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,7 +9,7 @@ rust-version = "1.75.0"
 license = "AGPL-3.0-only"
 name = "gcli"
 repository = "https://git.duniter.org/clients/rust/gcli-v2s"
-version = "0.2.6"
+version = "0.2.7"
 
 [dependencies]
 # subxt is main dependency
@@ -18,9 +18,11 @@ subxt = { git = 'https://github.com/duniter/subxt', branch = 'subxt-v0.34.0-duni
     "native",
     "jsonrpsee",
 ] }
+
 # substrate primitives dependencies
 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" }
+
 # crates.io dependencies
 anyhow = "^1.0"
 clap = { version = "^4.5.0", features = ["derive"] }
@@ -36,10 +38,16 @@ serde = { version = "^1.0", features = ["derive"] }
 serde_json = "^1.0.113"
 tokio = { version = "^1.36.0", features = ["macros"] }
 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"
 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 
 [features]
diff --git a/README.md b/README.md
index 8b082058984c748a53d2f7ba913d003697475f1a..defa7e778a2440240ec844d7daed0d75257767bd 100644
--- a/README.md
+++ b/README.md
@@ -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
 
-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
 
diff --git a/doc/config.md b/doc/config.md
index d4bf092ed6b9f95cfa8be944b4e1eb085093e3f8..cf012430f495a04af9dea9a034847df19b16492e 100644
--- a/doc/config.md
+++ b/doc/config.md
@@ -1,27 +1,72 @@
 # Äž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).
-Äž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
-# save Alice secret to config file
-cargo run -- -S predefined -s Alice config save
+# save Alice address to config file
+gcli -S predefined -s Alice config save
 
 # show config 
-cargo run -- config show
+gcli config show
 # [stdout]
 # Äžcli config
 # duniter endpoint ws://localhost:9944
 # indexer endpoint http://localhost:4350/graphql
-# address 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY (secret defined)
+# address 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
 
 # use different address in command line
-cargo run -- --address 5Fxune7f71ZbpP2FoY3mhYcmM596Erhv1gRue4nsPwkxMR4n config show
+gcli --address 5Fxune7f71ZbpP2FoY3mhYcmM596Erhv1gRue4nsPwkxMR4n config show
 # [stdout]
 # Äžcli config
 # duniter endpoint ws://localhost:9944
 # 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.
\ No newline at end of file
+This also applies to rpc endpoint config (`--url`) or indexer config (`--indexer`).
+
+## 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
diff --git a/doc/example.md b/doc/example.md
index 7d3ea4515e4e8d76037cbca39f9763e5bbab7a54..a4a04fcf8a38e2c7a7e4de56a5fb03dd1c944ca6 100644
--- a/doc/example.md
+++ b/doc/example.md
@@ -1,10 +1,8 @@
 # Examples of gcli commands for copy-paste
 
-Useful when developing: replace `gcli` by `cargo run --` to build in debug mode and launch gcli.
-
 ## 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
 # show config commands
@@ -13,11 +11,11 @@ gcli config
 gcli config where
 # save config to use gdev network for next commands
 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
 # the arguments above can be combined
 # 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).
@@ -33,8 +31,8 @@ gcli blockchain current-block
 gcli account balance
 # get identity information without indexer
 gcli --no-indexer identity get -a 5Hn2LeMZXPFitMwrmrGucwtAPSLEiP4o5zTF7kHzMBtEkJUr 
-# get information about test1 identity (needs indexer)
-gcli identity get --username test1
+# get information about Alice identity (needs indexer)
+gcli identity get --username Alice
 # claim universal dividends
 gcli ud claim
 # transfer 5000 units
diff --git a/src/commands.rs b/src/commands.rs
index 277764b2f115d52555166b96be2221fcd64a23fe..f5bce93ef8cabaa3addbe8c9c4d64504feeed786 100644
--- a/src/commands.rs
+++ b/src/commands.rs
@@ -6,6 +6,7 @@ pub mod collective;
 pub mod distance;
 pub mod expire;
 pub mod identity;
+pub mod vault;
 pub mod net_test;
 pub mod oneshot;
 pub mod publish;
diff --git a/src/commands/vault.rs b/src/commands/vault.rs
new file mode 100644
index 0000000000000000000000000000000000000000..14c376688f4f227e17eb7a89c981a0cedb4140eb
--- /dev/null
+++ b/src/commands/vault.rs
@@ -0,0 +1,118 @@
+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);
+}
diff --git a/src/conf.rs b/src/conf.rs
index 0b8f68389e66095fb12292a91864c1bd148495ab..f07ffff1caaa1b1282cec11d52369fae14f6ea70 100644
--- a/src/conf.rs
+++ b/src/conf.rs
@@ -6,14 +6,13 @@ const APP_NAME: &str = "gcli";
 /// defines structure of config file
 #[derive(Serialize, Deserialize, Debug)]
 pub struct Config {
-	// duniter endpoint
+	/// duniter endpoint
 	pub duniter_endpoint: String,
-	// indexer endpoint
+	/// indexer endpoint
 	pub indexer_endpoint: String,
-	// user address
+	/// user address
+	/// to perform actions, user must provide secret
 	pub address: Option<AccountId>,
-	// user secret (substrate format)
-	pub secret: Option<String>,
 }
 
 impl std::default::Default for Config {
@@ -22,18 +21,12 @@ impl std::default::Default for Config {
 			duniter_endpoint: String::from(data::LOCAL_DUNITER_ENDPOINT),
 			indexer_endpoint: String::from(data::LOCAL_INDEXER_ENDPOINT),
 			address: None,
-			secret: None,
 		}
 	}
 }
 
 impl std::fmt::Display for Config {
 	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 {
 			format!("{}", address)
 		} else {
@@ -42,7 +35,7 @@ impl std::fmt::Display for Config {
 		writeln!(f, "Äžcli config")?;
 		writeln!(f, "duniter endpoint {}", self.duniter_endpoint)?;
 		writeln!(f, "indexer endpoint {}", self.indexer_endpoint)?;
-		write!(f, "address {address} {secret}")
+		write!(f, "address {address}")
 	}
 }
 
diff --git a/src/data.rs b/src/data.rs
index 87a785400747bf69181695caf7fe637d99177682..ce0e06c5b1fda5c1fa05f0039d4e73aa0fb25ea4 100644
--- a/src/data.rs
+++ b/src/data.rs
@@ -25,7 +25,6 @@ pub const GDEV_INDEXER_ENDPOINTS: [&str; 2] = [
 
 /// Data of current command
 /// can also include fetched information
-#[derive(Default)]
 pub struct Data {
 	// command line arguments
 	pub args: Args,
@@ -47,6 +46,8 @@ pub struct Data {
 	pub genesis_hash: Hash,
 	// indexer genesis hash
 	pub indexer_genesis_hash: Hash,
+	// gcli base path
+	pub project_dir: directories::ProjectDirs,
 }
 
 /// system properties defined in client specs
@@ -57,10 +58,32 @@ struct SystemProperties {
 	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
 impl Data {
 	/// --- constructor ---
-	pub fn new(args: Args) -> Self {
+	pub fn new(args: Args) -> Result<Self, GcliError> {
 		Self {
 			args,
 			cfg: conf::load_conf(),
@@ -69,7 +92,6 @@ impl Data {
 			..Default::default()
 		}
 		.overwrite_from_args()
-		.build_from_config()
 	}
 	// --- getters ---
 	// the "unwrap" should not fail if data is well prepared
@@ -85,7 +107,12 @@ impl Data {
 	pub fn keypair(&self) -> KeyPair {
 		match self.keypair.clone() {
 			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 {
@@ -102,7 +129,7 @@ impl Data {
 	}
 	// --- mutators ---
 	/// use arguments to overwrite config
-	pub fn overwrite_from_args(mut self) -> Self {
+	pub fn overwrite_from_args(mut self) -> Result<Self, GcliError> {
 		// network
 		if let Some(network) = self.args.network.clone() {
 			// a network was provided as arugment
@@ -145,49 +172,18 @@ impl Data {
 			self.cfg.indexer_endpoint = indexer_endpoint
 		}
 		// secret format and value
-		if self.args.secret_format == SecretFormat::Predefined {
-			// predefined secret format overwrites secret with mnemonic
-			match self.args.secret.clone() {
-				None => {}
-				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);
+		if let Some(secret_format) = self.args.secret_format {
+			let keypair = get_keypair(secret_format, self.args.secret.as_deref())?;
+			self.cfg.address = Some(keypair.address());
+			self.keypair = Some(keypair);
 		}
 		// address
 		if let Some(address) = self.args.address.clone() {
 			self.cfg.address = Some(AccountId::from_str(&address).expect("invalid address"));
 			// if giving address, cancel secret
-			self.cfg.secret = None
-		}
-		self
-	}
-	/// build from config
-	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);
+			self.keypair = None
 		}
-		// 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
+		Ok(self)
 	}
 	/// build a client from url
 	pub async fn build_client(mut self) -> Result<Self, GcliError> {
diff --git a/src/keys.rs b/src/keys.rs
index ad49f8f9a6c390811752b0166f4fe04b4dab858d..ae695c901cd7a205ab7be45c6fa4e22846eb3f4e 100644
--- a/src/keys.rs
+++ b/src/keys.rs
@@ -93,12 +93,9 @@ pub fn get_keypair(
 	secret: Option<&str>,
 ) -> Result<KeyPair, GcliError> {
 	match (secret_format, secret) {
-		(SecretFormat::Cesium, None) => Ok(prompt_secret(SecretFormat::Cesium)),
 		(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()),
-		_ => 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> {
 }
 
 /// 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> {
 	let mut seed = [0; 32];
 	hex::decode_to_slice(secret, &mut seed)
@@ -195,3 +194,34 @@ pub fn prompt_secret(secret_format: SecretFormat) -> KeyPair {
 		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,
+	}
+}
diff --git a/src/main.rs b/src/main.rs
index a36c699dcfb52467aa9662bdbe9daebb327679e1..98458a9e5e55994720c5012ec81cb9bba7a70b39 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -40,13 +40,13 @@ pub struct Args {
 	/// Do not use indexer
 	#[clap(long)]
 	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)
 	#[clap(short, long)]
 	secret: Option<String>,
 	/// Secret key format (seed, substrate, cesium)
-	#[clap(short = 'S', long, default_value = SecretFormat::Substrate)]
-	secret_format: SecretFormat,
+	#[clap(short = 'S', long)]
+	secret_format: Option<SecretFormat>,
 	/// Address
 	#[clap(short, long)]
 	address: Option<String>,
@@ -137,6 +137,9 @@ pub enum Subcommand {
 	/// Config (show, save...)
 	#[clap(subcommand)]
 	Config(conf::Subcommand),
+	/// Key management (import, generate, list...)
+	#[clap(subcommand)]
+	Vault(commands::vault::Subcommand),
 	/// Cesium
 	#[clap(subcommand, hide = true)]
 	Cesium(commands::cesium::Subcommand),
@@ -152,7 +155,7 @@ async fn main() -> Result<(), GcliError> {
 	env_logger::init();
 
 	// parse argument and initialize data
-	let data = Data::new(Args::parse());
+	let data = Data::new(Args::parse())?;
 
 	// match subcommands
 	let result = match data.args.subcommand.clone() {
@@ -177,6 +180,7 @@ async fn main() -> Result<(), GcliError> {
 		}
 		Subcommand::Indexer(subcommand) => indexer::handle_command(data, subcommand).await,
 		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::Publish => commands::publish::handle_command().await,
 	};