record encryption (#1058)

* record encryption

* move paserk impl

* implicit assertions

* move wrapped cek

* add another test

* use host

* undo stray change

* more tests and docs

* fmt

* Update atuin-client/src/record/encryption.rs

Co-authored-by: Matteo Martellini <matteo@mercxry.me>

* Update atuin-client/src/record/encryption.rs

Co-authored-by: Matteo Martellini <matteo@mercxry.me>

* typo

---------

Co-authored-by: Matteo Martellini <matteo@mercxry.me>
This commit is contained in:
Conrad Ludgate 2023-06-26 07:52:37 +01:00 committed by GitHub
parent 1a63649608
commit 6c53242b64
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 976 additions and 89 deletions

452
Cargo.lock generated
View file

@ -18,7 +18,7 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
dependencies = [
"getrandom",
"getrandom 0.2.7",
"once_cell",
"version_check",
]
@ -54,7 +54,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95c2fcf79ad1932ac6269a738109997a83c227c09b75842ae564dc8ede6a861c"
dependencies = [
"base64ct",
"blake2",
"blake2 0.10.6",
"password-hash",
]
@ -151,15 +151,17 @@ dependencies = [
"memchr",
"minspan",
"parse_duration",
"rand",
"rand 0.8.5",
"regex",
"reqwest",
"rmp",
"rusty_paserk",
"rusty_paseto",
"semver",
"serde",
"serde_json",
"serde_regex",
"sha2",
"sha2 0.10.6",
"shellexpand",
"sql-builder",
"sqlx",
@ -176,8 +178,9 @@ name = "atuin-common"
version = "15.0.0"
dependencies = [
"chrono",
"eyre",
"pretty_assertions",
"rand",
"rand 0.8.5",
"serde",
"typed-builder",
"uuid",
@ -199,7 +202,7 @@ dependencies = [
"eyre",
"fs-err",
"http",
"rand",
"rand 0.8.5",
"reqwest",
"semver",
"serde",
@ -325,13 +328,33 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "blake2"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a4e37d16930f5459780f5621038b6382b9bb37c19016f39fb6b5808d831f174"
dependencies = [
"crypto-mac",
"digest 0.9.0",
"opaque-debug",
]
[[package]]
name = "blake2"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
dependencies = [
"digest",
"digest 0.10.7",
]
[[package]]
name = "block-buffer"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
dependencies = [
"generic-array",
]
[[package]]
@ -379,6 +402,28 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chacha20"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c80e5460aa66fe3b91d40bcbdab953a597b60053e34d684ac6903f863b680a6"
dependencies = [
"cfg-if",
"cipher 0.3.0",
"cpufeatures",
]
[[package]]
name = "chacha20"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
dependencies = [
"cfg-if",
"cipher 0.4.4",
"cpufeatures",
]
[[package]]
name = "chrono"
version = "0.4.22"
@ -390,7 +435,7 @@ dependencies = [
"num-integer",
"num-traits",
"serde",
"time",
"time 0.1.44",
"wasm-bindgen",
"winapi",
]
@ -404,6 +449,15 @@ dependencies = [
"chrono",
]
[[package]]
name = "cipher"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7"
dependencies = [
"generic-array",
]
[[package]]
name = "cipher"
version = "0.4.4"
@ -504,6 +558,12 @@ dependencies = [
"windows-sys 0.42.0",
]
[[package]]
name = "const-oid"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "520fbf3c07483f94e3e3ca9d0cfd913d7718ef2483d2cfd91c0d9e91474ab913"
[[package]]
name = "core-foundation"
version = "0.9.3"
@ -597,10 +657,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"rand_core",
"rand_core 0.6.4",
"typenum",
]
[[package]]
name = "crypto-mac"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab"
dependencies = [
"generic-array",
"subtle",
]
[[package]]
name = "ctor"
version = "0.1.26"
@ -611,6 +681,44 @@ dependencies = [
"syn 1.0.99",
]
[[package]]
name = "curve25519-dalek"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61"
dependencies = [
"byteorder",
"digest 0.9.0",
"rand_core 0.5.1",
"subtle",
"zeroize",
]
[[package]]
name = "curve25519-dalek"
version = "4.0.0-rc.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03d928d978dbec61a1167414f5ec534f24bea0d7a0d24dd9b6233d3d8223e585"
dependencies = [
"cfg-if",
"digest 0.10.7",
"fiat-crypto",
"packed_simd_2",
"platforms",
"subtle",
"zeroize",
]
[[package]]
name = "der"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56acb310e15652100da43d130af8d97b509e95af61aab1c5a7939ef24337ee17"
dependencies = [
"const-oid",
"zeroize",
]
[[package]]
name = "diff"
version = "0.1.13"
@ -619,11 +727,20 @@ checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "digest"
version = "0.10.5"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c"
checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
dependencies = [
"block-buffer",
"generic-array",
]
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer 0.10.3",
"crypto-common",
"subtle",
]
@ -666,6 +783,52 @@ dependencies = [
"dirs",
]
[[package]]
name = "ed25519"
version = "1.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7"
dependencies = [
"signature 1.6.4",
]
[[package]]
name = "ed25519"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fb04eee5d9d907f29e80ee6b0e78f7e2c82342c63e3580d8c4f69d9d5aad963"
dependencies = [
"pkcs8",
"signature 2.1.0",
]
[[package]]
name = "ed25519-dalek"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d"
dependencies = [
"curve25519-dalek 3.2.0",
"ed25519 1.5.3",
"rand 0.7.3",
"serde",
"sha2 0.9.9",
"zeroize",
]
[[package]]
name = "ed25519-dalek"
version = "2.0.0-rc.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "798f704d128510932661a3489b08e3f4c934a01d61c5def59ae7b8e48f19665a"
dependencies = [
"curve25519-dalek 4.0.0-rc.2",
"ed25519 2.2.1",
"serde",
"sha2 0.10.6",
"zeroize",
]
[[package]]
name = "either"
version = "1.8.0"
@ -737,6 +900,12 @@ dependencies = [
"once_cell",
]
[[package]]
name = "fiat-crypto"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77"
[[package]]
name = "filedescriptor"
version = "0.8.2"
@ -877,6 +1046,17 @@ dependencies = [
"version_check",
]
[[package]]
name = "getrandom"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
dependencies = [
"cfg-if",
"libc",
"wasi 0.9.0+wasi-snapshot-preview1",
]
[[package]]
name = "getrandom"
version = "0.2.7"
@ -970,7 +1150,7 @@ version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
"digest 0.10.7",
]
[[package]]
@ -1165,6 +1345,15 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "iso8601"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5b94fbeb759754d87e1daea745bc8efd3037cd16980331fe1d1524c9a79ce96"
dependencies = [
"nom",
]
[[package]]
name = "itertools"
version = "0.10.5"
@ -1201,6 +1390,12 @@ version = "0.2.141"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5"
[[package]]
name = "libm"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fc7aa29613bd6a620df431842069224d8bc9011086b1db4c0e0cd47fa03ec9a"
[[package]]
name = "libsqlite3-sys"
version = "0.24.2"
@ -1281,7 +1476,7 @@ version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66b48670c893079d3c2ed79114e3644b7004df1c361a4e0ad52e2e6940d07c3d"
dependencies = [
"digest",
"digest 0.10.7",
]
[[package]]
@ -1466,6 +1661,16 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "packed_simd_2"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1914cd452d8fccd6f9db48147b29fd4ae05bea9dc5d9ad578509f72415de282"
dependencies = [
"cfg-if",
"libm",
]
[[package]]
name = "parking_lot"
version = "0.11.2"
@ -1532,7 +1737,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
dependencies = [
"base64ct",
"rand_core",
"rand_core 0.6.4",
"subtle",
]
@ -1554,7 +1759,7 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917"
dependencies = [
"digest",
"digest 0.10.7",
]
[[package]]
@ -1595,12 +1800,28 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkcs8"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
dependencies = [
"der",
"spki",
]
[[package]]
name = "pkg-config"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"
[[package]]
name = "platforms"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3d7ddaed09e0eb771a79ab0fd64609ba0afb0a8366421957936ad14cbd13630"
[[package]]
name = "poly1305"
version = "0.8.0"
@ -1654,6 +1875,19 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
dependencies = [
"getrandom 0.1.16",
"libc",
"rand_chacha 0.2.2",
"rand_core 0.5.1",
"rand_hc",
]
[[package]]
name = "rand"
version = "0.8.5"
@ -1661,8 +1895,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
"rand_chacha 0.3.1",
"rand_core 0.6.4",
]
[[package]]
name = "rand_chacha"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
dependencies = [
"ppv-lite86",
"rand_core 0.5.1",
]
[[package]]
@ -1672,7 +1916,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
"rand_core 0.6.4",
]
[[package]]
name = "rand_core"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
dependencies = [
"getrandom 0.1.16",
]
[[package]]
@ -1681,7 +1934,16 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
"getrandom 0.2.7",
]
[[package]]
name = "rand_hc"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
dependencies = [
"rand_core 0.5.1",
]
[[package]]
@ -1699,7 +1961,7 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
dependencies = [
"getrandom",
"getrandom 0.2.7",
"redox_syscall",
"thiserror",
]
@ -1884,6 +2146,48 @@ version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70"
[[package]]
name = "rusty_paserk"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db0fa9527e508b8e466bbced04aae220b1d87b03a080868931ae7020c87d3902"
dependencies = [
"argon2",
"base64 0.13.1",
"base64ct",
"blake2 0.10.6",
"chacha20 0.9.1",
"cipher 0.4.4",
"curve25519-dalek 4.0.0-rc.2",
"digest 0.10.7",
"ed25519-dalek 2.0.0-rc.2",
"generic-array",
"rand 0.8.5",
"rusty_paseto",
"serde",
"sha2 0.10.6",
"subtle",
"x25519-dalek",
]
[[package]]
name = "rusty_paseto"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76c81107ec38df7977a58555d21eef19584878a6ce4d0005efbb16438bce19f4"
dependencies = [
"base64 0.13.1",
"blake2 0.9.2",
"chacha20 0.8.2",
"ed25519-dalek 1.0.1",
"hex",
"iso8601",
"ring",
"thiserror",
"time 0.3.22",
"zeroize",
]
[[package]]
name = "ryu"
version = "1.0.11"
@ -1896,7 +2200,7 @@ version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213"
dependencies = [
"cipher",
"cipher 0.4.4",
]
[[package]]
@ -2024,7 +2328,20 @@ checksum = "006769ba83e921b3085caa8334186b00cf92b4cb1a6cf4632fbccc8eff5c7549"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
"digest 0.10.7",
]
[[package]]
name = "sha2"
version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800"
dependencies = [
"block-buffer 0.9.0",
"cfg-if",
"cpufeatures",
"digest 0.9.0",
"opaque-debug",
]
[[package]]
@ -2035,7 +2352,7 @@ checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
"digest 0.10.7",
]
[[package]]
@ -2086,6 +2403,18 @@ dependencies = [
"libc",
]
[[package]]
name = "signature"
version = "1.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c"
[[package]]
name = "signature"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500"
[[package]]
name = "slab"
version = "0.4.7"
@ -2126,6 +2455,16 @@ dependencies = [
"lock_api",
]
[[package]]
name = "spki"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a"
dependencies = [
"base64ct",
"der",
]
[[package]]
name = "sql-builder"
version = "3.1.1"
@ -2196,13 +2535,13 @@ dependencies = [
"once_cell",
"paste",
"percent-encoding",
"rand",
"rand 0.8.5",
"rustls",
"rustls-pemfile",
"serde",
"serde_json",
"sha1",
"sha2",
"sha2 0.10.6",
"smallvec",
"sqlformat",
"sqlx-rt",
@ -2226,7 +2565,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"sha2",
"sha2 0.10.6",
"sqlx-core",
"sqlx-rt",
"syn 1.0.99",
@ -2262,9 +2601,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "subtle"
version = "2.4.1"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
[[package]]
name = "syn"
@ -2344,6 +2683,33 @@ dependencies = [
"winapi",
]
[[package]]
name = "time"
version = "0.3.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd"
dependencies = [
"itoa",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
[[package]]
name = "time-macros"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b"
dependencies = [
"time-core",
]
[[package]]
name = "tiny-bip39"
version = "1.0.0"
@ -2354,9 +2720,9 @@ dependencies = [
"hmac",
"once_cell",
"pbkdf2",
"rand",
"rand 0.8.5",
"rustc-hash",
"sha2",
"sha2 0.10.6",
"thiserror",
"unicode-normalization",
"wasm-bindgen",
@ -2615,9 +2981,9 @@ checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
[[package]]
name = "universal-hash"
version = "0.5.0"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d3160b73c9a19f7e2939a2fdad446c57c1bbbbf4d919d3213ff1267a580d8b5"
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
dependencies = [
"crypto-common",
"subtle",
@ -2652,7 +3018,7 @@ version = "1.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa2982af2eec27de306107c027578ff7f423d65f7250e40ce0fea8f45248b81"
dependencies = [
"getrandom",
"getrandom 0.2.7",
]
[[package]]
@ -2677,6 +3043,12 @@ dependencies = [
"try-lock",
]
[[package]]
name = "wasi"
version = "0.9.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
@ -3001,6 +3373,18 @@ dependencies = [
"winapi",
]
[[package]]
name = "x25519-dalek"
version = "2.0.0-rc.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fabd6e16dd08033932fc3265ad4510cc2eab24656058a6dcb107ffe274abcc95"
dependencies = [
"curve25519-dalek 4.0.0-rc.2",
"rand_core 0.6.4",
"serde",
"zeroize",
]
[[package]]
name = "xsalsa20poly1305"
version = "0.9.0"

View file

@ -18,7 +18,6 @@ sync = [
"reqwest",
"sha2",
"hex",
"base64",
"generic-array",
"xsalsa20poly1305",
]
@ -27,6 +26,7 @@ sync = [
atuin-common = { path = "../atuin-common", version = "15.0.0" }
log = { workspace = true }
base64 = { workspace = true }
chrono = { workspace = true }
clap = { workspace = true }
eyre = { workspace = true }
@ -52,15 +52,18 @@ lazy_static = "1"
memchr = "2.5"
rmp = { version = "0.8.11" }
typed-builder = "0.14.0"
tokio = { workspace = true }
semver = { workspace = true }
# encryption
rusty_paseto = { version = "0.5.0", default-features = false }
rusty_paserk = { version = "0.2.0", default-features = false, features = ["v4", "serde"] }
# sync
urlencoding = { version = "2.1.0", optional = true }
reqwest = { workspace = true, optional = true }
hex = { version = "0.4", optional = true }
sha2 = { version = "0.10", optional = true }
base64 = { workspace = true, optional = true }
tokio = { workspace = true }
semver = { workspace = true }
xsalsa20poly1305 = { version = "0.9.0", optional = true }
generic-array = { version = "0.14", optional = true, features = ["serde"] }

View file

@ -0,0 +1,3 @@
-- store content encryption keys in the record
alter table records
add column cek text;

View file

@ -1,5 +1,7 @@
use atuin_common::record::DecryptedData;
use eyre::{bail, ensure, eyre, Result};
use crate::record::encryption::PASETO_V4;
use crate::record::store::Store;
use crate::settings::Settings;
@ -14,7 +16,7 @@ pub struct KvRecord {
}
impl KvRecord {
pub fn serialize(&self) -> Result<Vec<u8>> {
pub fn serialize(&self) -> Result<DecryptedData> {
use rmp::encode;
let mut output = vec![];
@ -26,10 +28,10 @@ impl KvRecord {
encode::write_str(&mut output, &self.key)?;
encode::write_str(&mut output, &self.value)?;
Ok(output)
Ok(DecryptedData(output))
}
pub fn deserialize(data: &[u8], version: &str) -> Result<Self> {
pub fn deserialize(data: &DecryptedData, version: &str) -> Result<Self> {
use rmp::decode;
fn error_report<E: std::fmt::Debug>(err: E) -> eyre::Report {
@ -38,7 +40,7 @@ impl KvRecord {
match version {
KV_VERSION => {
let mut bytes = decode::Bytes::new(data);
let mut bytes = decode::Bytes::new(&data.0);
let nfields = decode::read_array_len(&mut bytes).map_err(error_report)?;
ensure!(nfields == 3, "too many entries in v0 kv record");
@ -84,6 +86,7 @@ impl KvStore {
pub async fn set(
&self,
store: &mut (impl Store + Send + Sync),
encryption_key: &[u8; 32],
namespace: &str,
key: &str,
value: &str,
@ -111,7 +114,9 @@ impl KvStore {
.data(bytes)
.build();
store.push(&record).await?;
store
.push(&record.encrypt::<PASETO_V4>(encryption_key))
.await?;
Ok(())
}
@ -121,6 +126,7 @@ impl KvStore {
pub async fn get(
&self,
store: &impl Store,
encryption_key: &[u8; 32],
namespace: &str,
key: &str,
) -> Result<Option<KvRecord>> {
@ -137,12 +143,17 @@ impl KvStore {
};
loop {
let kv = KvRecord::deserialize(&record.data, &record.version)?;
let decrypted = match record.version.as_str() {
KV_VERSION => record.decrypt::<PASETO_V4>(encryption_key)?,
version => bail!("unknown version {version:?}"),
};
let kv = KvRecord::deserialize(&decrypted.data, &decrypted.version)?;
if kv.key == key && kv.namespace == namespace {
return Ok(Some(kv));
}
if let Some(parent) = record.parent {
if let Some(parent) = decrypted.parent {
record = store.get(parent.as_str()).await?;
} else {
break;
@ -172,7 +183,7 @@ mod tests {
let encoded = kv.serialize().unwrap();
let decoded = KvRecord::deserialize(&encoded, KV_VERSION).unwrap();
assert_eq!(encoded, &snapshot);
assert_eq!(encoded.0, &snapshot);
assert_eq!(decoded, kv);
}
}

View file

@ -0,0 +1,361 @@
use atuin_common::record::{AdditionalData, DecryptedData, EncryptedData, Encryption};
use base64::{engine::general_purpose, Engine};
use eyre::{ensure, Context, Result};
use rusty_paserk::{Key, KeyId, Local, PieWrappedKey};
use rusty_paseto::core::{
ImplicitAssertion, Key as DataKey, Local as LocalPurpose, Paseto, PasetoNonce, Payload, V4,
};
use serde::{Deserialize, Serialize};
/// Use PASETO V4 Local encryption using the additional data as an implicit assertion.
#[allow(non_camel_case_types)]
pub struct PASETO_V4;
/*
Why do we use a random content-encryption key?
Originally I was planning on using a derived key for encryption based on additional data.
This would be a lot more secure than using the master key directly.
However, there's an established norm of using a random key. This scheme might be otherwise known as
- client-side encryption
- envelope encryption
- key wrapping
A HSM (Hardware Security Module) provider, eg: AWS, Azure, GCP, or even a physical device like a YubiKey
will have some keys that they keep to themselves. These keys never leave their physical hardware.
If they never leave the hardware, then encrypting large amounts of data means giving them the data and waiting.
This is not a practical solution. Instead, generate a unique key for your data, encrypt that using your HSM
and then store that with your data.
See
- <https://docs.aws.amazon.com/wellarchitected/latest/financial-services-industry-lens/use-envelope-encryption-with-customer-master-keys.html>
- <https://cloud.google.com/kms/docs/envelope-encryption>
- <https://learn.microsoft.com/en-us/azure/storage/blobs/client-side-encryption?tabs=dotnet#encryption-and-decryption-via-the-envelope-technique>
- <https://www.yubico.com/gb/product/yubihsm-2-fips/>
- <https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html#encrypting-stored-keys>
Why would we care? In the past we have recieved some requests for company solutions. If in future we can configure a
KMS service with little effort, then that would solve a lot of issues for their security team.
Even for personal use, if a user is not comfortable with sharing keys between hosts,
GCP HSM costs $1/month and $0.03 per 10,000 key operations. Assuming an active user runs
1000 atuin records a day, that would only cost them $1 and 10 cent a month.
Additionally, key rotations are much simpler using this scheme. Rotating a key is as simple as re-encrypting the CEK, and not the message contents.
This makes it very fast to rotate a key in bulk.
For future reference, with asymmetric encryption, you can encrypt the CEK without the HSM's involvement, but decrypting
will need the HSM. This allows the encryption path to still be extremely fast (no network calls) but downloads/decryption
that happens in the background can make the network calls to the HSM
*/
impl Encryption for PASETO_V4 {
fn re_encrypt(
mut data: EncryptedData,
_ad: AdditionalData,
old_key: &[u8; 32],
new_key: &[u8; 32],
) -> Result<EncryptedData> {
let cek = Self::decrypt_cek(data.content_encryption_key, old_key)?;
data.content_encryption_key = Self::encrypt_cek(cek, new_key);
Ok(data)
}
fn encrypt(data: DecryptedData, ad: AdditionalData, key: &[u8; 32]) -> EncryptedData {
// generate a random key for this entry
// aka content-encryption-key (CEK)
let random_key = Key::<V4, Local>::new_os_random();
// encode the implicit assertions
let assertions = Assertions::from(ad).encode();
// build the payload and encrypt the token
let payload = general_purpose::URL_SAFE_NO_PAD.encode(data.0);
let nonce = DataKey::<32>::try_new_random().expect("could not source from random");
let nonce = PasetoNonce::<V4, LocalPurpose>::from(&nonce);
let token = Paseto::<V4, LocalPurpose>::builder()
.set_payload(Payload::from(payload.as_str()))
.set_implicit_assertion(ImplicitAssertion::from(assertions.as_str()))
.try_encrypt(&random_key.into(), &nonce)
.expect("error encrypting atuin data");
EncryptedData {
data: token,
content_encryption_key: Self::encrypt_cek(random_key, key),
}
}
fn decrypt(data: EncryptedData, ad: AdditionalData, key: &[u8; 32]) -> Result<DecryptedData> {
let token = data.data;
let cek = Self::decrypt_cek(data.content_encryption_key, key)?;
// encode the implicit assertions
let assertions = Assertions::from(ad).encode();
// decrypt the payload with the footer and implicit assertions
let payload = Paseto::<V4, LocalPurpose>::try_decrypt(
&token,
&cek.into(),
None,
ImplicitAssertion::from(&*assertions),
)
.context("could not decrypt entry")?;
let data = general_purpose::URL_SAFE_NO_PAD.decode(payload)?;
Ok(DecryptedData(data))
}
}
impl PASETO_V4 {
fn decrypt_cek(wrapped_cek: String, key: &[u8; 32]) -> Result<Key<V4, Local>> {
let wrapping_key = Key::<V4, Local>::from_bytes(*key);
// let wrapping_key = PasetoSymmetricKey::from(Key::from(key));
let AtuinFooter { kid, wpk } = serde_json::from_str(&wrapped_cek)
.context("wrapped cek did not contain the correct contents")?;
// check that the wrapping key matches the required key to decrypt.
// In future, we could support multiple keys and use this key to
// look up the key rather than only allow one key.
// For now though we will only support the one key and key rotation will
// have to be a hard reset
let current_kid = wrapping_key.to_id();
ensure!(
current_kid == kid,
"attempting to decrypt with incorrect key. currently using {current_kid}, expecting {kid}"
);
// decrypt the random key
Ok(wpk.unwrap_key(&wrapping_key)?)
}
fn encrypt_cek(cek: Key<V4, Local>, key: &[u8; 32]) -> String {
// aka key-encryption-key (KEK)
let wrapping_key = Key::<V4, Local>::from_bytes(*key);
// wrap the random key so we can decrypt it later
let wrapped_cek = AtuinFooter {
wpk: cek.wrap_pie(&wrapping_key),
kid: wrapping_key.to_id(),
};
serde_json::to_string(&wrapped_cek).expect("could not serialize wrapped cek")
}
}
#[derive(Serialize, Deserialize)]
/// Well-known footer claims for decrypting. This is not encrypted but is stored in the record.
/// <https://github.com/paseto-standard/paseto-spec/blob/master/docs/02-Implementation-Guide/04-Claims.md#optional-footer-claims>
struct AtuinFooter {
/// Wrapped key
wpk: PieWrappedKey<V4, Local>,
/// ID of the key which was used to wrap
kid: KeyId<V4, Local>,
}
/// Used in the implicit assertions. This is not encrypted and not stored in the data blob.
// This cannot be changed, otherwise it breaks the authenticated encryption.
#[derive(Debug, Copy, Clone, Serialize)]
struct Assertions<'a> {
id: &'a str,
version: &'a str,
tag: &'a str,
host: &'a str,
}
impl<'a> From<AdditionalData<'a>> for Assertions<'a> {
fn from(ad: AdditionalData<'a>) -> Self {
Self {
id: ad.id,
version: ad.version,
tag: ad.tag,
host: ad.host,
}
}
}
impl Assertions<'_> {
fn encode(&self) -> String {
serde_json::to_string(self).expect("could not serialize implicit assertions")
}
}
#[cfg(test)]
mod tests {
use atuin_common::record::Record;
use super::*;
#[test]
fn round_trip() {
let key = Key::<V4, Local>::new_os_random();
let ad = AdditionalData {
id: "foo",
version: "v0",
tag: "kv",
host: "1234",
};
let data = DecryptedData(vec![1, 2, 3, 4]);
let encrypted = PASETO_V4::encrypt(data.clone(), ad, &key.to_bytes());
let decrypted = PASETO_V4::decrypt(encrypted, ad, &key.to_bytes()).unwrap();
assert_eq!(decrypted, data);
}
#[test]
fn same_entry_different_output() {
let key = Key::<V4, Local>::new_os_random();
let ad = AdditionalData {
id: "foo",
version: "v0",
tag: "kv",
host: "1234",
};
let data = DecryptedData(vec![1, 2, 3, 4]);
let encrypted = PASETO_V4::encrypt(data.clone(), ad, &key.to_bytes());
let encrypted2 = PASETO_V4::encrypt(data, ad, &key.to_bytes());
assert_ne!(
encrypted.data, encrypted2.data,
"re-encrypting the same contents should have different output due to key randomization"
);
}
#[test]
fn cannot_decrypt_different_key() {
let key = Key::<V4, Local>::new_os_random();
let fake_key = Key::<V4, Local>::new_os_random();
let ad = AdditionalData {
id: "foo",
version: "v0",
tag: "kv",
host: "1234",
};
let data = DecryptedData(vec![1, 2, 3, 4]);
let encrypted = PASETO_V4::encrypt(data, ad, &key.to_bytes());
let _ = PASETO_V4::decrypt(encrypted, ad, &fake_key.to_bytes()).unwrap_err();
}
#[test]
fn cannot_decrypt_different_id() {
let key = Key::<V4, Local>::new_os_random();
let ad = AdditionalData {
id: "foo",
version: "v0",
tag: "kv",
host: "1234",
};
let data = DecryptedData(vec![1, 2, 3, 4]);
let encrypted = PASETO_V4::encrypt(data, ad, &key.to_bytes());
let ad = AdditionalData {
id: "foo1",
version: "v0",
tag: "kv",
host: "1234",
};
let _ = PASETO_V4::decrypt(encrypted, ad, &key.to_bytes()).unwrap_err();
}
#[test]
fn re_encrypt_round_trip() {
let key1 = Key::<V4, Local>::new_os_random();
let key2 = Key::<V4, Local>::new_os_random();
let ad = AdditionalData {
id: "foo",
version: "v0",
tag: "kv",
host: "1234",
};
let data = DecryptedData(vec![1, 2, 3, 4]);
let encrypted1 = PASETO_V4::encrypt(data.clone(), ad, &key1.to_bytes());
let encrypted2 =
PASETO_V4::re_encrypt(encrypted1.clone(), ad, &key1.to_bytes(), &key2.to_bytes())
.unwrap();
// we only re-encrypt the content keys
assert_eq!(encrypted1.data, encrypted2.data);
assert_ne!(
encrypted1.content_encryption_key,
encrypted2.content_encryption_key
);
let decrypted = PASETO_V4::decrypt(encrypted2, ad, &key2.to_bytes()).unwrap();
assert_eq!(decrypted, data);
}
#[test]
fn full_record_round_trip() {
let key = [0x55; 32];
let record = Record::builder()
.id("1".to_owned())
.version("v0".to_owned())
.tag("kv".to_owned())
.host("host1".to_owned())
.timestamp(1687244806000000)
.data(DecryptedData(vec![1, 2, 3, 4]))
.build();
let encrypted = record.encrypt::<PASETO_V4>(&key);
assert!(!encrypted.data.data.is_empty());
assert!(!encrypted.data.content_encryption_key.is_empty());
assert_eq!(encrypted.id, "1");
assert_eq!(encrypted.host, "host1");
assert_eq!(encrypted.version, "v0");
assert_eq!(encrypted.tag, "kv");
assert_eq!(encrypted.timestamp, 1687244806000000);
let decrypted = encrypted.decrypt::<PASETO_V4>(&key).unwrap();
assert_eq!(decrypted.data.0, [1, 2, 3, 4]);
assert_eq!(decrypted.id, "1");
assert_eq!(decrypted.host, "host1");
assert_eq!(decrypted.version, "v0");
assert_eq!(decrypted.tag, "kv");
assert_eq!(decrypted.timestamp, 1687244806000000);
}
#[test]
fn full_record_round_trip_fail() {
let key = [0x55; 32];
let record = Record::builder()
.id("1".to_owned())
.version("v0".to_owned())
.tag("kv".to_owned())
.host("host1".to_owned())
.timestamp(1687244806000000)
.data(DecryptedData(vec![1, 2, 3, 4]))
.build();
let encrypted = record.encrypt::<PASETO_V4>(&key);
let mut enc1 = encrypted.clone();
enc1.host = "host2".to_owned();
let _ = enc1
.decrypt::<PASETO_V4>(&key)
.expect_err("tampering with the host should result in auth failure");
let mut enc2 = encrypted;
enc2.id = "2".to_owned();
let _ = enc2
.decrypt::<PASETO_V4>(&key)
.expect_err("tampering with the id should result in auth failure");
}
}

View file

@ -1,2 +1,3 @@
pub mod encryption;
pub mod sqlite_store;
pub mod store;

View file

@ -13,7 +13,7 @@ use sqlx::{
Row,
};
use atuin_common::record::Record;
use atuin_common::record::{EncryptedData, Record};
use super::store::Store;
@ -53,11 +53,14 @@ impl SqliteStore {
Ok(())
}
async fn save_raw(tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>, r: &Record) -> Result<()> {
async fn save_raw(
tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
r: &Record<EncryptedData>,
) -> Result<()> {
// In sqlite, we are "limited" to i64. But that is still fine, until 2262.
sqlx::query(
"insert or ignore into records(id, host, tag, timestamp, parent, version, data)
values(?1, ?2, ?3, ?4, ?5, ?6, ?7)",
"insert or ignore into records(id, host, tag, timestamp, parent, version, data, cek)
values(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
)
.bind(r.id.as_str())
.bind(r.host.as_str())
@ -65,14 +68,15 @@ impl SqliteStore {
.bind(r.timestamp as i64)
.bind(r.parent.as_ref())
.bind(r.version.as_str())
.bind(r.data.as_slice())
.bind(r.data.data.as_str())
.bind(r.data.content_encryption_key.as_str())
.execute(tx)
.await?;
Ok(())
}
fn query_row(row: SqliteRow) -> Record {
fn query_row(row: SqliteRow) -> Record<EncryptedData> {
let timestamp: i64 = row.get("timestamp");
Record {
@ -82,14 +86,20 @@ impl SqliteStore {
timestamp: timestamp as u64,
tag: row.get("tag"),
version: row.get("version"),
data: row.get("data"),
data: EncryptedData {
data: row.get("data"),
content_encryption_key: row.get("cek"),
},
}
}
}
#[async_trait]
impl Store for SqliteStore {
async fn push_batch(&self, records: impl Iterator<Item = &Record> + Send + Sync) -> Result<()> {
async fn push_batch(
&self,
records: impl Iterator<Item = &Record<EncryptedData>> + Send + Sync,
) -> Result<()> {
let mut tx = self.pool.begin().await?;
for record in records {
@ -101,7 +111,7 @@ impl Store for SqliteStore {
Ok(())
}
async fn get(&self, id: &str) -> Result<Record> {
async fn get(&self, id: &str) -> Result<Record<EncryptedData>> {
let res = sqlx::query("select * from records where id = ?1")
.bind(id)
.map(Self::query_row)
@ -122,7 +132,7 @@ impl Store for SqliteStore {
Ok(res.0 as u64)
}
async fn next(&self, record: &Record) -> Result<Option<Record>> {
async fn next(&self, record: &Record<EncryptedData>) -> Result<Option<Record<EncryptedData>>> {
let res = sqlx::query("select * from records where parent = ?1")
.bind(record.id.clone())
.map(Self::query_row)
@ -136,7 +146,7 @@ impl Store for SqliteStore {
}
}
async fn first(&self, host: &str, tag: &str) -> Result<Option<Record>> {
async fn first(&self, host: &str, tag: &str) -> Result<Option<Record<EncryptedData>>> {
let res = sqlx::query(
"select * from records where host = ?1 and tag = ?2 and parent is null limit 1",
)
@ -149,7 +159,7 @@ impl Store for SqliteStore {
Ok(res)
}
async fn last(&self, host: &str, tag: &str) -> Result<Option<Record>> {
async fn last(&self, host: &str, tag: &str) -> Result<Option<Record<EncryptedData>>> {
let res = sqlx::query(
"select * from records rp where tag=?1 and host=?2 and (select count(1) from records where parent=rp.id) = 0;",
)
@ -165,18 +175,21 @@ impl Store for SqliteStore {
#[cfg(test)]
mod tests {
use atuin_common::record::Record;
use atuin_common::record::{EncryptedData, Record};
use crate::record::store::Store;
use crate::record::{encryption::PASETO_V4, store::Store};
use super::SqliteStore;
fn test_record() -> Record {
fn test_record() -> Record<EncryptedData> {
Record::builder()
.host(atuin_common::utils::uuid_v7().simple().to_string())
.version("v1".into())
.tag(atuin_common::utils::uuid_v7().simple().to_string())
.data(vec![0, 1, 2, 3])
.data(EncryptedData {
data: "1234".into(),
content_encryption_key: "1234".into(),
})
.build()
}
@ -261,7 +274,9 @@ mod tests {
db.push(&tail).await.expect("failed to push record");
for _ in 1..100 {
tail = tail.new_child(vec![1, 2, 3, 4]);
tail = tail
.new_child(vec![1, 2, 3, 4])
.encrypt::<PASETO_V4>(&[0; 32]);
db.push(&tail).await.unwrap();
}
@ -276,13 +291,13 @@ mod tests {
async fn append_a_big_bunch() {
let db = SqliteStore::new(":memory:").await.unwrap();
let mut records: Vec<Record> = Vec::with_capacity(10000);
let mut records: Vec<Record<EncryptedData>> = Vec::with_capacity(10000);
let mut tail = test_record();
records.push(tail.clone());
for _ in 1..10000 {
tail = tail.new_child(vec![1, 2, 3]);
tail = tail.new_child(vec![1, 2, 3]).encrypt::<PASETO_V4>(&[0; 32]);
records.push(tail.clone());
}
@ -299,13 +314,13 @@ mod tests {
async fn test_chain() {
let db = SqliteStore::new(":memory:").await.unwrap();
let mut records: Vec<Record> = Vec::with_capacity(1000);
let mut records: Vec<Record<EncryptedData>> = Vec::with_capacity(1000);
let mut tail = test_record();
records.push(tail.clone());
for _ in 1..1000 {
tail = tail.new_child(vec![1, 2, 3]);
tail = tail.new_child(vec![1, 2, 3]).encrypt::<PASETO_V4>(&[0; 32]);
records.push(tail.clone());
}

View file

@ -1,7 +1,7 @@
use async_trait::async_trait;
use eyre::Result;
use atuin_common::record::Record;
use atuin_common::record::{EncryptedData, Record};
/// A record store stores records
/// In more detail - we tend to need to process this into _another_ format to actually query it.
@ -10,21 +10,24 @@ use atuin_common::record::Record;
#[async_trait]
pub trait Store {
// Push a record
async fn push(&self, record: &Record) -> Result<()> {
async fn push(&self, record: &Record<EncryptedData>) -> Result<()> {
self.push_batch(std::iter::once(record)).await
}
// Push a batch of records, all in one transaction
async fn push_batch(&self, records: impl Iterator<Item = &Record> + Send + Sync) -> Result<()>;
async fn push_batch(
&self,
records: impl Iterator<Item = &Record<EncryptedData>> + Send + Sync,
) -> Result<()>;
async fn get(&self, id: &str) -> Result<Record>;
async fn get(&self, id: &str) -> Result<Record<EncryptedData>>;
async fn len(&self, host: &str, tag: &str) -> Result<u64>;
/// Get the record that follows this record
async fn next(&self, record: &Record) -> Result<Option<Record>>;
async fn next(&self, record: &Record<EncryptedData>) -> Result<Option<Record<EncryptedData>>>;
/// Get the first record for a given host and tag
async fn first(&self, host: &str, tag: &str) -> Result<Option<Record>>;
async fn first(&self, host: &str, tag: &str) -> Result<Option<Record<EncryptedData>>>;
/// Get the last record for a given host and tag
async fn last(&self, host: &str, tag: &str) -> Result<Option<Record>>;
async fn last(&self, host: &str, tag: &str) -> Result<Option<Record<EncryptedData>>>;
}

View file

@ -17,4 +17,7 @@ serde = { workspace = true }
uuid = { workspace = true }
rand = { workspace = true }
typed-builder = { workspace = true }
eyre = { workspace = true }
[dev-dependencies]
pretty_assertions = "1.3.0"

View file

@ -1,11 +1,21 @@
use std::collections::HashMap;
use eyre::Result;
use serde::{Deserialize, Serialize};
use typed_builder::TypedBuilder;
#[derive(Clone, Debug, PartialEq)]
pub struct DecryptedData(pub Vec<u8>);
#[derive(Debug, Clone, PartialEq)]
pub struct EncryptedData {
pub data: String,
pub content_encryption_key: String,
}
/// A single record stored inside of our local database
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TypedBuilder)]
pub struct Record {
pub struct Record<Data> {
/// a unique ID
#[builder(default = crate::utils::uuid_v7().as_simple().to_string())]
pub id: String,
@ -35,17 +45,26 @@ pub struct Record {
pub tag: String,
/// Some data. This can be anything you wish to store. Use the tag field to know how to handle it.
pub data: Vec<u8>,
pub data: Data,
}
impl Record {
pub fn new_child(&self, data: Vec<u8>) -> Record {
/// Extra data from the record that should be encoded in the data
#[derive(Debug, Copy, Clone)]
pub struct AdditionalData<'a> {
pub id: &'a str,
pub version: &'a str,
pub tag: &'a str,
pub host: &'a str,
}
impl<Data> Record<Data> {
pub fn new_child(&self, data: Vec<u8>) -> Record<DecryptedData> {
Record::builder()
.host(self.host.clone())
.version(self.version.clone())
.parent(Some(self.id.clone()))
.tag(self.tag.clone())
.data(data)
.data(DecryptedData(data))
.build()
}
}
@ -71,7 +90,7 @@ impl RecordIndex {
}
/// Insert a new tail record into the store
pub fn set(&mut self, tail: Record) {
pub fn set(&mut self, tail: Record<DecryptedData>) {
self.hosts
.entry(tail.host)
.or_default()
@ -128,17 +147,93 @@ impl RecordIndex {
}
}
pub trait Encryption {
fn re_encrypt(
data: EncryptedData,
ad: AdditionalData,
old_key: &[u8; 32],
new_key: &[u8; 32],
) -> Result<EncryptedData> {
let data = Self::decrypt(data, ad, old_key)?;
Ok(Self::encrypt(data, ad, new_key))
}
fn encrypt(data: DecryptedData, ad: AdditionalData, key: &[u8; 32]) -> EncryptedData;
fn decrypt(data: EncryptedData, ad: AdditionalData, key: &[u8; 32]) -> Result<DecryptedData>;
}
impl Record<DecryptedData> {
pub fn encrypt<E: Encryption>(self, key: &[u8; 32]) -> Record<EncryptedData> {
let ad = AdditionalData {
id: &self.id,
version: &self.version,
tag: &self.tag,
host: &self.host,
};
Record {
data: E::encrypt(self.data, ad, key),
id: self.id,
host: self.host,
parent: self.parent,
timestamp: self.timestamp,
version: self.version,
tag: self.tag,
}
}
}
impl Record<EncryptedData> {
pub fn decrypt<E: Encryption>(self, key: &[u8; 32]) -> Result<Record<DecryptedData>> {
let ad = AdditionalData {
id: &self.id,
version: &self.version,
tag: &self.tag,
host: &self.host,
};
Ok(Record {
data: E::decrypt(self.data, ad, key)?,
id: self.id,
host: self.host,
parent: self.parent,
timestamp: self.timestamp,
version: self.version,
tag: self.tag,
})
}
pub fn re_encrypt<E: Encryption>(
self,
old_key: &[u8; 32],
new_key: &[u8; 32],
) -> Result<Record<EncryptedData>> {
let ad = AdditionalData {
id: &self.id,
version: &self.version,
tag: &self.tag,
host: &self.host,
};
Ok(Record {
data: E::re_encrypt(self.data, ad, old_key, new_key)?,
id: self.id,
host: self.host,
parent: self.parent,
timestamp: self.timestamp,
version: self.version,
tag: self.tag,
})
}
}
#[cfg(test)]
mod tests {
use super::{Record, RecordIndex};
use pretty_assertions::{assert_eq, assert_ne};
use super::{DecryptedData, Record, RecordIndex};
use pretty_assertions::assert_eq;
fn test_record() -> Record {
fn test_record() -> Record<DecryptedData> {
Record::builder()
.host(crate::utils::uuid_v7().simple().to_string())
.version("v1".into())
.tag(crate::utils::uuid_v7().simple().to_string())
.data(vec![0, 1, 2, 3])
.data(DecryptedData(vec![0, 1, 2, 3]))
.build()
}

View file

@ -1,7 +1,7 @@
use clap::Subcommand;
use eyre::Result;
use eyre::{Context, Result};
use atuin_client::{kv::KvStore, record::store::Store, settings::Settings};
use atuin_client::{encryption, kv::KvStore, record::store::Store, settings::Settings};
#[derive(Subcommand)]
#[command(infer_subcommands = true)]
@ -29,20 +29,28 @@ pub enum Cmd {
impl Cmd {
pub async fn run(
&self,
_settings: &Settings,
settings: &Settings,
store: &mut (impl Store + Send + Sync),
) -> Result<()> {
let kv_store = KvStore::new();
let encryption_key: [u8; 32] = encryption::load_key(settings)
.context("could not load encryption key")?
.into();
match self {
Self::Set {
key,
value,
namespace,
} => kv_store.set(store, namespace, key, value).await,
} => {
kv_store
.set(store, &encryption_key, namespace, key, value)
.await
}
Self::Get { key, namespace } => {
let val = kv_store.get(store, namespace, key).await?;
let val = kv_store.get(store, &encryption_key, namespace, key).await?;
if let Some(kv) = val {
println!("{}", kv.value);