This commit is contained in:
Generated
+212
@@ -8,6 +8,41 @@ version = "2.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aead"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-common",
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aes"
|
||||||
|
version = "0.8.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cipher",
|
||||||
|
"cpufeatures",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aes-gcm"
|
||||||
|
version = "0.10.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
|
||||||
|
dependencies = [
|
||||||
|
"aead",
|
||||||
|
"aes",
|
||||||
|
"cipher",
|
||||||
|
"ctr",
|
||||||
|
"ghash",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.4"
|
version = "1.1.4"
|
||||||
@@ -47,6 +82,18 @@ version = "1.0.103"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
|
checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "argon2"
|
||||||
|
version = "0.5.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
"blake2",
|
||||||
|
"cpufeatures",
|
||||||
|
"password-hash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atk"
|
name = "atk"
|
||||||
version = "0.18.2"
|
version = "0.18.2"
|
||||||
@@ -94,6 +141,12 @@ version = "0.22.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64ct"
|
||||||
|
version = "1.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bit-set"
|
name = "bit-set"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
@@ -124,6 +177,15 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "blake2"
|
||||||
|
version = "0.10.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
|
||||||
|
dependencies = [
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block-buffer"
|
name = "block-buffer"
|
||||||
version = "0.10.4"
|
version = "0.10.4"
|
||||||
@@ -321,6 +383,16 @@ dependencies = [
|
|||||||
"windows-link 0.2.1",
|
"windows-link 0.2.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cipher"
|
||||||
|
version = "0.4.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-common",
|
||||||
|
"inout",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "combine"
|
name = "combine"
|
||||||
version = "4.6.7"
|
version = "4.6.7"
|
||||||
@@ -421,6 +493,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"generic-array",
|
"generic-array",
|
||||||
|
"rand_core",
|
||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -463,6 +536,15 @@ version = "0.0.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1"
|
checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ctr"
|
||||||
|
version = "0.9.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
|
||||||
|
dependencies = [
|
||||||
|
"cipher",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "darling"
|
name = "darling"
|
||||||
version = "0.23.0"
|
version = "0.23.0"
|
||||||
@@ -546,6 +628,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"block-buffer",
|
"block-buffer",
|
||||||
"crypto-common",
|
"crypto-common",
|
||||||
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1021,6 +1104,16 @@ dependencies = [
|
|||||||
"r-efi 6.0.0",
|
"r-efi 6.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ghash"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
|
||||||
|
dependencies = [
|
||||||
|
"opaque-debug",
|
||||||
|
"polyval",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gio"
|
name = "gio"
|
||||||
version = "0.18.4"
|
version = "0.18.4"
|
||||||
@@ -1466,6 +1559,15 @@ dependencies = [
|
|||||||
"cfb",
|
"cfb",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "inout"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ipnet"
|
name = "ipnet"
|
||||||
version = "2.12.0"
|
version = "2.12.0"
|
||||||
@@ -2010,6 +2112,12 @@ version = "1.21.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "opaque-debug"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "option-ext"
|
name = "option-ext"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -2064,6 +2172,17 @@ dependencies = [
|
|||||||
"windows-link 0.2.1",
|
"windows-link 0.2.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "password-hash"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
|
||||||
|
dependencies = [
|
||||||
|
"base64ct",
|
||||||
|
"rand_core",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.2"
|
version = "2.3.2"
|
||||||
@@ -2174,6 +2293,18 @@ dependencies = [
|
|||||||
"miniz_oxide",
|
"miniz_oxide",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "polyval"
|
||||||
|
version = "0.6.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"opaque-debug",
|
||||||
|
"universal-hash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "potential_utf"
|
name = "potential_utf"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@@ -2189,6 +2320,15 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ppv-lite86"
|
||||||
|
version = "0.2.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "precomputed-hash"
|
name = "precomputed-hash"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -2292,6 +2432,7 @@ name = "rabby"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rabby-core",
|
"rabby-core",
|
||||||
|
"rabby-runtime",
|
||||||
"serde",
|
"serde",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
@@ -2309,10 +2450,45 @@ dependencies = [
|
|||||||
name = "rabby-runtime"
|
name = "rabby-runtime"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"aes-gcm",
|
||||||
|
"argon2",
|
||||||
|
"base64 0.22.1",
|
||||||
"rabby-core",
|
"rabby-core",
|
||||||
|
"rand",
|
||||||
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.8.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"rand_chacha",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_chacha"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||||
|
dependencies = [
|
||||||
|
"ppv-lite86",
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.6.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.2.17",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "raw-window-handle"
|
name = "raw-window-handle"
|
||||||
version = "0.6.2"
|
version = "0.6.2"
|
||||||
@@ -2831,6 +3007,12 @@ version = "0.11.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "subtle"
|
||||||
|
version = "2.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "swift-rs"
|
name = "swift-rs"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
@@ -3575,6 +3757,16 @@ version = "1.13.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
|
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "universal-hash"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-common",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.5.8"
|
version = "2.5.8"
|
||||||
@@ -4345,6 +4537,26 @@ dependencies = [
|
|||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy"
|
||||||
|
version = "0.8.54"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerocopy-derive"
|
||||||
|
version = "0.8.54"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.118",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerofrom"
|
name = "zerofrom"
|
||||||
version = "0.1.8"
|
version = "0.1.8"
|
||||||
|
|||||||
@@ -61,3 +61,8 @@ The current app can open but is still a shell. The MVP work should prioritize:
|
|||||||
- Unsafe/incomplete wallet requests are rejected.
|
- Unsafe/incomplete wallet requests are rejected.
|
||||||
- Dashboard totals and high-risk approval counts are computed.
|
- Dashboard totals and high-risk approval counts are computed.
|
||||||
- Settings customization defaults validate.
|
- Settings customization defaults validate.
|
||||||
|
|
||||||
|
|
||||||
|
## Implemented runtime slices
|
||||||
|
|
||||||
|
- Encrypted local wallet dashboard vault: `rabby-runtime::wallet_vault::EncryptedWalletVault` writes AES-256-GCM encrypted JSON envelopes using Argon2id-derived keys, rejects blank/wrong passphrases, avoids plaintext wallet fields in storage, and is exposed to Tauri via `save_demo_wallet_vault` / `load_wallet_vault`.
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ pub struct WalletFeatureSpec {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub const RABBY_WALLET_MVP_FEATURES: &[WalletFeatureSpec] = &[
|
pub const RABBY_WALLET_MVP_FEATURES: &[WalletFeatureSpec] = &[
|
||||||
WalletFeatureSpec { key: "onboarding-password", title: "Create/unlock wallet", area: WalletFeatureArea::Onboarding, rabby_reference: "Welcome, CreatePassword, Unlock, ForgotPassword", mvp_acceptance: "User can create a password, lock/unlock, and keep encrypted local state", status: WalletFeatureStatus::Modeled },
|
WalletFeatureSpec { key: "onboarding-password", title: "Create/unlock wallet", area: WalletFeatureArea::Onboarding, rabby_reference: "Welcome, CreatePassword, Unlock, ForgotPassword", mvp_acceptance: "User can create a password, lock/unlock, and keep encrypted local state", status: WalletFeatureStatus::RuntimeImplemented },
|
||||||
WalletFeatureSpec { key: "import-create-accounts", title: "Import/create accounts", area: WalletFeatureArea::Accounts, rabby_reference: "CreateMnemonics, ImportMnemonics, ImportPrivateKey, ImportJson, watch address", mvp_acceptance: "Seed phrase/private key/JSON/watch-only account records validate and display", status: WalletFeatureStatus::Modeled },
|
WalletFeatureSpec { key: "import-create-accounts", title: "Import/create accounts", area: WalletFeatureArea::Accounts, rabby_reference: "CreateMnemonics, ImportMnemonics, ImportPrivateKey, ImportJson, watch address", mvp_acceptance: "Seed phrase/private key/JSON/watch-only account records validate and display", status: WalletFeatureStatus::Modeled },
|
||||||
WalletFeatureSpec { key: "hardware-wallets", title: "Hardware and institutional accounts", area: WalletFeatureArea::Accounts, rabby_reference: "Ledger, Trezor, Keystone, GridPlus, OneKey, BitBox02, Gnosis Safe, Coinbase, WalletConnect, Cobo Argus", mvp_acceptance: "Account source model covers hardware/safe/walletconnect/custody sources", status: WalletFeatureStatus::Modeled },
|
WalletFeatureSpec { key: "hardware-wallets", title: "Hardware and institutional accounts", area: WalletFeatureArea::Accounts, rabby_reference: "Ledger, Trezor, Keystone, GridPlus, OneKey, BitBox02, Gnosis Safe, Coinbase, WalletConnect, Cobo Argus", mvp_acceptance: "Account source model covers hardware/safe/walletconnect/custody sources", status: WalletFeatureStatus::Modeled },
|
||||||
WalletFeatureSpec { key: "multi-chain", title: "Multi-chain network support", area: WalletFeatureArea::Portfolio, rabby_reference: "ChainList, CustomRPC, CustomTestnet, offline chain support", mvp_acceptance: "Chains have IDs, RPC URLs, testnet/custom/offline flags, and validation", status: WalletFeatureStatus::Modeled },
|
WalletFeatureSpec { key: "multi-chain", title: "Multi-chain network support", area: WalletFeatureArea::Portfolio, rabby_reference: "ChainList, CustomRPC, CustomTestnet, offline chain support", mvp_acceptance: "Chains have IDs, RPC URLs, testnet/custom/offline flags, and validation", status: WalletFeatureStatus::Modeled },
|
||||||
|
|||||||
@@ -10,4 +10,9 @@ path = "src/lib.rs"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rabby-core = { path = "../rabby-core" }
|
rabby-core = { path = "../rabby-core" }
|
||||||
|
aes-gcm = "0.10"
|
||||||
|
argon2 = "0.5"
|
||||||
|
base64 = "0.22"
|
||||||
|
rand = "0.8"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
pub mod config_store;
|
pub mod config_store;
|
||||||
pub mod pty;
|
pub mod pty;
|
||||||
|
pub mod wallet_vault;
|
||||||
|
|||||||
@@ -0,0 +1,280 @@
|
|||||||
|
use aes_gcm::aead::{Aead, KeyInit};
|
||||||
|
use aes_gcm::{Aes256Gcm, Nonce};
|
||||||
|
use argon2::Argon2;
|
||||||
|
use base64::engine::general_purpose::STANDARD as B64;
|
||||||
|
use base64::Engine;
|
||||||
|
use rabby_core::WalletDashboard;
|
||||||
|
use rand::rngs::OsRng;
|
||||||
|
use rand::RngCore;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fmt;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
const VAULT_VERSION: u8 = 1;
|
||||||
|
const SALT_LEN: usize = 16;
|
||||||
|
const NONCE_LEN: usize = 12;
|
||||||
|
const KEY_LEN: usize = 32;
|
||||||
|
const CIPHER: &str = "aes-256-gcm";
|
||||||
|
const KDF: &str = "argon2id";
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum WalletVaultError {
|
||||||
|
Io(std::io::Error),
|
||||||
|
Json(serde_json::Error),
|
||||||
|
Crypto(String),
|
||||||
|
Invalid(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for WalletVaultError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Io(err) => write!(f, "wallet vault I/O error: {err}"),
|
||||||
|
Self::Json(err) => write!(f, "wallet vault JSON error: {err}"),
|
||||||
|
Self::Crypto(message) => write!(f, "wallet vault crypto error: {message}"),
|
||||||
|
Self::Invalid(message) => write!(f, "invalid wallet vault: {message}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for WalletVaultError {}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for WalletVaultError {
|
||||||
|
fn from(value: std::io::Error) -> Self {
|
||||||
|
Self::Io(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<serde_json::Error> for WalletVaultError {
|
||||||
|
fn from(value: serde_json::Error) -> Self {
|
||||||
|
Self::Json(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct VaultEnvelope {
|
||||||
|
pub version: u8,
|
||||||
|
pub kdf: String,
|
||||||
|
pub cipher: String,
|
||||||
|
pub salt_b64: String,
|
||||||
|
pub nonce_b64: String,
|
||||||
|
pub ciphertext_b64: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||||
|
pub struct WalletVaultStatus {
|
||||||
|
pub path: String,
|
||||||
|
pub cipher: &'static str,
|
||||||
|
pub kdf: &'static str,
|
||||||
|
pub encrypted: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EncryptedWalletVault;
|
||||||
|
|
||||||
|
impl EncryptedWalletVault {
|
||||||
|
pub fn save_dashboard(
|
||||||
|
path: impl AsRef<Path>,
|
||||||
|
dashboard: &WalletDashboard,
|
||||||
|
passphrase: &str,
|
||||||
|
) -> Result<WalletVaultStatus, WalletVaultError> {
|
||||||
|
validate_passphrase(passphrase)?;
|
||||||
|
dashboard.validate().map_err(WalletVaultError::Invalid)?;
|
||||||
|
|
||||||
|
let mut salt = [0u8; SALT_LEN];
|
||||||
|
let mut nonce = [0u8; NONCE_LEN];
|
||||||
|
OsRng.fill_bytes(&mut salt);
|
||||||
|
OsRng.fill_bytes(&mut nonce);
|
||||||
|
|
||||||
|
let key = derive_key(passphrase, &salt)?;
|
||||||
|
let cipher = Aes256Gcm::new_from_slice(&key).map_err(|_| {
|
||||||
|
WalletVaultError::Crypto("failed to create AES-256-GCM key".to_string())
|
||||||
|
})?;
|
||||||
|
let plaintext = serde_json::to_vec(dashboard)?;
|
||||||
|
let ciphertext = cipher
|
||||||
|
.encrypt(Nonce::from_slice(&nonce), plaintext.as_ref())
|
||||||
|
.map_err(|_| {
|
||||||
|
WalletVaultError::Crypto("failed to encrypt wallet dashboard".to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let envelope = VaultEnvelope {
|
||||||
|
version: VAULT_VERSION,
|
||||||
|
kdf: KDF.to_string(),
|
||||||
|
cipher: CIPHER.to_string(),
|
||||||
|
salt_b64: B64.encode(salt),
|
||||||
|
nonce_b64: B64.encode(nonce),
|
||||||
|
ciphertext_b64: B64.encode(ciphertext),
|
||||||
|
};
|
||||||
|
write_envelope(path.as_ref(), &envelope)?;
|
||||||
|
Ok(WalletVaultStatus {
|
||||||
|
path: path.as_ref().display().to_string(),
|
||||||
|
cipher: CIPHER,
|
||||||
|
kdf: KDF,
|
||||||
|
encrypted: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_dashboard(
|
||||||
|
path: impl AsRef<Path>,
|
||||||
|
passphrase: &str,
|
||||||
|
) -> Result<WalletDashboard, WalletVaultError> {
|
||||||
|
validate_passphrase(passphrase)?;
|
||||||
|
let raw = std::fs::read_to_string(path.as_ref())?;
|
||||||
|
let envelope: VaultEnvelope = serde_json::from_str(&raw)?;
|
||||||
|
envelope.validate()?;
|
||||||
|
|
||||||
|
let salt = decode_fixed::<SALT_LEN>(&envelope.salt_b64, "salt")?;
|
||||||
|
let nonce = decode_fixed::<NONCE_LEN>(&envelope.nonce_b64, "nonce")?;
|
||||||
|
let ciphertext = B64
|
||||||
|
.decode(envelope.ciphertext_b64.as_bytes())
|
||||||
|
.map_err(|_| WalletVaultError::Invalid("ciphertext is not valid base64".to_string()))?;
|
||||||
|
let key = derive_key(passphrase, &salt)?;
|
||||||
|
let cipher = Aes256Gcm::new_from_slice(&key).map_err(|_| {
|
||||||
|
WalletVaultError::Crypto("failed to create AES-256-GCM key".to_string())
|
||||||
|
})?;
|
||||||
|
let plaintext = cipher
|
||||||
|
.decrypt(Nonce::from_slice(&nonce), ciphertext.as_ref())
|
||||||
|
.map_err(|_| {
|
||||||
|
WalletVaultError::Crypto(
|
||||||
|
"failed to decrypt wallet vault; check passphrase".to_string(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let dashboard: WalletDashboard = serde_json::from_slice(&plaintext)?;
|
||||||
|
dashboard.validate().map_err(WalletVaultError::Invalid)?;
|
||||||
|
Ok(dashboard)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VaultEnvelope {
|
||||||
|
fn validate(&self) -> Result<(), WalletVaultError> {
|
||||||
|
if self.version != VAULT_VERSION {
|
||||||
|
return Err(WalletVaultError::Invalid(format!(
|
||||||
|
"unsupported vault version {}",
|
||||||
|
self.version
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if self.kdf != KDF {
|
||||||
|
return Err(WalletVaultError::Invalid(format!(
|
||||||
|
"unsupported vault KDF {}",
|
||||||
|
self.kdf
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if self.cipher != CIPHER {
|
||||||
|
return Err(WalletVaultError::Invalid(format!(
|
||||||
|
"unsupported vault cipher {}",
|
||||||
|
self.cipher
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if self.ciphertext_b64.trim().is_empty() {
|
||||||
|
return Err(WalletVaultError::Invalid(
|
||||||
|
"ciphertext is required".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_passphrase(passphrase: &str) -> Result<(), WalletVaultError> {
|
||||||
|
if passphrase.trim().len() < 8 {
|
||||||
|
return Err(WalletVaultError::Invalid(
|
||||||
|
"passphrase must be at least 8 non-blank characters".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn derive_key(passphrase: &str, salt: &[u8]) -> Result<[u8; KEY_LEN], WalletVaultError> {
|
||||||
|
let mut key = [0u8; KEY_LEN];
|
||||||
|
Argon2::default()
|
||||||
|
.hash_password_into(passphrase.as_bytes(), salt, &mut key)
|
||||||
|
.map_err(|err| WalletVaultError::Crypto(format!("failed to derive key: {err}")))?;
|
||||||
|
Ok(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_fixed<const N: usize>(value: &str, label: &str) -> Result<[u8; N], WalletVaultError> {
|
||||||
|
let decoded = B64
|
||||||
|
.decode(value.as_bytes())
|
||||||
|
.map_err(|_| WalletVaultError::Invalid(format!("{label} is not valid base64")))?;
|
||||||
|
decoded
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| WalletVaultError::Invalid(format!("{label} has incorrect length")))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_envelope(path: &Path, envelope: &VaultEnvelope) -> Result<(), WalletVaultError> {
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
std::fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
let raw = serde_json::to_string_pretty(envelope)?;
|
||||||
|
std::fs::write(path, raw)?;
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use rabby_core::demo_dashboard;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
fn temp_path(name: &str) -> PathBuf {
|
||||||
|
let unique = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_nanos();
|
||||||
|
std::env::temp_dir().join(format!("rabby-vault-{name}-{unique}.json"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn encrypted_vault_round_trips_dashboard_with_passphrase() {
|
||||||
|
let path = temp_path("round-trip");
|
||||||
|
let dashboard = demo_dashboard();
|
||||||
|
|
||||||
|
EncryptedWalletVault::save_dashboard(&path, &dashboard, "correct horse battery staple")
|
||||||
|
.unwrap();
|
||||||
|
let restored =
|
||||||
|
EncryptedWalletVault::load_dashboard(&path, "correct horse battery staple").unwrap();
|
||||||
|
|
||||||
|
assert_eq!(restored, dashboard);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn encrypted_vault_does_not_store_wallet_plaintext() {
|
||||||
|
let path = temp_path("opaque");
|
||||||
|
let dashboard = demo_dashboard();
|
||||||
|
|
||||||
|
EncryptedWalletVault::save_dashboard(&path, &dashboard, "correct horse battery staple")
|
||||||
|
.unwrap();
|
||||||
|
let raw = std::fs::read_to_string(&path).unwrap();
|
||||||
|
|
||||||
|
assert!(raw.contains("aes-256-gcm"));
|
||||||
|
assert!(!raw.contains(&dashboard.active_account.address));
|
||||||
|
assert!(!raw.contains("Main Wallet"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn encrypted_vault_rejects_wrong_passphrase() {
|
||||||
|
let path = temp_path("wrong-passphrase");
|
||||||
|
let dashboard = demo_dashboard();
|
||||||
|
|
||||||
|
EncryptedWalletVault::save_dashboard(&path, &dashboard, "correct horse battery staple")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let err = EncryptedWalletVault::load_dashboard(&path, "wrong password").unwrap_err();
|
||||||
|
assert!(
|
||||||
|
err.to_string().to_lowercase().contains("decrypt")
|
||||||
|
|| err.to_string().to_lowercase().contains("passphrase")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn encrypted_vault_rejects_blank_passphrase() {
|
||||||
|
let path = temp_path("blank-passphrase");
|
||||||
|
let dashboard = demo_dashboard();
|
||||||
|
|
||||||
|
let err = EncryptedWalletVault::save_dashboard(&path, &dashboard, " ").unwrap_err();
|
||||||
|
|
||||||
|
assert!(err.to_string().to_lowercase().contains("passphrase"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,5 +10,6 @@ tauri-build = { version = "2", features = [] }
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rabby-core = { path = "../rabby-core" }
|
rabby-core = { path = "../rabby-core" }
|
||||||
|
rabby-runtime = { path = "../rabby-runtime" }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
tauri = { version = "2", features = [] }
|
tauri = { version = "2", features = [] }
|
||||||
|
|||||||
+45
-1
@@ -1,7 +1,9 @@
|
|||||||
use rabby_core::{
|
use rabby_core::{
|
||||||
demo_dashboard, wallet_feature_summary, WalletDashboard, WalletFeatureStatus, Workspace,
|
demo_dashboard, wallet_feature_summary, WalletDashboard, WalletFeatureStatus, Workspace,
|
||||||
};
|
};
|
||||||
|
use rabby_runtime::wallet_vault::{EncryptedWalletVault, WalletVaultStatus};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct FeatureRow {
|
struct FeatureRow {
|
||||||
@@ -41,13 +43,55 @@ fn wallet_dashboard() -> WalletDashboard {
|
|||||||
demo_dashboard()
|
demo_dashboard()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn save_demo_wallet_vault(
|
||||||
|
passphrase: String,
|
||||||
|
path: Option<String>,
|
||||||
|
) -> Result<WalletVaultStatus, String> {
|
||||||
|
let vault_path = resolve_vault_path(path);
|
||||||
|
EncryptedWalletVault::save_dashboard(&vault_path, &demo_dashboard(), &passphrase)
|
||||||
|
.map_err(|err| err.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn load_wallet_vault(path: String, passphrase: String) -> Result<WalletDashboard, String> {
|
||||||
|
EncryptedWalletVault::load_dashboard(path, &passphrase).map_err(|err| err.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_vault_path(path: Option<String>) -> PathBuf {
|
||||||
|
path.map(PathBuf::from)
|
||||||
|
.unwrap_or_else(|| std::env::temp_dir().join("rabby-demo-wallet-vault.json"))
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
workspace_summary,
|
workspace_summary,
|
||||||
wallet_mvp_summary,
|
wallet_mvp_summary,
|
||||||
wallet_dashboard
|
wallet_dashboard,
|
||||||
|
save_demo_wallet_vault,
|
||||||
|
load_wallet_vault
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("failed to run Rabby Tauri application");
|
.expect("failed to run Rabby Tauri application");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_vault_path_has_stable_file_name() {
|
||||||
|
let path = resolve_vault_path(None);
|
||||||
|
assert_eq!(
|
||||||
|
path.file_name().and_then(|value| value.to_str()),
|
||||||
|
Some("rabby-demo-wallet-vault.json")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn explicit_vault_path_is_preserved() {
|
||||||
|
let path = resolve_vault_path(Some("/tmp/custom-rabby-vault.json".to_string()));
|
||||||
|
assert_eq!(path, PathBuf::from("/tmp/custom-rabby-vault.json"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const fallbackDashboard = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const fallbackFeatures = [
|
const fallbackFeatures = [
|
||||||
['Create/unlock wallet', 'Password, lock/unlock, encrypted local state', 'Modeled'],
|
['Create/unlock wallet', 'Password, lock/unlock, encrypted local state', 'RuntimeImplemented'],
|
||||||
['Import/create accounts', 'Seed phrase, private key, JSON, watch-only', 'Modeled'],
|
['Import/create accounts', 'Seed phrase, private key, JSON, watch-only', 'Modeled'],
|
||||||
['Multi-chain networks', 'Built-in, custom RPC, testnet, offline chain flags', 'Modeled'],
|
['Multi-chain networks', 'Built-in, custom RPC, testnet, offline chain flags', 'Modeled'],
|
||||||
['Portfolio dashboard', 'Balances, NFTs, DeFi positions, activity', 'UiPrototype'],
|
['Portfolio dashboard', 'Balances, NFTs, DeFi positions, activity', 'UiPrototype'],
|
||||||
@@ -117,6 +117,25 @@ function renderFeatures(features) {
|
|||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveDemoVault() {
|
||||||
|
const passphrase = document.getElementById('vault-passphrase').value;
|
||||||
|
const output = document.getElementById('vault-status');
|
||||||
|
output.textContent = 'Encrypting demo vault…';
|
||||||
|
try {
|
||||||
|
const tauri = window.__TAURI__?.core;
|
||||||
|
if (!tauri) throw new Error('Tauri bridge unavailable in browser preview');
|
||||||
|
const status = await tauri.invoke('save_demo_wallet_vault', { passphrase, path: null });
|
||||||
|
output.textContent = `Encrypted with ${status.cipher} + ${status.kdf}: ${status.path}`;
|
||||||
|
} catch (error) {
|
||||||
|
output.textContent = String(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function wireVaultActions() {
|
||||||
|
const button = document.getElementById('save-vault');
|
||||||
|
if (button) button.addEventListener('click', saveDemoVault);
|
||||||
|
}
|
||||||
|
|
||||||
async function boot() {
|
async function boot() {
|
||||||
const [dashboard, features] = await Promise.all([
|
const [dashboard, features] = await Promise.all([
|
||||||
invokeOrFallback('wallet_dashboard', fallbackDashboard),
|
invokeOrFallback('wallet_dashboard', fallbackDashboard),
|
||||||
@@ -124,6 +143,7 @@ async function boot() {
|
|||||||
]);
|
]);
|
||||||
renderDashboard(dashboard);
|
renderDashboard(dashboard);
|
||||||
renderFeatures(features);
|
renderFeatures(features);
|
||||||
|
wireVaultActions();
|
||||||
}
|
}
|
||||||
|
|
||||||
boot();
|
boot();
|
||||||
|
|||||||
@@ -120,6 +120,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="settings-summary" class="settings-grid"></div>
|
<div id="settings-summary" class="settings-grid"></div>
|
||||||
|
<div class="vault-form">
|
||||||
|
<p class="eyebrow">Encrypted local state</p>
|
||||||
|
<label for="vault-passphrase">Demo vault passphrase</label>
|
||||||
|
<div class="vault-row">
|
||||||
|
<input id="vault-passphrase" type="password" value="correct horse battery staple" />
|
||||||
|
<button id="save-vault" class="secondary" type="button">Save vault</button>
|
||||||
|
</div>
|
||||||
|
<small id="vault-status">AES-256-GCM + Argon2id runtime wired through Tauri.</small>
|
||||||
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -257,6 +257,34 @@ h3 { margin-bottom: 0; font-size: 18px; }
|
|||||||
}
|
}
|
||||||
.settings-grid span { display: block; color: #7d89a5; font-size: 12px; }
|
.settings-grid span { display: block; color: #7d89a5; font-size: 12px; }
|
||||||
|
|
||||||
|
.vault-form {
|
||||||
|
margin-top: 18px;
|
||||||
|
padding-top: 18px;
|
||||||
|
border-top: 1px solid #edf1fb;
|
||||||
|
}
|
||||||
|
.vault-form label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #4a5878;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
.vault-row { display: flex; gap: 8px; }
|
||||||
|
.vault-row input {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
border: 1px solid #dfe6f7;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
color: #24345f;
|
||||||
|
background: #f8faff;
|
||||||
|
}
|
||||||
|
.vault-form small {
|
||||||
|
display: block;
|
||||||
|
margin-top: 10px;
|
||||||
|
color: #7d89a5;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
.feature-grid {
|
.feature-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
|
|||||||
Reference in New Issue
Block a user