From 90714bb25ce7a4b7d2bc32158e64db4bd4bc2da5 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Fri, 16 Oct 2026 01:01:26 +1100 Subject: [PATCH 01/15] feat(stack-encrypt)!: seal through vitaminc ADR 0001 so a value is described once and every leaf records its type Pins the root workspace's vitaminc crates to the branch that implements vitaminc ADR 0001 (cipherstash/vitaminc #439 to #445) with a temporary `[patch.crates-io]`, and moves stack-encrypt and the guest ABI onto it. - A value describes itself with vitaminc's `Plaintext` and reads back through `Decrypt`. `PendingStackCipherText::plan` walks it into vitaminc's pending tree with no I/O; the target layer's `Pending` still fetches every key of an assembly in one batched call, and the leaves are then sealed and opened through the bytes-only `Cipher`: `MintedKeys` seals each leaf under the next generated key, `BoundKeys` opens a leaf under the key bound to it, and vitaminc derives every leaf's AAD and checks the markers and container rules on both sides. - Every leaf is tagged (wire version 2), so a Rust `u32` field and a data plan's `uint32` field seal the same leaf and each opens the other's. The record fixture is regenerated; its terms are unchanged. - `None` is a null leaf, a passthrough is a `PassthroughFrame`, and `StackCipherText` is `Node`. - `dynamic::FfiValue` and `ValueKind` move into this crate, unchanged in shape, with `Plaintext` and a `Decrypt` sink, and cross a binding's boundary as transport frames. The deeper rework of `dynamic::Value` and `Scalar` is #1140. - stack-guest-abi's outputs and last error are a `HostValue` written as a transport frame. Closes #1118 --- Cargo.lock | 102 +- Cargo.toml | 22 +- packages/stack-encrypt-derive/src/lib.rs | 6 +- packages/stack-encrypt/CHANGELOG.md | 31 + packages/stack-encrypt/CONTEXT.md | 5 +- packages/stack-encrypt/Cargo.toml | 6 +- packages/stack-encrypt/examples/mixed_user.rs | 107 +- packages/stack-encrypt/src/cipher.rs | 990 ++++-------------- packages/stack-encrypt/src/dynamic/context.rs | 4 +- .../stack-encrypt/src/dynamic/ffi_value.rs | 675 ++++++++++++ packages/stack-encrypt/src/dynamic/kind.rs | 10 +- packages/stack-encrypt/src/dynamic/mod.rs | 33 +- packages/stack-encrypt/src/dynamic/record.rs | 168 ++- packages/stack-encrypt/src/dynamic/target.rs | 2 +- packages/stack-encrypt/src/dynamic/term.rs | 2 +- packages/stack-encrypt/src/dynamic/value.rs | 52 +- packages/stack-encrypt/src/lib.rs | 43 +- packages/stack-encrypt/src/plan/build.rs | 8 +- packages/stack-encrypt/src/plan/chain.rs | 8 +- packages/stack-encrypt/src/plan/mod.rs | 13 +- packages/stack-encrypt/src/plan/value.rs | 8 +- packages/stack-encrypt/src/target/core.rs | 47 +- packages/stack-encrypt/src/target/index.rs | 6 +- packages/stack-encrypt/src/target/mod.rs | 2 +- .../stack-encrypt/src/target/operations.rs | 8 +- .../stack-encrypt/src/target/transcode.rs | 21 +- packages/stack-encrypt/tests/common/mod.rs | 17 + .../stack-encrypt/tests/fixtures/README.md | 7 +- .../tests/fixtures/record_lowering.json | 16 +- packages/stack-encrypt/tests/index.rs | 5 +- packages/stack-encrypt/tests/plan_grammar.rs | 4 +- .../stack-encrypt/tests/record_lowering.rs | 46 +- packages/stack-encrypt/tests/roundtrip.rs | 169 +-- packages/stack-encrypt/tests/source_mode.rs | 40 +- packages/stack-encrypt/tests/transcode.rs | 45 +- .../ui/decrypt_field_not_decryptable.stderr | 4 +- .../stack-encrypt/tests/ui/drop_record.stderr | 2 +- .../tests/ui/owned_fan_out_needs_clone.rs | 4 +- .../tests/ui/owned_fan_out_needs_clone.stderr | 4 +- .../tests/ui/pass/owned_without_clone.rs | 4 +- .../tests/ui/pass/query_only_and_borrowed.rs | 2 +- .../tests/ui/plaintext_needs_encrypt.stderr | 24 +- .../tests/ui/struct_field_must_open.rs | 24 +- .../tests/ui/struct_field_must_open.stderr | 49 +- packages/stack-guest-abi/Cargo.toml | 18 +- packages/stack-guest-abi/src/abi.rs | 2 +- packages/stack-guest-abi/src/call.rs | 14 +- packages/stack-guest-abi/src/last_error.rs | 90 +- packages/stack-guest-abi/src/lib.rs | 7 +- packages/stack-guest-abi/src/value.rs | 350 +++++++ 50 files changed, 1821 insertions(+), 1505 deletions(-) create mode 100644 packages/stack-encrypt/src/dynamic/ffi_value.rs create mode 100644 packages/stack-guest-abi/src/value.rs diff --git a/Cargo.lock b/Cargo.lock index 5c8405d09..123d5c85a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2336,6 +2336,15 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.17+spec-1.1.0", +] + [[package]] name = "proc-macro-error-attr2" version = "2.0.0" @@ -3290,7 +3299,7 @@ dependencies = [ "trybuild", "uuid", "vitaminc-aead", - "vitaminc-aead-value", + "vitaminc-aead-frame", "vitaminc-encrypt", "vitaminc-hmac", "vitaminc-prf", @@ -3319,7 +3328,8 @@ dependencies = [ "proptest", "serde_json", "thiserror 1.0.69", - "vitaminc-aead-value", + "vitaminc-aead", + "vitaminc-aead-frame", "vitaminc-protected", "zeroize", ] @@ -3688,7 +3698,7 @@ dependencies = [ "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.11", - "toml_edit", + "toml_edit 0.22.27", ] [[package]] @@ -3703,7 +3713,7 @@ dependencies = [ "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", - "winnow", + "winnow 0.7.14", ] [[package]] @@ -3724,6 +3734,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_datetime" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.22.27" @@ -3735,16 +3754,28 @@ dependencies = [ "serde_spanned 0.6.9", "toml_datetime 0.6.11", "toml_write", - "winnow", + "winnow 0.7.14", +] + +[[package]] +name = "toml_edit" +version = "0.25.17+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c" +dependencies = [ + "indexmap", + "toml_datetime 1.1.2+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", ] [[package]] name = "toml_parser" -version = "1.0.6+spec-1.1.0" +version = "1.1.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" dependencies = [ - "winnow", + "winnow 1.0.4", ] [[package]] @@ -4078,8 +4109,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vitaminc" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35042fb7a81d72dc43c53fec835e61027f18b4d01cc0f7e12d6c6a6ffbf3bfb9" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "vitaminc-aead", "vitaminc-context", @@ -4092,11 +4122,12 @@ dependencies = [ [[package]] name = "vitaminc-aead" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62cacd4dae485e98cfe5407e919187a8468dd933ea4234be5115b7d13a30f3c6" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "bytes", + "futures-util", "serde", + "thiserror 2.0.18", "vitaminc-aead-derive", "vitaminc-context", "vitaminc-protected", @@ -4107,8 +4138,7 @@ dependencies = [ [[package]] name = "vitaminc-aead-derive" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ff34056b70a8417ad3bf9db73b2bd3be97f448794566c6de2080bd8a7cd9b6b" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "proc-macro2", "quote", @@ -4116,21 +4146,17 @@ dependencies = [ ] [[package]] -name = "vitaminc-aead-value" +name = "vitaminc-aead-frame" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d78cdadbc6374c3bd7c19fb4d99abca693b96953b18fef8c608efbe092cf3ea" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "vitaminc-aead", - "vitaminc-protected", - "zeroize", ] [[package]] name = "vitaminc-context" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7df52501fe33cdb3ee9e155b5242fac184c037d147e58c96e5143ed011252a8" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "mutants", "vitaminc-protected", @@ -4139,8 +4165,7 @@ dependencies = [ [[package]] name = "vitaminc-encrypt" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbd0e1748055e381e02033e08af385de879c4dcf32e0f6739ac0c0cf0c6f13d" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "aes-gcm", "aws-lc-rs", @@ -4153,8 +4178,7 @@ dependencies = [ [[package]] name = "vitaminc-hmac" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15fe558a5e92d4f2b3f811c5b67d863d9255da5f66d08a71d4f0dd3d0011c563" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "hmac", "sha2 0.11.0", @@ -4166,8 +4190,7 @@ dependencies = [ [[package]] name = "vitaminc-prf" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712d31d46d76b38c6b62deb35f516fc5636a35f90f39c0213d0ca9a052b4400b" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "mutants", "thiserror 2.0.18", @@ -4178,8 +4201,7 @@ dependencies = [ [[package]] name = "vitaminc-protected" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7b514f605a63f88874ba46ffea81775fad64994475ab239cb19da159e884b7b" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "bitvec", "digest 0.11.3", @@ -4195,9 +4217,9 @@ dependencies = [ [[package]] name = "vitaminc-protected-derive" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cb0a3d11afbe2c909f0cfc9996fe37fd89b1196eb0b8d89a66e28716360ee09" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ + "proc-macro-crate", "proc-macro2", "quote", "syn 3.0.3", @@ -4206,8 +4228,7 @@ dependencies = [ [[package]] name = "vitaminc-random" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7b1bfab35ee322de37269c6fd94d8cd94047a66f97cf6722e53e3321482167" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "chacha20", "getrandom 0.4.2", @@ -4221,8 +4242,7 @@ dependencies = [ [[package]] name = "vitaminc-random-derives" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0915e2bfb417ce11e23123fb7ec122af5d27d8cfd22d6ad6a2cf7a1c2dbb4712" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "proc-macro2", "quote", @@ -4232,8 +4252,7 @@ dependencies = [ [[package]] name = "vitaminc-traits" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b31ec300e351a7a59e64899d0a2d7e7227cfa74ac0514f4e745acb2cc9335cee" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "anyhow", "bytes", @@ -4873,6 +4892,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.50.0" diff --git a/Cargo.toml b/Cargo.toml index 3da394ab4..30bb41e57 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -122,11 +122,25 @@ zeroize = { version = "1.8.1", features = ["derive"] } temp-env = { git = "https://github.com/cipherstash/temp-env", branch = "main" } vitaminc = { version = "0.5.1", features = ["random", "protected", "encrypt"] } vitaminc-aead = "0.5.1" -# The dynamic value model (`FfiValue`) and its FFI transport codec, shared by -# every language binding. Optional in stack-encrypt (the `dynamic` feature). -# 0.5.1 adds `ValueKind`, the field-type vocabulary a dynamic plan declares. -vitaminc-aead-value = "0.5.1" +# The transport frame and ciphertext codec every language binding carries +# across its FFI boundary. Optional in stack-encrypt (the `dynamic` feature). +vitaminc-aead-frame = "0.5.1" vitaminc-encrypt = "0.5.1" vitaminc-hmac = "0.5.1" vitaminc-prf = "0.5.1" vitaminc-protected = "0.5.1" + +# Temporary: every vitaminc crate this workspace uses comes from the branch +# that implements vitaminc ADR 0001 (cipherstash/vitaminc PRs #439 to #445, +# top branch `feat/aead-remove-legacy-traits`) until vitaminc releases it. +# Replace this block with the released versions then. The Go guests are +# detached workspaces and carry the same block, so every build of these +# crates sees one vitaminc. +[patch.crates-io] +vitaminc = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } +vitaminc-aead = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } +vitaminc-aead-frame = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } +vitaminc-encrypt = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } +vitaminc-hmac = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } +vitaminc-prf = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } +vitaminc-protected = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } diff --git a/packages/stack-encrypt-derive/src/lib.rs b/packages/stack-encrypt-derive/src/lib.rs index 99018bf47..05170d5f1 100644 --- a/packages/stack-encrypt-derive/src/lib.rs +++ b/packages/stack-encrypt-derive/src/lib.rs @@ -108,8 +108,8 @@ //! use stack_encrypt::target::transcode::{Transcode, Visitor}; //! use stack_encrypt::target::{self, CallerContext}; //! use stack_encrypt::{ -//! CipherText, Decryptable, Encrypt, EncryptFrom, Encryption, Error, SealedValue, StackCipher, -//! nonempty, +//! CipherText, Decryptable, EncryptFrom, Encryption, Error, Plaintext, SealedValue, +//! StackCipher, nonempty, //! }; //! use stack_encrypt::kms::FakeDataKeySource; //! @@ -138,7 +138,7 @@ //! } //! // The declaration: the canonical ciphertext operation, read into this type. //! // It seals under the AEAD half of the context the record threads to it. -//! impl EncryptFrom for LeafBytes { +//! impl EncryptFrom for LeafBytes { //! type Context = CallerContext; //! fn encryption<'s, K: 'static>() -> Encryption<'s, S, Self, K, Self::Context> //! where diff --git a/packages/stack-encrypt/CHANGELOG.md b/packages/stack-encrypt/CHANGELOG.md index df56f71ed..218c15b49 100644 --- a/packages/stack-encrypt/CHANGELOG.md +++ b/packages/stack-encrypt/CHANGELOG.md @@ -142,6 +142,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 same way. Storing the empty filter tells a reader of the database that the value is empty, separator-only or shorter than the n-gram length (ADR-0009). +- **Values describe themselves with vitaminc's `Plaintext` and read back + with `Decrypt` (vitaminc ADR 0001).** `KeysetCipher::encrypt`, + `StackCipher::decrypt` and every target take `Plaintext` where they took + `Encrypt`, and `Decrypt` where they took `Decrypt<'static>`; derive both + with `vitaminc_aead::{Plaintext, Decrypt}`. `Encrypt`, `Decipher`, + `Element` and `Cipher` are no longer re-exported, and `StackDecipher` and + `BoxedPassthrough` are gone. A pending tree is planned rather than driven: + `PendingStackCipherText::plan(value, aad)`, then `seal` or + `into_pending` as before. The key fetch is unchanged: one batched + `generate_keys` per assembly to seal, one `retrieve_keys` per keyset to + open. +- **Every leaf records its type, so a Rust field and a data-plan field + share leaves.** A Rust `u32` and a data plan's `uint32` field seal the + same leaf under the same context, and each opens the other's; a reader of + another type is refused rather than handed changed plaintext, as a bare + `String` reader used to read a binding's leaf as `"\nalice"` (#1118). + Leaves are vitaminc's wire version 2, so a ciphertext sealed by an + earlier build does not open. +- **An absent value is a sealed null leaf**, a `Single` like any other + value: the absent marker (`CipherText::None`) and + `transcode::Visitor::absent` are gone. +- **A passthrough travels as its frame.** `StackCipherText` is vitaminc's + `Node`: a passthrough subtree is a `PassthroughFrame`, the + transport frame of its value, in place of a boxed value, and + `transcode::Visitor::passthrough` takes one. +- **`dynamic::FfiValue` and `dynamic::ValueKind` are this crate's own**, + with the same variants and kind names, since vitaminc no longer has a + value tree. A value crosses a binding's boundary as a transport frame + (`FfiValue::to_frame`, `FfiValue::from_frame`), and is read through its + `Decrypt` sink: an 8- or 16-bit integer in a frame reads as the 32-bit + variant of its sign, and a scalar no variant holds is refused. - **`StackCipher::query_index` is gone**, with `QueryIndexBuilder` and `QueryIndex`. Every query names an operator now that the orderings have diff --git a/packages/stack-encrypt/CONTEXT.md b/packages/stack-encrypt/CONTEXT.md index c9a9b678d..c59e3e5b2 100644 --- a/packages/stack-encrypt/CONTEXT.md +++ b/packages/stack-encrypt/CONTEXT.md @@ -8,8 +8,9 @@ derivation of searchable index terms from the same values. Covers ## Language **Cipher-directed**: -Encryption driven by the value's shape: the value's `Encrypt` implementation -walks the cipher and the caller decides the context, which may be absent. +Encryption driven by the value's shape: the value describes itself once +(vitaminc's `Plaintext`), vitaminc walks it into sealed leaves, and the +caller decides the context, which may be absent. Nothing declares an index and no output type is involved. One of the two preferred entry points for a native Rust caller. A language SDK may leave it out: the Go SDK seals a whole value through a plan, so that every sealed diff --git a/packages/stack-encrypt/Cargo.toml b/packages/stack-encrypt/Cargo.toml index d62c87f47..0be8a4526 100644 --- a/packages/stack-encrypt/Cargo.toml +++ b/packages/stack-encrypt/Cargo.toml @@ -31,7 +31,7 @@ stack-encrypt-derive = { path = "../stack-encrypt-derive", version = "0.2.0" } # version or the aead crate is duplicated. vitaminc-aead = { workspace = true } # `dynamic`: the runtime value model every language binding funnels through. -vitaminc-aead-value = { workspace = true, optional = true } +vitaminc-aead-frame = { workspace = true, optional = true } vitaminc-encrypt = { workspace = true } vitaminc-hmac = { workspace = true } vitaminc-prf = { workspace = true } @@ -60,8 +60,8 @@ default = ["http"] http = ["dep:stack-auth", "stack-auth/http", "stack-kms/http"] # `stack_encrypt::dynamic`: encrypt and decrypt values whose types are known # only at runtime, as an FFI binding's are. Off by default — it is only of -# use to a binding author, and it pulls `vitaminc-aead-value` in. -dynamic = ["dep:vitaminc-aead-value"] +# use to a binding author, and it pulls `vitaminc-aead-frame` in. +dynamic = ["dep:vitaminc-aead-frame"] # `stack_encrypt::kms::FakeDataKeySource`: the in-memory key source for tests # with no ZeroKMS, so a caller's dev-dependency on stack-encrypt is enough and # no direct stack-kms dependency has to agree with this crate's. diff --git a/packages/stack-encrypt/examples/mixed_user.rs b/packages/stack-encrypt/examples/mixed_user.rs index bb64bd84b..f7cd18004 100644 --- a/packages/stack-encrypt/examples/mixed_user.rs +++ b/packages/stack-encrypt/examples/mixed_user.rs @@ -9,17 +9,17 @@ //! derivation chain. //! //! Element *positions* are not. Every element of a sequence is sealed under -//! the same derived AAD — deliberately, since that is what lets a single row -//! of a batch decrypt on its own as `Element` — so reordering the elements -//! of a stored sequence still verifies. Order and length are the caller's +//! the same derived AAD, so reordering the elements of a stored sequence +//! still verifies. Order and length are the caller's //! obligation; if they matter, bind them into the AAD yourself or store the //! index alongside the row. //! //! Passthrough values travel in the clear and are **not authenticated** — //! use them for non-sensitive routing/display data only. //! -//! This is the *cipher-directed* layer — vitaminc's `Encrypt` / `Decrypt` -//! driven by hand — one level below `#[derive(EncryptFrom)]`, which the +//! This is the *cipher-directed* layer — vitaminc's `Plaintext` / `Decrypt` +//! derives, sealed through the keyset directly — one level below +//! `#[derive(EncryptFrom)]`, which the //! `encrypted_record` example uses. Reach for this layer when a value needs //! what the derive does not express: fields stored in the clear beside //! sealed ones, or a single ciphertext whose internal shape (this @@ -37,84 +37,23 @@ //! `CS_WORKSPACE_CRN` and `CS_CLIENT_ID` / `CS_CLIENT_KEY` instead (see the //! `zerokms_auth` example for the lookup order). -use stack_encrypt::{ - Cipher, CipherText, Decipher, Decrypt, Encrypt, IntoAad, Label, StackCipher, StackCipherText, - Unspecified, -}; -use vitaminc_aead::{DecipherVisitor, MapAccess, MapCipher, Passthrough}; +use stack_encrypt::{CipherText, Label, StackCipher, StackCipherText}; +use vitaminc_aead::{Decrypt, Plaintext}; // --- The record type --------------------------------------------------------- -#[derive(Debug, PartialEq)] +// The derives walk the struct as a map keyed by field name. Keys travel in +// the clear; each *encrypted* value is sealed against an AAD derived from the +// map's AAD + its key, so entries cannot be renamed or swapped. Passthrough +// entries carry no such binding. +#[derive(Debug, PartialEq, Plaintext, Decrypt)] struct User { - id: u32, // passthrough: visible in the stored ciphertext - display_name: String, // passthrough - email: String, // encrypted - age: u32, // encrypted -} - -impl Encrypt for User { - fn encrypt_with_aad<'a, C, A>(self, cipher: C, aad: A) -> Result - where - C: Cipher, - A: IntoAad<'a>, - { - // Keys travel in the clear; each *encrypted* value is sealed against - // an AAD derived from the map's AAD + its key, so entries cannot be - // renamed or swapped. Passthrough entries carry no such binding. - cipher - .encrypt_map(aad) - .encrypt_entry("id", Passthrough(self.id))? - .encrypt_entry("display_name", Passthrough(self.display_name))? - .encrypt_entry("email", self.email)? - .encrypt_entry("age", self.age)? - .end() - } -} - -impl<'c> Decrypt<'c> for User { - fn decrypt_with_aad<'a, D, A>(decipher: D, aad: A) -> D::Ok - where - D: Decipher<'c>, - A: IntoAad<'a>, - { - struct UserVisitor; - impl<'c> DecipherVisitor<'c> for UserVisitor { - type Value = User; - - fn visit_map>(self, mut map: M) -> Result { - // Entries arrive in encryption order; each is pulled with its - // expected type and its key is checked. - fn entry<'c, M: MapAccess<'c>, T: Decrypt<'c> + 'c>( - map: &mut M, - key: &str, - ) -> Result { - let (k, value) = map - .next_entry::() - .map_err(|_| Unspecified)? - .ok_or(Unspecified)?; - if k == key { - Ok(value) - } else { - Err(Unspecified) - } - } - - let Passthrough(id) = entry::<_, Passthrough>(&mut map, "id")?; - let Passthrough(display_name) = - entry::<_, Passthrough>(&mut map, "display_name")?; - let email: String = entry(&mut map, "email")?; - let age: u32 = entry(&mut map, "age")?; - Ok(User { - id, - display_name, - email, - age, - }) - } - } - decipher.decrypt_map(UserVisitor, aad) - } + #[aead(passthrough)] + id: u32, // visible in the stored ciphertext + #[aead(passthrough)] + display_name: String, + email: String, // encrypted + age: u32, // encrypted } // --- Inspect what a server would see ----------------------------------------- @@ -131,11 +70,13 @@ fn describe(ciphertext: &StackCipherText, indent: usize) { CipherText::Map(entries) => { for (key, value) in entries { match value { - CipherText::Passthrough(boxed) => { - // Passthrough values are readable without any key. - if let Some(v) = boxed.downcast_ref::() { + CipherText::Passthrough(frame) => { + // Passthrough values are readable without any key: + // the frame of the value, in the clear. + let Ok(frame) = frame.parse() else { continue }; + if let Ok(v) = frame.visit(u32::sink()) { println!("{pad}{key}: {v} (passthrough, in the clear)"); - } else if let Some(v) = boxed.downcast_ref::() { + } else if let Ok(v) = frame.visit(String::sink()) { println!("{pad}{key}: {v:?} (passthrough, in the clear)"); } } diff --git a/packages/stack-encrypt/src/cipher.rs b/packages/stack-encrypt/src/cipher.rs index 46103500c..716f7d019 100644 --- a/packages/stack-encrypt/src/cipher.rs +++ b/packages/stack-encrypt/src/cipher.rs @@ -2,11 +2,13 @@ //! module documents the internals. //! //! `StackCipher` is scoped to one ZeroKMS client; a [`KeysetCipher`] — the -//! cipher bound to one of that client's keysets — is a vitaminc [`Cipher`] -//! whose per-leaf keys are ZeroKMS data keys, minted under that keyset, -//! rather than one fixed key. Structurally it mirrors -//! `vitaminc_encrypt::Aes256Cipher`: encrypting an [`Encrypt`] value produces -//! a recursive ciphertext tree ([`StackCipherText`], the analog of +//! cipher bound to one of that client's keysets — seals values whose +//! per-leaf keys are ZeroKMS data keys, minted under that keyset, rather +//! than one fixed key. The value side is vitaminc's: a value describes +//! itself once ([`Plaintext`]), vitaminc plans it into a tree of pending +//! leaves, each with its tagged plaintext and the AAD its place derives, +//! and a bytes-only [`Cipher`] seals and opens one leaf at a time. The +//! result is a ciphertext tree ([`StackCipherText`], the analog of //! `AesCipherText`) whose leaves ([`SealedValue`]) each carry the ZeroKMS //! metadata for their own data key — the keyset it was minted under //! included, which is why decrypting needs no keyset named and lives on the @@ -14,41 +16,48 @@ //! //! ## Batching the key fetch //! -//! Data-key generation/retrieval is an async ZeroKMS round-trip, so the key -//! fetch cannot happen inside the synchronous [`Cipher`]/[`Decipher`] trait -//! methods. It is front-loaded on both sides; the AES work stays inside the -//! trait drive: +//! Data-key generation/retrieval is an async ZeroKMS round-trip, and each +//! leaf needs its own key, so the keys are fetched for the whole tree +//! before any leaf is sealed or opened. The AES work stays in vitaminc's +//! synchronous walk: //! -//! * **Encrypt** — driving the [`Cipher`] trait over a `&KeysetCipher` builds -//! a *pending* tree ([`PendingStackCipherText`]) that holds plaintext plus -//! each leaf's fully derived AAD, but does no I/O. A single +//! * **Encrypt** — [`PendingStackCipherText::plan`] walks the value into +//! vitaminc's pending tree and does no I/O. A single //! [`PendingStackCipherText::seal`] (or the [`KeysetCipher::encrypt`] //! convenience) then batches **one** `generate_keys` call for the whole -//! tree, under the handle's keyset, and seals every leaf. +//! tree, under the handle's keyset, and seals every leaf through +//! `MintedKeys`, the sealing half of this crate's [`Cipher`], one key per +//! leaf in walk order. //! * **Decrypt** — [`StackCipher::decrypt`] batches **one** `retrieve_keys` -//! call per keyset the leaves were sealed under and zips each key onto its -//! leaf, building a [`StackDecipher`] it does not hand out. The -//! value's [`Decrypt`] impl then drives that decipher exactly as it would -//! `AesDecipher`: each leaf is opened under the AAD the drive supplies, so -//! the visitor pattern (nested `Vec`/`HashMap`/`Option`/`Protected` values, -//! and AAD-deriving wrappers such as `vitaminc_aead::Element`) works -//! identically to `Aes256Cipher`. +//! call per keyset the leaves were sealed under and zips each key onto +//! its leaf. vitaminc's [`decrypt`](vitaminc_aead::decrypt) then opens +//! every leaf through `BoundKeys`, the opening half, and walks the opened +//! tree into the value's [`Decrypt`] sink. +//! +//! Neither half is vitaminc's `AsyncCipher`. Its `seal_many` batches the +//! leaves of one document, but a ZeroKMS request names the descriptor of +//! the context the whole tree is sealed under, which a leaf's derived AAD +//! does not carry, and the target layer merges the key requests of many +//! documents (the fields of a record, the rows of a column) into one call +//! (RFC 0002). So the fetch is the target layer's +//! [`Pending`](crate::target::Pending), and the cipher vitaminc drives is +//! the synchronous one. //! //! ## AAD derivation //! -//! The caller's AAD is refined per node with the same domain-separated -//! derivations `Aes256Cipher` uses, so the container *shape* is authenticated: +//! vitaminc derives each leaf's AAD from the caller's, on both sides, with +//! the same domain-separated derivations every vitaminc cipher gets, so the +//! container *shape* is authenticated: //! //! * sequence elements are sealed under [`Context::for_sequence_element`]; //! * map values under [`Context::for_map_entry`] of their cleartext key (so //! swapping or renaming keys fails decryption); -//! * authenticated-absent markers under [`Context::for_none`], and empty -//! sequences/maps under [`Context::for_empty_sequence`]/[`Context::for_empty_map`] — -//! each sealing an *empty* plaintext, verified as empty on open. +//! * empty sequences/maps under [`Context::for_empty_sequence`] / +//! [`Context::for_empty_map`], each sealing an *empty* plaintext, +//! verified as empty on open. An absent value (`None`) is a null leaf like +//! any other scalar. //! -//! The decrypt side performs the same derivations inside [`StackDecipher`]'s -//! `decrypt_seq`/`decrypt_map`/`decrypt_option` as the caller's `Decrypt` impl -//! drives it, so there is a single source of truth for the per-node AAD. +//! This crate binds its own envelope on top of that, per leaf (below). //! //! ## Leaf crypto and wire format //! @@ -82,9 +91,7 @@ //! concatenation). A compatibility module can be added later to read existing //! records as customers of `cipherstash-client` migrate. -use std::any::Any; -use std::borrow::Cow; -use std::collections::HashSet; +use std::cell::RefCell; use std::num::NonZeroUsize; use std::sync::{Arc, Mutex, PoisonError}; use std::time::Duration; @@ -96,27 +103,26 @@ use stack_kms::{EnvKeyProvider, StackKms, StackKmsBuilder}; #[cfg(all(feature = "http", not(target_arch = "wasm32")))] use stack_kms::{FallbackKeyProvider, KeyProvider, KeyProviderError, ProfileStore}; use uuid::Uuid; +use vitaminc_aead::seal::seal_all; use vitaminc_aead::{ - Cipher, CipherText, Context, Decipher, DecipherVisitor, Decrypt, Encrypt, IntoAad, IntoContext, - LocalCipherText, MapAccess, MapCipher, SeqAccess, SeqCipher, Unspecified, + Cipher, CipherText, Context, Decrypt, IntoAad, IntoContext, LocalCipherText, Node, Pending, + Plaintext, Unspecified, }; -use vitaminc_encrypt::{Aes256Cipher, AesCipherText, Key as AesKey}; -use vitaminc_protected::{Controlled, Protected}; +use vitaminc_encrypt::{Aes256Cipher, Key as AesKey}; +use vitaminc_protected::Protected; use crate::keyset::{KeysetCache, KeysetCipher, KeysetState, Lookup, DEFAULT_NAME_TTL}; use crate::Descriptor; -/// The passthrough payload type: type-erased, as for Rust-native vitaminc -/// ciphers. Callers box on the way in and downcast on the way out. -pub type BoxedPassthrough = Box; - /// The recursive ciphertext container produced by [`StackCipher`]: vitaminc's -/// generic [`CipherText`] tree over [`SealedValue`] leaves. Its shape -/// mirrors the encrypted plaintext: a scalar yields `Single`, a `Vec` yields -/// `Sequence` (or `EmptySequence`), a `HashMap` or struct yields `Map` (or -/// `EmptyMap`). Map keys are stored in the clear but bound into their value's -/// AAD. -pub type StackCipherText = CipherText; +/// ciphertext tree ([`Node`]) over [`SealedValue`] leaves. Its shape +/// mirrors the encrypted plaintext: a scalar, `None` included, yields +/// `Single`, a `Vec` yields `Sequence` (or `EmptySequence`), a `HashMap` or +/// struct yields `Map` (or `EmptyMap`). Map keys are stored in the clear but +/// bound into their value's AAD. A passthrough subtree travels in the clear +/// as a [`PassthroughFrame`](vitaminc_aead::PassthroughFrame): the subtree's +/// frame bytes. +pub type StackCipherText = Node; /// Errors from sealing or opening a [`StackCipherText`]. /// @@ -812,11 +818,11 @@ impl KeysetCipher<'_, K> { /// Every key is requested under the [`Descriptor`] of `aad`. pub async fn encrypt<'a, T, A>(&self, value: T, aad: A) -> Result where - T: Encrypt, + T: Plaintext, A: IntoContext<'a>, { let context = aad.into_context(); - let pending = value.encrypt_with_aad(self, context.clone().into_aad())?; + let pending = PendingStackCipherText::plan(value, context.clone())?; pending.seal(self, context).await } @@ -825,61 +831,49 @@ impl KeysetCipher<'_, K> { /// is retrieved. pub async fn decrypt<'a, T, A>(&self, ciphertext: StackCipherText, aad: A) -> Result where - T: Decrypt<'static> + 'static, + T: Decrypt, A: IntoContext<'a>, { decrypt_through(self, ciphertext, aad).await } } -/// Retrieve every leaf's data key under `aad`'s descriptor and bind them -/// onto the ciphertext, for either scope. The work is the same on both — -/// one descriptor, one pending, one settle — and the scope is the whole +/// `decrypt`, for either scope: retrieve every leaf's data key under +/// `aad`'s descriptor, bind them onto the ciphertext, and open it into `T` +/// under the same `aad`. The work is the same on both scopes — one +/// descriptor, one pending, one settle — and the scope is the whole /// difference: a [`KeysetCipher`] constrains the leaves to its keyset, a /// [`StackCipher`] constrains nothing. /// -/// Deliberately private. The returned [`StackDecipher`] is driven with an -/// AAD supplied per call, so exposing this would let a caller retrieve keys -/// under one context and authenticate the ciphertext under an unrelated -/// one. Every leaf's AAD is a derivation of the descriptor its key was -/// minted under — `for_sequence_element`, `for_map_entry`, `for_leaf` — -/// and that derivation is the library's to compute, never the caller's to -/// supply. [`decrypt_through`] is the only way in, and it passes one `aad` -/// to both halves. -async fn decipher_through<'s, 'a, K: DataKeySource + 's>( - scope: impl crate::target::CipherScope<'s, K>, - ciphertext: StackCipherText, - aad: impl IntoContext<'a>, -) -> Result { - crate::target::decipher_pending(scope, ciphertext, Descriptor::of(aad)) - .settle() - .await -} - -/// `decrypt`, for either scope: [`decipher_through`], then the value's own -/// [`Decrypt`] drive under the same `aad`. +/// One `aad` goes to both halves, and that is the point: every leaf's AAD +/// is a derivation of the descriptor its key was minted under — +/// `for_sequence_element`, `for_map_entry`, `for_leaf` — and that +/// derivation is the library's to compute, never the caller's to supply. async fn decrypt_through<'s, 'a, T, K: DataKeySource + 's>( scope: impl crate::target::CipherScope<'s, K>, ciphertext: StackCipherText, aad: impl IntoContext<'a>, ) -> Result where - T: Decrypt<'static> + 'static, + T: Decrypt, { let context = aad.into_context(); - let decipher = decipher_through(scope, ciphertext, context.clone()).await?; - T::decrypt_with_aad(decipher, context.into_aad()).map_err(Error::from) + let keyed = crate::target::bind_pending(scope, ciphertext, Descriptor::of(context.clone())) + .settle() + .await?; + open_keyed(keyed, context) } impl StackCipher { /// Decrypt a [`StackCipherText`] into `T`, authenticating against `aad`. /// /// One batched `retrieve_keys` call per keyset the leaves were sealed - /// under, every key under the [`Descriptor`] of `aad`, then `T`'s - /// [`Decrypt`] impl drives the resulting [`StackDecipher`] with the - /// *same* `aad` — exactly as `Aes256Cipher::decrypt_with_aad` drives - /// `AesDecipher`. One context in, and every leaf's AAD derived from it; - /// there is no form of this call that takes two. + /// under, every key under the [`Descriptor`] of `aad`, then vitaminc + /// opens every leaf under the AAD it derives from the *same* `aad` and + /// walks the result into `T`'s [`Decrypt`] sink — exactly as + /// `vitaminc_aead::decrypt` does over an `Aes256Cipher`. One context in, + /// and every leaf's AAD derived from it; there is no form of this call + /// that takes two. /// /// Not keyset-scoped: each leaf carries the id of the keyset it was /// sealed under, and this opens leaves from any keyset the client is @@ -900,7 +894,7 @@ impl StackCipher { /// a foreign leaf before any. pub async fn decrypt<'a, T, A>(&self, ciphertext: StackCipherText, aad: A) -> Result where - T: Decrypt<'static> + 'static, + T: Decrypt, A: IntoContext<'a>, { decrypt_through(self, ciphertext, aad).await @@ -1208,21 +1202,20 @@ impl TryFrom for SealedValue { } /// A leaf with its retrieved data key bound alongside. Produced by -/// [`bind_keys`] once the batched `retrieve_keys` call has returned; consumed by -/// [`StackDecipher`], which opens it under whatever AAD the driving -/// [`Decrypt`] impl supplies. +/// [`bind_keys`] once the batched `retrieve_keys` call has returned; opened +/// by [`BoundKeys`] under the AAD vitaminc's walk derives for its place. pub(crate) struct KeyedLeaf { leaf: SealedValue, key: DataKey, } /// [`StackCipherText`] with a [`DataKey`] zipped onto every keyed leaf. -pub(crate) type KeyedCipherText = CipherText; +pub(crate) type KeyedCipherText = Node; /// Zip retrieved keys onto the tree in the same depth-first order the -/// target layer requested them (`retrieve_requests`), so each leaf carries its -/// own key and the subsequent [`Decipher`] drive is free of ordering -/// assumptions. +/// target layer requested them (`retrieve_requests`), so each leaf carries +/// its own key and opening it makes no assumption about the order vitaminc +/// visits leaves in. pub(crate) fn bind_keys( ciphertext: StackCipherText, keys: &mut impl Iterator, @@ -1237,7 +1230,6 @@ pub(crate) fn bind_keys( match ciphertext { CipherText::Single(leaf) => Ok(CipherText::Single(bind(leaf, keys)?)), - CipherText::None(leaf) => Ok(CipherText::None(bind(leaf, keys)?)), CipherText::EmptySequence(leaf) => Ok(CipherText::EmptySequence(bind(leaf, keys)?)), CipherText::EmptyMap(leaf) => Ok(CipherText::EmptyMap(bind(leaf, keys)?)), CipherText::Sequence(items) => { @@ -1259,59 +1251,53 @@ pub(crate) fn bind_keys( } // ============================================================================= -// Pending tree (`Cipher::Ok`) — built synchronously, sealed asynchronously +// Pending tree — planned synchronously, sealed asynchronously // ============================================================================= -/// The intermediate result of driving the [`Cipher`] trait: a tree that holds -/// plaintext (and the AAD each leaf will be sealed against, fully derived) but -/// has done no ZeroKMS I/O. [`seal`](Self::seal) turns it into a +/// A value planned for sealing: vitaminc's tree of pending leaves, each +/// holding its tagged plaintext and the AAD its place in the value derives, +/// with no ZeroKMS I/O done yet. [`seal`](Self::seal) turns it into a /// [`StackCipherText`]. -pub enum PendingStackCipherText { - /// A scalar awaiting a data key, with its bound (derived) AAD. - Single { - plaintext: Protected>, - aad: Context<'static>, - }, - /// A pending sequence with at least one element. - Sequence(Vec), - /// A pending empty-sequence marker; `aad` is already the - /// [`Context::for_empty_sequence`] derivation. - EmptySequence { aad: Context<'static> }, - /// A pending map with at least one entry. - Map(Vec<(String, PendingStackCipherText)>), - /// A pending empty-map marker; `aad` is already the [`Context::for_empty_map`] - /// derivation. - EmptyMap { aad: Context<'static> }, - /// A pending authenticated-absent marker; `aad` is already the - /// [`Context::for_none`] derivation. - None { aad: Context<'static> }, - /// A passthrough value (needs no key). - Passthrough(BoxedPassthrough), -} +/// +/// [`plan`](Self::plan) walks the value once, through vitaminc's +/// `EncryptSink`, which owns every container rule (element and entry AAD, +/// the empty markers, duplicate keys, a container with nothing sealed in +/// it). This crate decides only where each leaf's key comes from. +pub struct PendingStackCipherText(Node); impl PendingStackCipherText { - /// Number of leaves that need a ZeroKMS data key (everything but - /// passthrough — markers are sealed leaves too). + /// Plan `value` for sealing under `aad`. No key is requested. + /// + /// A value vitaminc refuses to encrypt as given (a map with a repeated + /// key, a container whose every child is a passthrough, a nesting deeper + /// than the transport carries) is [`Error::Aead`], as it is when the + /// value is sealed in one step. + pub fn plan<'a, P: Plaintext>(value: P, aad: impl IntoAad<'a>) -> Result { + vitaminc_aead::plan::<_, Unspecified, _>(value, aad) + .map(Self) + .map_err(|_| Error::Aead) + } + + /// Number of leaves that need a ZeroKMS data key: every leaf, the empty + /// markers included; a passthrough needs none. pub(crate) fn key_count(&self) -> usize { - match self { - PendingStackCipherText::Single { .. } - | PendingStackCipherText::None { .. } - | PendingStackCipherText::EmptySequence { .. } - | PendingStackCipherText::EmptyMap { .. } => 1, - PendingStackCipherText::Sequence(items) => items.iter().map(Self::key_count).sum(), - PendingStackCipherText::Map(entries) => { - entries.iter().map(|(_, v)| v.key_count()).sum() + fn count(node: &Node) -> usize { + match node { + CipherText::Single(_) | CipherText::EmptySequence(_) | CipherText::EmptyMap(_) => 1, + CipherText::Sequence(items) => items.iter().map(count).sum(), + CipherText::Map(entries) => entries.iter().map(|(_, v)| count(v)).sum(), + CipherText::Passthrough(_) => 0, } - PendingStackCipherText::Passthrough(_) => 0, } + count(&self.0) } /// Generate one data key per keyed leaf (one batched ZeroKMS call — none /// for a passthrough-only tree), every key under the [`Descriptor`] of /// `aad`, and seal the whole tree. /// - /// `aad` is the context the tree was built under — the value passed to - /// `encrypt_with_aad`, in the same shape (see the + /// `aad` is the context the tree was planned under — the value passed to + /// [`plan`](Self::plan), in the same shape (see the /// [descriptor docs](crate::descriptor)). The tree itself only carries /// the *derived* per-leaf AADs, so the root is named here; nothing can /// check that the two agree, which is why [`KeysetCipher::encrypt`], which @@ -1334,8 +1320,8 @@ impl PendingStackCipherText { /// /// [`seal`](Self::seal) is this plus an immediate settle — one ZeroKMS /// call per tree. `into_pending` exists for callers that hold *several* - /// independently built trees (each from its own [`Encrypt`] drive, e.g. - /// one per record field in a language binding) and want them merged with + /// independently planned trees (one per record field in a language + /// binding, say) and want them merged with /// [`Pending::zip`](crate::target::Pending::zip) / /// [`Pending::all`](crate::target::Pending::all) so the whole assembly /// seals in **one** batched `generate_keys` call. Same sealing path @@ -1360,56 +1346,18 @@ impl PendingStackCipherText { crate::target::seal_pending(cipher, self, Descriptor::of(aad)) } - /// Recursively seal under `keyset_id`, drawing one key per leaf from - /// `keys` in traversal order. + /// Seal under `keyset_id`, drawing one key per leaf from `keys` in + /// vitaminc's walk order. A leaf with no key left is refused. pub(crate) fn seal_with( self, keyset_id: Uuid, - keys: &mut impl Iterator, + mut keys: impl Iterator, ) -> Result { - // Markers seal an *empty* plaintext so the AEAD tag still binds their - // (already domain-separated) AAD, mirroring `Aes256Cipher`. - fn seal_marker( - aad: Context<'static>, - keyset_id: Uuid, - keys: &mut impl Iterator, - ) -> Result { - let key = keys.next().ok_or(Unspecified)?; - seal_leaf(Protected::new(Vec::new()), &aad, keyset_id, key) - } - - match self { - PendingStackCipherText::Single { plaintext, aad } => { - let key = keys.next().ok_or(Unspecified)?; - Ok(CipherText::Single(seal_leaf( - plaintext, &aad, keyset_id, key, - )?)) - } - PendingStackCipherText::None { aad } => { - Ok(CipherText::None(seal_marker(aad, keyset_id, keys)?)) - } - PendingStackCipherText::EmptySequence { aad } => Ok(CipherText::EmptySequence( - seal_marker(aad, keyset_id, keys)?, - )), - PendingStackCipherText::EmptyMap { aad } => { - Ok(CipherText::EmptyMap(seal_marker(aad, keyset_id, keys)?)) - } - PendingStackCipherText::Sequence(items) => { - let mut out = Vec::with_capacity(items.len()); - for item in items { - out.push(item.seal_with(keyset_id, keys)?); - } - Ok(CipherText::Sequence(out)) - } - PendingStackCipherText::Map(entries) => { - let mut out = Vec::with_capacity(entries.len()); - for (k, v) in entries { - out.push((k, v.seal_with(keyset_id, keys)?)); - } - Ok(CipherText::Map(out)) - } - PendingStackCipherText::Passthrough(value) => Ok(CipherText::Passthrough(value)), - } + let minted = MintedKeys { + keyset_id, + keys: RefCell::new(&mut keys), + }; + seal_all(&minted, self.0).map_err(|_| Unspecified) } } @@ -1466,11 +1414,12 @@ fn leaf_aad(aad: &Context<'_>, keyset_id: Uuid, tag: &[u8]) -> Context<'static> ]) } -/// Seal one plaintext leaf under a freshly generated data key. +/// Seal one leaf's tagged plaintext under a freshly generated data key. /// -/// The AAD is the [`leaf_aad`] derivation of the caller's (derived) AAD, the -/// keyset the key was minted under, and the key `tag` — `tag` is always -/// bound, so the leaf is cryptographically tied to its ZeroKMS data key. +/// The AAD is the [`leaf_aad`] derivation of the AAD vitaminc derived for +/// the leaf's place, the keyset the key was minted under, and the key +/// `tag` — `tag` is always bound, so the leaf is cryptographically tied to +/// its ZeroKMS data key. fn seal_leaf( plaintext: Protected>, aad: &Context<'_>, @@ -1485,641 +1434,96 @@ fn seal_leaf( SealedValue::tag_fits_length_field(&key.tag).map_err(|_| Unspecified)?; let iv = key.key.iv; let cipher = leaf_cipher(&key.key)?; - match (&cipher).encrypt_bytes_vec(plaintext, leaf_aad(aad, keyset_id, &key.tag))? { - AesCipherText::Single(ciphertext) => Ok(SealedValue { - keyset_id, - iv, - tag: key.tag, - ciphertext, - }), - _ => Err(Unspecified), - } + let ciphertext = cipher.seal(plaintext, &leaf_aad(aad, keyset_id, &key.tag))?; + Ok(SealedValue { + keyset_id, + iv, + tag: key.tag, + ciphertext, + }) } -/// Open one keyed leaf under `aad`, returning the plaintext bytes. +/// Open one keyed leaf under `aad`, returning its tagged plaintext. fn open_leaf(keyed: KeyedLeaf, aad: &Context<'_>) -> Result>, Unspecified> { - /// Keeps the recovered bytes inside `Protected` across the visitor - /// boundary (the blanket `Decrypt for Vec` would unwrap them). - struct ProtectedBytes; - impl<'c> DecipherVisitor<'c> for ProtectedBytes { - type Value = Protected>; - fn visit_bytes_vec(self, data: Protected>) -> Result { - Ok(data) - } - } - let KeyedLeaf { leaf, key } = keyed; let cipher = leaf_cipher(&key)?; - cipher - .decipher(AesCipherText::Single(leaf.ciphertext)) - .decrypt_bytes(ProtectedBytes, leaf_aad(aad, leaf.keyset_id, &leaf.tag)) -} - -/// Open one marker leaf (absent / empty-sequence / empty-map) and require the -/// sealed plaintext to be empty. Without the emptiness check, a `Single` leaf -/// could be re-tagged as a marker of the same AAD derivation. -fn verify_empty_marker(keyed: KeyedLeaf, aad: &Context<'_>) -> Result<(), Unspecified> { - let plaintext = open_leaf(keyed, aad)?; - if plaintext.risky_ref().is_empty() { - Ok(()) - } else { - Err(Unspecified) - } + cipher.open(leaf.ciphertext, &leaf_aad(aad, leaf.keyset_id, &leaf.tag)) } // ============================================================================= -// Encrypt side: `Cipher` impl over a `&KeysetCipher` (builds the pending tree) +// The vitaminc ciphers: one leaf at a time, keys already in hand // ============================================================================= -impl<'c, 'k, K> Cipher for &'c KeysetCipher<'k, K> { - type Ok = PendingStackCipherText; - type Error = Unspecified; - type Passthrough = BoxedPassthrough; - type SeqCipher = PendingSeqCipher<'c, 'k, K>; - type MapCipher = PendingMapCipher<'c, 'k, K>; - - fn encrypt_bytes_vec<'a, A>( - self, - data: Protected>, - aad: A, - ) -> Result - where - A: IntoAad<'a>, - { - Ok(PendingStackCipherText::Single { - plaintext: data, - aad: aad.into_aad().into_owned(), - }) - } - - fn encrypt_seq<'a, A>(self, size_hint: Option, aad: A) -> Self::SeqCipher - where - A: IntoAad<'a>, - { - let aad = aad.into_aad().into_owned(); - PendingSeqCipher { - cipher: self, - items: Vec::with_capacity(size_hint.unwrap_or(0)), - // Both derived once here, then borrowed per element. - element_aad: aad.for_sequence_element(), - aad, - encrypted: false, - } - } - - fn encrypt_map<'a, A>(self, aad: A) -> Self::MapCipher - where - A: IntoAad<'a>, - { - PendingMapCipher { - cipher: self, - entries: Vec::new(), - seen_keys: HashSet::new(), - current_key: None, - aad: aad.into_aad().into_owned(), - encrypted: false, - } - } - - fn encrypt_none<'a, A>(self, aad: A) -> Result - where - A: IntoAad<'a>, - { - // Domain-separated so a `Single` leaf sealed under the bare AAD can - // never be re-tagged as an authenticated absence (and vice versa). - Ok(PendingStackCipherText::None { - aad: aad.into_aad().for_none(), - }) - } - - fn passthrough(self, value: Self::Passthrough) -> Result { - Ok(PendingStackCipherText::Passthrough(value)) - } - - fn passthrough_boxed( - self, - value: Box, - ) -> Result { - // This cipher's passthrough type *is* `Box`, so the - // type-erased box is already the passthrough payload. - self.passthrough(value) - } -} - -/// [`SeqCipher`] driver: accumulates a pending sub-tree per element. Holds the -/// cipher only to re-drive nested [`Encrypt`] values (no I/O happens here). -pub struct PendingSeqCipher<'c, 'k, K> { - cipher: &'c KeysetCipher<'k, K>, - items: Vec, - /// The AAD fixed at [`Cipher::encrypt_seq`]; the empty marker is sealed - /// against its `for_empty_sequence` derivation. - aad: Context<'static>, - /// [`Context::for_sequence_element`] of `aad`, derived once and applied to - /// every element. - element_aad: Context<'static>, - /// Whether at least one element went through the authenticated - /// [`encrypt_next`](SeqCipher::encrypt_next) path *and* produced a sealed - /// node — see [`end`](SeqCipher::end). - encrypted: bool, +/// The sealing half of this crate's vitaminc [`Cipher`]: seals each leaf +/// under the next of a batch of data keys ZeroKMS has already generated, +/// in the order vitaminc's [`seal_all`] visits the leaves. +/// +/// The batch is fetched before the walk, in one `generate_keys` call per +/// pending (see [`PendingStackCipherText::seal`]), so the walk itself is +/// synchronous. A leaf for which no key is left is refused. +struct MintedKeys<'k, I> { + keyset_id: Uuid, + keys: RefCell<&'k mut I>, } -impl SeqCipher for PendingSeqCipher<'_, '_, K> { - type Ok = PendingStackCipherText; +impl> Cipher for MintedKeys<'_, I> { + type Leaf = SealedValue; type Error = Unspecified; - type Passthrough = BoxedPassthrough; - fn encrypt_next(mut self, data: T) -> Result - where - T: Encrypt, - { - // Borrow the stored derived AAD — no allocation per element. - let pending = data.encrypt_with_aad(self.cipher, self.element_aad.clone())?; - // A nested `Encrypt` impl may route through the passthrough channel; - // only a genuinely pending-sealed node may satisfy `end`'s - // all-passthrough rejection. - self.encrypted |= !matches!(pending, PendingStackCipherText::Passthrough(_)); - self.items.push(pending); - Ok(self) - } - - fn passthrough_next(mut self, value: Self::Passthrough) -> Result { - self.items.push(PendingStackCipherText::Passthrough(value)); - Ok(self) + fn seal( + &self, + plaintext: Protected>, + aad: &Context<'_>, + ) -> Result { + let key = self.keys.borrow_mut().next().ok_or(Unspecified)?; + seal_leaf(plaintext, aad, self.keyset_id, key) } - fn end(self) -> Result { - if self.items.is_empty() { - Ok(PendingStackCipherText::EmptySequence { - aad: self.aad.for_empty_sequence(), - }) - } else if !self.encrypted { - // Every item is a passthrough: nothing in the container would - // authenticate the AAD, so refuse to produce it — mirroring - // `decrypt_tree`'s rejection on open. - Err(Unspecified) - } else { - Ok(PendingStackCipherText::Sequence(self.items)) - } + /// Opening goes through [`BoundKeys`], whose leaves carry their + /// retrieved keys. A minted key is never used to open. + fn open(&self, _: SealedValue, _: &Context<'_>) -> Result>, Unspecified> { + Err(Unspecified) } } -/// [`MapCipher`] driver: keys are stored in the clear; values become pending -/// sub-trees sealed against [`Context::for_map_entry`] of the map AAD and their -/// key. Mirrors `AesMapCipher`'s key/value and duplicate-key contract checks. -pub struct PendingMapCipher<'c, 'k, K> { - cipher: &'c KeysetCipher<'k, K>, - entries: Vec<(String, PendingStackCipherText)>, - /// Duplicate keys are rejected at encrypt time: `decrypt_tree` rejects - /// them outright, so accepting one here would produce a permanently - /// unreadable ciphertext. - seen_keys: HashSet, - current_key: Option>, - /// The AAD fixed at [`Cipher::encrypt_map`]. - aad: Context<'static>, - /// See [`PendingSeqCipher::encrypted`]. - encrypted: bool, -} +/// The opening half of this crate's vitaminc [`Cipher`]: opens a leaf +/// whose retrieved data key [`bind_keys`] has already bound to it, under +/// the AAD vitaminc's walk derives for the leaf's place. vitaminc then +/// checks the empty markers and the container rules and hands each +/// scalar to the sink, so the decrypt side has one definition of the +/// per-node AAD, shared with every other vitaminc cipher. +pub(crate) struct BoundKeys; -impl MapCipher for PendingMapCipher<'_, '_, K> { - type Ok = PendingStackCipherText; +impl Cipher for BoundKeys { + type Leaf = KeyedLeaf; type Error = Unspecified; - type Passthrough = BoxedPassthrough; - - fn encrypt_key(mut self, key: S) -> Result - where - S: Into>, - { - // A pending key means `encrypt_key` ran twice with no intervening - // `encrypt_value` — fail rather than silently drop the first key. - if self.current_key.is_some() { - return Err(Unspecified); - } - let key = key.into(); - if !self.seen_keys.insert(key.as_ref().to_owned()) { - return Err(Unspecified); - } - self.current_key = Some(key); - Ok(self) - } - - fn encrypt_value(mut self, value: U) -> Result - where - U: Encrypt, - { - let key = self.current_key.take().ok_or(Unspecified)?; - // Seal against PAE(domain, aad, key) — key and value are inseparable. - // `decrypt_tree` derives the same AAD on open. - let entry_aad = self.aad.for_map_entry(&key); - let pending = value.encrypt_with_aad(self.cipher, entry_aad)?; - self.encrypted |= !matches!(pending, PendingStackCipherText::Passthrough(_)); - self.entries.push((key.into_owned(), pending)); - Ok(self) - } - - fn passthrough_entry(mut self, key: S, value: Self::Passthrough) -> Result - where - S: Into>, - { - // Adopting a pending key here would silently drop it — same contract - // violation `encrypt_key` rejects. - if self.current_key.is_some() { - return Err(Unspecified); - } - let key = key.into(); - if !self.seen_keys.insert(key.as_ref().to_owned()) { - return Err(Unspecified); - } - self.entries - .push((key.into_owned(), PendingStackCipherText::Passthrough(value))); - Ok(self) - } - - fn passthrough_entry_boxed( - self, - key: S, - value: Box, - ) -> Result - where - S: Into>, - { - // This cipher's passthrough type *is* `Box`, so the - // type-erased box is already the payload — same as `passthrough_boxed`. - self.passthrough_entry(key, value) - } - - fn end(self) -> Result { - // Finalising with a pending key would silently drop the entry. - if self.current_key.is_some() { - return Err(Unspecified); - } - if self.entries.is_empty() { - Ok(PendingStackCipherText::EmptyMap { - aad: self.aad.for_empty_map(), - }) - } else if !self.encrypted { - // Every entry is a passthrough — see `PendingSeqCipher::end`. - Err(Unspecified) - } else { - Ok(PendingStackCipherText::Map(self.entries)) - } - } -} - -// Decrypt side: a synchronous `Decipher` over a key-bound ciphertext tree -// ============================================================================= - -/// A [`Decipher`] over a single [`StackCipherText`] whose leaves already -/// carry their retrieved data keys, built inside -/// [`StackCipher::decrypt`] and driven there by the value's [`Decrypt`] -/// impl under the same context the keys were retrieved with. -/// -/// Structurally identical to `vitaminc_encrypt::AesDecipher` — the only -/// difference is where each leaf's key comes from. The AAD is supplied per call -/// by [`Decrypt::decrypt_with_aad`] and refined here exactly as the encrypt side -/// refined it: sequence elements under [`Context::for_sequence_element`], map -/// values under [`Context::for_map_entry`] of their key, markers under their -/// respective derivations with an enforced-empty plaintext. Because the -/// derivation lives in this drive (not in a pre-pass), `Decrypt` impls that -/// transform the AAD themselves (e.g. `vitaminc_aead::Element`) work unchanged. -pub struct StackDecipher { - ciphertext: KeyedCipherText, -} - -impl StackDecipher { - pub(crate) fn over(ciphertext: KeyedCipherText) -> Self { - Self { ciphertext } - } - - /// Typed convenience over [`Decipher::decrypt_passthrough`] for this - /// cipher's [`BoxedPassthrough`] payload type: recovers the payload and - /// downcasts it to `T`, returning [`Unspecified`] if the ciphertext is not - /// a passthrough or the stored type does not match. - pub fn decrypt_passthrough_as(self) -> Result - where - T: Any + Send + 'static, - { - self.decrypt_passthrough()? - .downcast::() - .map(|b| *b) - .map_err(|_| Unspecified) - } -} - -impl<'c> Decipher<'c> for StackDecipher { - type Ok - = Result - where - T: Send + 'c; - - type Passthrough = BoxedPassthrough; - - fn map_ok(ok: Self::Ok, f: F) -> Self::Ok - where - T: Send + 'c, - U: Send + 'c, - F: FnOnce(T) -> U, - { - ok.map(f) - } - - fn decrypt_bytes<'a, V, A>(self, visitor: V, aad: A) -> Self::Ok - where - V: DecipherVisitor<'c> + Send + 'c, - A: IntoAad<'a>, - { - match self.ciphertext { - CipherText::Single(keyed) => { - let bytes = open_leaf(keyed, &aad.into_aad())?; - visitor.visit_bytes_vec(bytes) - } - _ => Err(Unspecified), - } - } - - fn decrypt_seq<'a, V, A>(self, visitor: V, aad: A) -> Self::Ok - where - V: DecipherVisitor<'c> + Send + 'c, - A: IntoAad<'a>, - { - match self.ciphertext { - // At least one non-passthrough item required: passthrough items - // authenticate nothing, so an all-passthrough (or entry-less) - // sequence would verify under any AAD. The encrypt side refuses to - // produce one; refuse to open one. Emptiness is only provable by - // the authenticated `EmptySequence` marker. - CipherText::Sequence(items) - if items - .iter() - .any(|i| !matches!(i, CipherText::Passthrough(_))) => - { - visitor.visit_seq(StackSeqAccess { - items: items.into_iter(), - element_aad: aad.into_aad().for_sequence_element(), - }) - } - CipherText::EmptySequence(keyed) => { - let aad = aad.into_aad(); - verify_empty_marker(keyed, &aad.for_empty_sequence())?; - // Store the element derivation exactly as the non-empty arm - // does: never read (the iterator is empty), but a divergent - // value here would silently break a visitor that consulted it. - visitor.visit_seq(StackSeqAccess { - items: Vec::new().into_iter(), - element_aad: aad.for_sequence_element(), - }) - } - _ => Err(Unspecified), - } - } - - fn decrypt_map<'a, V, A>(self, visitor: V, aad: A) -> Self::Ok - where - V: DecipherVisitor<'c> + Send + 'c, - A: IntoAad<'a>, - { - match self.ciphertext { - // At least one non-passthrough entry required — see `decrypt_seq`. - CipherText::Map(entries) - if entries - .iter() - .any(|(_, v)| !matches!(v, CipherText::Passthrough(_))) => - { - // Reject duplicate keys before the visitor sees any entry: two - // ciphertexts of the same logical record seal a given key's - // value against the identical `for_map_entry` AAD, so a stale - // entry appended to a current ciphertext *verifies* — with a - // last-wins visitor that is a single-field rollback. - let mut seen = HashSet::with_capacity(entries.len()); - if !entries.iter().all(|(key, _)| seen.insert(key.as_str())) { - return Err(Unspecified); - } - visitor.visit_map(StackMapAccess { - entries: entries.into_iter(), - aad: aad.into_aad(), - pending: None, - }) - } - CipherText::EmptyMap(keyed) => { - let aad = aad.into_aad(); - verify_empty_marker(keyed, &aad.for_empty_map())?; - // Raw caller AAD, not the marker derivation — see `decrypt_seq`. - visitor.visit_map(StackMapAccess { - entries: Vec::new().into_iter(), - aad, - pending: None, - }) - } - _ => Err(Unspecified), - } - } - fn decrypt_any<'a, V, A>(self, visitor: V, aad: A) -> Self::Ok - where - V: DecipherVisitor<'c> + Send + 'c, - A: IntoAad<'a>, - { - match self.ciphertext { - ct @ CipherText::Single(_) => Self::over(ct).decrypt_bytes(visitor, aad), - ct @ (CipherText::Sequence(_) | CipherText::EmptySequence(_)) => { - Self::over(ct).decrypt_seq(visitor, aad) - } - ct @ (CipherText::Map(_) | CipherText::EmptyMap(_)) => { - Self::over(ct).decrypt_map(visitor, aad) - } - CipherText::None(keyed) => { - // Verify the domain-separated marker (tag AND empty plaintext) - // before reporting absence — an unauthenticated `visit_none` - // would let an attacker forge "absent" values, and a bare-AAD - // check would let a `Single` leaf be re-tagged as one. Mirrors - // `decrypt_option`. - verify_empty_marker(keyed, &aad.into_aad().for_none())?; - visitor.visit_none() - } - // A self-describing visitor recovers a passthrough via - // `visit_passthrough` (type-erased); visitors that do not override - // it inherit the default rejection. - CipherText::Passthrough(boxed) => visitor.visit_passthrough(boxed), - } - } - - fn decrypt_passthrough(self) -> Self::Ok { - match self.ciphertext { - CipherText::Passthrough(boxed) => Ok(boxed), - _ => Err(Unspecified), - } + /// Sealing goes through [`MintedKeys`]: a leaf is sealed under a key + /// generated for it, never one retrieved for another leaf. + fn seal(&self, _: Protected>, _: &Context<'_>) -> Result { + Err(Unspecified) } - fn decrypt_option<'a, T, A>(self, aad: A) -> Self::Ok> - where - T: Decrypt<'c> + 'c, - A: IntoAad<'a>, - { - match self.ciphertext { - CipherText::None(keyed) => { - // Verify the tag over the domain-separated marker AAD AND that - // the sealed plaintext is actually empty. Without both, a - // `Single(leaf)` sealed under the same caller AAD could be - // re-tagged as `None(leaf)` and decrypt as Ok(None) — silent - // authenticated data deletion. - verify_empty_marker(keyed, &aad.into_aad().for_none())?; - Ok(None) - } - // Passthrough must never be decoded as an Option payload. - CipherText::Passthrough(_) => Err(Unspecified), - // Any other variant is the `Some` payload: recurse into `T` with - // the caller's AAD unchanged (there is no depth tag; the shape of - // nested options is decided by `T` at the call site, as in - // `AesDecipher`). - other => T::decrypt_with_aad(Self::over(other), aad).map(Some), - } - } -} - -struct StackSeqAccess { - items: std::vec::IntoIter, - /// [`Context::for_sequence_element`] of the caller's AAD, derived once at - /// construction and re-supplied per element by borrowing. Mirrors - /// `PendingSeqCipher::element_aad` on the encrypt side. - element_aad: Context<'static>, -} - -impl<'c> SeqAccess<'c> for StackSeqAccess { - type Error = Unspecified; - - fn next_element + 'c>(&mut self) -> Result, Self::Error> { - match self.items.next() { - Some(ct) => { - T::decrypt_with_aad(StackDecipher::over(ct), self.element_aad.clone()).map(Some) - } - None => Ok(None), - } + fn open(&self, leaf: KeyedLeaf, aad: &Context<'_>) -> Result>, Unspecified> { + open_leaf(leaf, aad) } } -struct StackMapAccess<'a> { - entries: std::vec::IntoIter<(String, KeyedCipherText)>, - aad: Context<'a>, - /// The entry handed out by `next_key` and not yet consumed by - /// `next_value` / `next_passthrough`. Held as ciphertext rather than - /// decrypted up front so the caller can choose the plaintext type after - /// seeing the key — see [`MapAccess::next_key`] — alongside the entry - /// AAD it was sealed under, derived once here so the key itself moves - /// out to the caller. - pending: Option<(Context<'static>, KeyedCipherText)>, -} - -impl<'c, 'a> MapAccess<'c> for StackMapAccess<'a> { - type Error = Unspecified; - - fn next_key(&mut self) -> Result, Self::Error> { - // A still-pending entry means the caller skipped a value. Refused - // rather than tolerated: an entry whose value is never opened is an - // entry whose AAD binding is never verified. - if self.pending.is_some() { - return Err(Unspecified); - } - match self.entries.next() { - Some((key, ct)) => { - // Mirror `PendingMapCipher::encrypt_value`: the value was - // sealed against `for_map_entry(key)`, so a swapped or - // renamed key fails when the entry is opened. - self.pending = Some((self.aad.for_map_entry(&key), ct)); - Ok(Some(key)) - } - None => Ok(None), - } - } - - fn next_value + 'c>(&mut self) -> Result { - let (entry_aad, ct) = self.pending.take().ok_or(Unspecified)?; - T::decrypt_with_aad(StackDecipher::over(ct), entry_aad) - } - - fn next_passthrough(&mut self) -> Result, Self::Error> { - match self.pending.take() { - Some((_key, CipherText::Passthrough(value))) => Ok(value), - // A sealed value under a key the caller asked to read as a - // passthrough: refuse rather than hand it back with its tag - // unchecked — but keep the entry pending. The refusal is the - // caller's answer, not a reason to lose the entry: it can still - // open it with `next_value`, and until it does `next_key` keeps - // refusing to move past it. - Some(entry) => { - self.pending = Some(entry); - Err(Unspecified) - } - None => Err(Unspecified), - } - } +/// Open a key-bound tree into `T` under `aad`. Every failure is +/// [`Error::Aead`]: a wrong key or AAD, a tampered leaf or marker, a +/// malformed tree, and a value that is not a `T` all read the same to the +/// caller, as they did before vitaminc split them. +pub(crate) fn open_keyed<'a, T: Decrypt>( + keyed: KeyedCipherText, + aad: impl IntoAad<'a>, +) -> Result { + vitaminc_aead::decrypt(&BoundKeys, keyed, aad).map_err(|_| Error::Aead) } #[cfg(test)] mod tests { use super::*; - fn map_access(entries: Vec<(&str, KeyedCipherText)>) -> StackMapAccess<'static> { - StackMapAccess { - entries: entries - .into_iter() - .map(|(key, ct)| (key.to_string(), ct)) - .collect::>() - .into_iter(), - aad: Context::from_encoded(b"map"), - pending: None, - } - } - - #[test] - fn a_passthrough_entry_is_read_back_as_the_boxed_value() { - let mut map = map_access(vec![("plain", CipherText::Passthrough(Box::new(7u32)))]); - - assert_eq!(map.next_key(), Ok(Some("plain".to_string()))); - let value = map.next_passthrough().expect("passthrough entry"); - assert_eq!(value.downcast_ref::(), Some(&7)); - assert_eq!(map.next_key(), Ok(None)); - } - - /// Asking for a sealed entry as a passthrough is refused, and the entry - /// stays pending: it is neither handed back unverified nor lost, so the - /// caller can still open it with `next_value` and cannot skip it. - #[test] - fn a_sealed_entry_survives_being_misread_as_a_passthrough() { - let mut map = map_access(vec![ - ("sealed", CipherText::Sequence(vec![])), - ("plain", CipherText::Passthrough(Box::new(7u32))), - ]); - - assert_eq!(map.next_key(), Ok(Some("sealed".to_string()))); - assert!(map.next_passthrough().is_err()); - // Still pending: the map refuses to advance past an unopened entry. - assert_eq!(map.next_key(), Err(Unspecified)); - // And the refusal is repeatable, not a one-shot that then drops it. - assert!(map.next_passthrough().is_err()); - assert_eq!(map.next_key(), Err(Unspecified)); - } - - /// A draw with nothing pending — before any `next_key`, or after the - /// entry has already been taken — is a clean refusal, never a value and - /// never a panic. This is the guard that keeps an absent or unverified - /// value from being handed back, so a refactor from `ok_or(..)?` to an - /// `unwrap` would regress silently without it. - #[test] - fn drawing_a_value_with_nothing_pending_is_refused() { - let mut map = map_access(vec![("plain", CipherText::Passthrough(Box::new(7u32)))]); - - // No `next_key` yet: nothing is pending. - assert!(map.next_value::().is_err()); - assert!(map.next_passthrough().is_err()); - - // A legitimate draw consumes the entry, so a second draw of either - // kind is refused too. - assert_eq!(map.next_key(), Ok(Some("plain".to_string()))); - assert!(map.next_passthrough().is_ok()); - assert!(map.next_passthrough().is_err()); - assert!(map.next_value::().is_err()); - assert_eq!(map.next_key(), Ok(None)); - } - /// A cipher's `Debug` — and a keyset handle's — names the keyset and the /// source's type and stops there: the cache behind a cipher holds every /// loaded keyset's index key, a handle carries its keyset's, and the @@ -2145,30 +1549,6 @@ mod tests { } } - /// `decrypt_passthrough` hands back the payload of a passthrough node and - /// nothing else: any sealed shape is refused rather than surfaced - /// unopened, and a payload of another type is refused by the downcast. - #[test] - fn decrypt_passthrough_opens_only_a_passthrough() { - let passthrough = || StackDecipher::over(CipherText::Passthrough(Box::new(7u32))); - - assert_eq!( - passthrough().decrypt_passthrough_as::(), - Ok(7), - "a passthrough payload of the requested type should be handed back" - ); - assert_eq!( - passthrough().decrypt_passthrough_as::(), - Err(Unspecified), - "a payload of another type should fail the downcast" - ); - assert_eq!( - StackDecipher::over(CipherText::Sequence(vec![])).decrypt_passthrough_as::(), - Err(Unspecified), - "a sealed shape should be refused rather than surfaced unopened" - ); - } - /// Byte-level pin for the [`leaf_aad`] derivation. This is part of the /// frozen leaf format: a change to the domain label, the version byte, /// the keyset id's place, the piece order, or the PAE framing makes diff --git a/packages/stack-encrypt/src/dynamic/context.rs b/packages/stack-encrypt/src/dynamic/context.rs index 49c983222..160c4a15b 100644 --- a/packages/stack-encrypt/src/dynamic/context.rs +++ b/packages/stack-encrypt/src/dynamic/context.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; -use vitaminc_aead_value::FfiValue; +use super::FfiValue; use vitaminc_protected::Controlled; use super::{Error, Reason}; @@ -36,7 +36,7 @@ use crate::{ContextPiece, NonEmpty}; /// /// A bare string is one text part. An array is a list, and may nest as deep /// as the transport codec allows -/// ([`MAX_DEPTH`](vitaminc_aead_value::transport::MAX_DEPTH) levels, counted +/// ([`MAX_DEPTH`](vitaminc_aead::frame::MAX_DEPTH) levels, counted /// from the root of the encoded value); a deeper value is refused by the /// codec before this module sees it. Text and bytes with the same content /// are distinct contexts (UTF-8 versus bytes typed leaves) — the same diff --git a/packages/stack-encrypt/src/dynamic/ffi_value.rs b/packages/stack-encrypt/src/dynamic/ffi_value.rs new file mode 100644 index 000000000..d3d1795e5 --- /dev/null +++ b/packages/stack-encrypt/src/dynamic/ffi_value.rs @@ -0,0 +1,675 @@ +//! The runtime value tree every binding funnels through: [`FfiValue`], and +//! the kinds a plan field declares ([`ValueKind`]). +//! +//! Both were `vitaminc-aead-value`'s until vitaminc ADR 0001 reduced that +//! crate to the transport (`vitaminc-aead-frame`). A value now describes +//! itself to vitaminc through [`Plaintext`] and is read back through a +//! [`Decrypt`] sink, so an [`FfiValue`] seals the same tagged leaves a +//! typed Rust value of the same kind does, and crosses an FFI boundary as a +//! transport frame ([`to_frame`](FfiValue::to_frame), +//! [`from_frame`](FfiValue::from_frame)). The tree is kept here, unchanged +//! in shape, so the engine and the bindings built on it keep their +//! behaviour while they move onto vitaminc's model. + +use std::fmt; +use std::str::FromStr; + +use vitaminc_aead::frame::{FrameError, FrameWriteError}; +use vitaminc_aead::plaintext::{ + visit_map, visit_seq, Error as _, Float, Int, Logical, MapSink, Physical, SeqSink, Sink, + SourceError, +}; +use vitaminc_aead::{to_frame, DecodeError, Decrypt, Frame, Plaintext}; +use vitaminc_protected::{Controlled, Protected}; +use zeroize::Zeroize; + +/// An owned, `Send`, language-neutral representation of a dynamically typed +/// host value: what a binding converts a host value into on the host's +/// thread, so encryption and decryption can run on any thread. +/// Secret-bearing leaves (strings and bytes) are held in [`Protected`] so +/// the Rust-side copies are wiped on drop; copies owned by the host +/// runtime's heap cannot be wiped from here. +/// +/// Each variant is one of vitaminc's scalars or containers: `Null` is a +/// null, `Undefined` a null under the `Undefined` logical type, the +/// numeric variants are ints and floats of their width, `String` is text, +/// `Bytes` bytes, `Array` a sequence, `Object` a map, and `Passthrough` a +/// subtree that travels in the clear. So a value seals as the leaves a +/// typed Rust value of the same kind seals as: `UInt32(34)` and `34u32` +/// are the same ciphertext under the same context. +#[derive(Zeroize)] +pub enum FfiValue { + /// Null (JS `null`, Python `None`, Go `nil`). + Null, + /// JavaScript `undefined`. Languages without an analog decode this to + /// their null value and never encode it. + Undefined, + /// Boolean. + Bool(bool), + /// 32-bit signed integer. + Int32(i32), + /// 64-bit signed integer. + Int64(i64), + /// 32-bit unsigned integer. + UInt32(u32), + /// 64-bit unsigned integer. + UInt64(u64), + /// 32-bit floating-point number. Round-trips by IEEE-754 bit pattern. + Float32(f32), + /// 64-bit floating-point number. Round-trips by IEEE-754 bit pattern. + Float64(f64), + /// String as UTF-8 bytes, validated at construction ([`Utf8String`]). + String(Utf8String), + /// Binary data. + Bytes(Protected>), + /// Array: a sequence. + Array(Vec), + /// String-keyed object: a map. Keys travel in the clear, bound into + /// each value's AAD. + Object(Vec<(String, FfiValue)>), + /// A subtree that travels alongside the ciphertext **unencrypted and + /// unauthenticated**. Non-sensitive fields only: anyone holding the + /// ciphertext can read and alter it. + // Skipped by `Zeroize`: the payload is non-sensitive by contract (it is + // sent in the clear), and `Box` is not `Zeroize` anyway. + #[zeroize(skip)] + Passthrough(Box), +} + +/// UTF-8 string payload for [`FfiValue::String`], validated at construction. +/// +/// Wraps `Protected>` with the invariant that the bytes are valid +/// UTF-8, so a `String` leaf can never seal bytes that would not read back +/// as text. +#[derive(Zeroize)] +pub struct Utf8String(Protected>); + +impl Utf8String { + /// The validated UTF-8 bytes. Same chain-of-custody caveats as + /// [`Protected::risky_ref`]: the reference must not outlive its use. + pub fn risky_ref(&self) -> &[u8] { + self.0.risky_ref() + } + + /// Unwrap to the protected byte payload (still valid UTF-8; the + /// invariant is dropped with the type). + pub fn into_inner(self) -> Protected> { + self.0 + } + + /// The text. `None` cannot happen while the invariant holds; it is + /// checked rather than assumed because this is boundary code. + fn as_str(&self) -> Option<&str> { + std::str::from_utf8(self.0.risky_ref()).ok() + } +} + +impl From for Utf8String { + fn from(s: String) -> Self { + Self(Protected::new(s.into_bytes())) + } +} + +impl From<&str> for Utf8String { + fn from(s: &str) -> Self { + Self(Protected::new(s.as_bytes().to_vec())) + } +} + +impl TryFrom>> for Utf8String { + type Error = vitaminc_aead::Unspecified; + + /// Validates the bytes; on failure the rejected payload is dropped (and + /// wiped) here. + fn try_from(bytes: Protected>) -> Result { + let _ = std::str::from_utf8(bytes.risky_ref()).map_err(|_| vitaminc_aead::Unspecified)?; + Ok(Self(bytes)) + } +} + +impl FfiValue { + /// This value as a transport frame: what a binding hands its host. + /// + /// # Errors + /// + /// [`FrameWriteError`] for a value nested deeper than the frame + /// carries, or an object with a repeated key. + pub fn to_frame(&self) -> Result>, FrameWriteError> { + to_frame(self) + } + + /// Read one value from a transport frame: what a host hands a binding. + /// + /// # Errors + /// + /// [`FromFrameError`] when the bytes are not exactly one frame, or the + /// frame holds a scalar no variant holds (an 8-bit integer, a date). + pub fn from_frame(bytes: &[u8]) -> Result { + let frame = Frame::parse(bytes).map_err(FromFrameError::Frame)?; + frame.visit(Self::sink()).map_err(FromFrameError::Value) + } +} + +/// Why [`FfiValue::from_frame`] refused its bytes. +#[derive(Debug, thiserror::Error)] +pub enum FromFrameError { + /// The bytes are not exactly one well-formed frame. + #[error(transparent)] + Frame(FrameError), + /// The frame holds a value no [`FfiValue`] variant holds. + #[error(transparent)] + Value(DecodeError), +} + +/// Walks the value, leaving it where it is. Text and bytes are handed to +/// the sink as borrows of the `Protected` payloads. +impl Plaintext for &FfiValue { + fn visit(self, sink: S) -> Result { + match self { + FfiValue::Null => sink.scalar(None, Physical::Null), + FfiValue::Undefined => sink.scalar(Some(Logical::Undefined), Physical::Null), + FfiValue::Bool(b) => sink.scalar(None, Physical::Bool(*b)), + FfiValue::Int32(i) => sink.scalar(None, Physical::Int(Int::I32(*i))), + FfiValue::Int64(i) => sink.scalar(None, Physical::Int(Int::I64(*i))), + FfiValue::UInt32(u) => sink.scalar(None, Physical::Int(Int::U32(*u))), + FfiValue::UInt64(u) => sink.scalar(None, Physical::Int(Int::U64(*u))), + FfiValue::Float32(f) => sink.scalar(None, Physical::Float(Float::F32(*f))), + FfiValue::Float64(f) => sink.scalar(None, Physical::Float(Float::F64(*f))), + FfiValue::String(s) => match s.as_str() { + Some(text) => sink.scalar(None, Physical::Text(text)), + None => Err(S::Error::source(SourceError::msg("a string is not UTF-8"))), + }, + FfiValue::Bytes(b) => sink.scalar(None, Physical::Bytes(b.risky_ref())), + FfiValue::Array(items) => visit_seq(sink, Some(items.len()), items), + FfiValue::Object(entries) => visit_map( + sink, + Some(entries.len()), + entries.iter().map(|(key, value)| (key.as_str(), value)), + ), + FfiValue::Passthrough(inner) => sink.passthrough(&**inner), + } + } +} + +/// Walks the value by reference, then drops it, which wipes its text and +/// bytes. +impl Plaintext for FfiValue { + fn visit(self, sink: S) -> Result { + (&self).visit(sink) + } +} + +impl Decrypt for FfiValue { + type Sink = ValueSink; + fn sink() -> Self::Sink { + ValueSink(Root) + } +} + +/// Where a finished value goes: back to the caller, or into its parent. +pub trait Put { + /// What putting a value produces. + type Ok; + /// Put `value` in place. + fn put(self, value: FfiValue) -> Self::Ok; +} + +/// The top of a walk: the value is the result. +pub struct Root; + +impl Put for Root { + type Ok = FfiValue; + fn put(self, value: FfiValue) -> FfiValue { + value + } +} + +/// The next element of an array under construction. +pub struct Element<'s>(&'s mut Vec); + +impl Put for Element<'_> { + type Ok = (); + fn put(self, value: FfiValue) { + self.0.push(value); + } +} + +/// One entry of an object under construction. +pub struct Entry<'s>(&'s mut Vec<(String, FfiValue)>, String); + +impl Put for Entry<'_> { + type Ok = (); + fn put(self, value: FfiValue) { + self.0.push((self.1, value)); + } +} + +/// The [`Decrypt`] sink of an [`FfiValue`]: builds the value a walk +/// describes. An 8- or 16-bit integer reads as the 32-bit variant of its +/// sign, which holds it exactly: a host with narrower integers (Go's `int8`, +/// say) sends them at their own width, and the value model has no variant +/// for them. A scalar no variant holds (a 128-bit integer, or a logical +/// type other than `Undefined`) is refused. +pub struct ValueSink(O); + +impl ValueSink { + /// A sink that puts the value it builds into `out`. + pub fn new(out: O) -> Self { + Self(out) + } +} + +/// What [`ValueSink`] refuses a scalar with. +const EXPECTED: &str = "a null, bool, int of at most 64 bits, float, string or bytes"; + +impl Sink for ValueSink { + type Ok = O::Ok; + type Error = DecodeError; + type Seq = ValueSeq; + type Map = ValueMap; + + fn scalar(self, ty: Option, value: Physical<'_>) -> Result { + let value = match (ty, value) { + (None, Physical::Null) => FfiValue::Null, + (Some(Logical::Undefined), Physical::Null) => FfiValue::Undefined, + (None, Physical::Bool(b)) => FfiValue::Bool(b), + (None, Physical::Int(Int::I8(i))) => FfiValue::Int32(i.into()), + (None, Physical::Int(Int::I16(i))) => FfiValue::Int32(i.into()), + (None, Physical::Int(Int::I32(i))) => FfiValue::Int32(i), + (None, Physical::Int(Int::I64(i))) => FfiValue::Int64(i), + (None, Physical::Int(Int::U8(u))) => FfiValue::UInt32(u.into()), + (None, Physical::Int(Int::U16(u))) => FfiValue::UInt32(u.into()), + (None, Physical::Int(Int::U32(u))) => FfiValue::UInt32(u), + (None, Physical::Int(Int::U64(u))) => FfiValue::UInt64(u), + (None, Physical::Float(Float::F32(f))) => FfiValue::Float32(f), + (None, Physical::Float(Float::F64(f))) => FfiValue::Float64(f), + (None, Physical::Text(text)) => FfiValue::String(text.into()), + (None, Physical::Bytes(bytes)) => FfiValue::Bytes(Protected::new(bytes.to_vec())), + _ => return Err(DecodeError::Expected(EXPECTED)), + }; + Ok(self.0.put(value)) + } + + fn seq(self, len: Option) -> Result, DecodeError> { + Ok(ValueSeq { + out: self.0, + items: Vec::with_capacity(capacity(len)), + }) + } + + fn map(self, len: Option) -> Result, DecodeError> { + Ok(ValueMap { + out: self.0, + entries: Vec::with_capacity(capacity(len)), + }) + } + + fn passthrough(self, value: P) -> Result { + let inner = value.visit(ValueSink(Root))?; + Ok(self.0.put(FfiValue::Passthrough(Box::new(inner)))) + } +} + +/// A length hint is the source's word, so it only seeds a capacity, and +/// a bounded one. +fn capacity(len: Option) -> usize { + len.unwrap_or(0).min(1024) +} + +/// The sequence sink of [`ValueSink`]. +pub struct ValueSeq { + out: O, + items: Vec, +} + +impl SeqSink for ValueSeq { + type Ok = O::Ok; + type Error = DecodeError; + type Element<'s> + = ValueSink> + where + Self: 's; + + fn element(&mut self) -> Result>, DecodeError> { + Ok(ValueSink(Element(&mut self.items))) + } + + fn end(self) -> Result { + Ok(self.out.put(FfiValue::Array(self.items))) + } +} + +/// The map sink of [`ValueSink`]. A repeated key is refused, as an object +/// holds each key once. +pub struct ValueMap { + out: O, + entries: Vec<(String, FfiValue)>, +} + +impl MapSink for ValueMap { + type Ok = O::Ok; + type Error = DecodeError; + type Entry<'s> + = ValueSink> + where + Self: 's; + + fn entry(&mut self, key: &str) -> Result>, DecodeError> { + if self.entries.iter().any(|(k, _)| k == key) { + return Err(DecodeError::duplicate_key(key)); + } + Ok(ValueSink(Entry(&mut self.entries, key.to_owned()))) + } + + fn end(self) -> Result { + Ok(self.out.put(FfiValue::Object(self.entries))) + } +} + +#[cfg(test)] +impl PartialEq for FfiValue { + /// Structural equality for tests only. Variable-time, which is why + /// this is `cfg(test)`. Floats compare by raw bit pattern so `-0.0` and + /// `NaN` payloads compare as they seal. + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (FfiValue::Null, FfiValue::Null) => true, + (FfiValue::Undefined, FfiValue::Undefined) => true, + (FfiValue::Bool(a), FfiValue::Bool(b)) => a == b, + (FfiValue::Int32(a), FfiValue::Int32(b)) => a == b, + (FfiValue::Int64(a), FfiValue::Int64(b)) => a == b, + (FfiValue::UInt32(a), FfiValue::UInt32(b)) => a == b, + (FfiValue::UInt64(a), FfiValue::UInt64(b)) => a == b, + (FfiValue::Float32(a), FfiValue::Float32(b)) => a.to_bits() == b.to_bits(), + (FfiValue::Float64(a), FfiValue::Float64(b)) => a.to_bits() == b.to_bits(), + (FfiValue::String(a), FfiValue::String(b)) => a.risky_ref() == b.risky_ref(), + (FfiValue::Bytes(a), FfiValue::Bytes(b)) => a.risky_ref() == b.risky_ref(), + (FfiValue::Array(a), FfiValue::Array(b)) => a == b, + (FfiValue::Object(a), FfiValue::Object(b)) => a == b, + (FfiValue::Passthrough(a), FfiValue::Passthrough(b)) => a == b, + _ => false, + } + } +} + +/// Names the variant and nothing more of a string or bytes: the value is +/// plaintext. +impl fmt::Debug for FfiValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + FfiValue::Null => f.write_str("Null"), + FfiValue::Undefined => f.write_str("Undefined"), + FfiValue::Bool(b) => write!(f, "Bool({b})"), + FfiValue::Int32(i) => write!(f, "Int32({i})"), + FfiValue::Int64(i) => write!(f, "Int64({i})"), + FfiValue::UInt32(u) => write!(f, "UInt32({u})"), + FfiValue::UInt64(u) => write!(f, "UInt64({u})"), + FfiValue::Float32(n) => write!(f, "Float32({n})"), + FfiValue::Float64(n) => write!(f, "Float64({n})"), + FfiValue::String(_) => f.write_str("String()"), + FfiValue::Bytes(_) => f.write_str("Bytes()"), + FfiValue::Array(items) => f.debug_tuple("Array").field(items).finish(), + FfiValue::Object(entries) => f.debug_tuple("Object").field(entries).finish(), + FfiValue::Passthrough(inner) => f.debug_tuple("Passthrough").field(inner).finish(), + } + } +} + +/// The kind of an [`FfiValue`]: its variant, without a payload. This is the +/// type vocabulary a plan field declares in its `"type"` key. +/// +/// `Null`, `Undefined` and `Passthrough` have no kind: each null is a type +/// with one value, so declaring a field as one says nothing, and a +/// passthrough is a transport choice, not a type. +/// +/// # Names are wire format +/// +/// [`name`](Self::name) (also its [`Display`](fmt::Display) and [`FromStr`] +/// form) is how a binding spells a kind. The names are frozen: matched +/// exactly, case-sensitively, and never renamed. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +pub enum ValueKind { + /// `"bool"`. + Bool, + /// `"int32"`. + Int32, + /// `"int64"`. + Int64, + /// `"uint32"`. + UInt32, + /// `"uint64"`. + UInt64, + /// `"float32"`. + Float32, + /// `"float64"`. + Float64, + /// `"string"`. + String, + /// `"bytes"`. + Bytes, + /// `"array"`. Says nothing about the elements' kinds. + Array, + /// `"object"`. Says nothing about the entries' kinds. + Object, +} + +impl ValueKind { + /// Every kind, in declaration order. + pub const ALL: [ValueKind; 11] = [ + ValueKind::Bool, + ValueKind::Int32, + ValueKind::Int64, + ValueKind::UInt32, + ValueKind::UInt64, + ValueKind::Float32, + ValueKind::Float64, + ValueKind::String, + ValueKind::Bytes, + ValueKind::Array, + ValueKind::Object, + ]; + + /// How a declaration spells this kind: `"bool"`, `"int32"`, `"int64"`, + /// `"uint32"`, `"uint64"`, `"float32"`, `"float64"`, `"string"`, + /// `"bytes"`, `"array"` or `"object"`. Frozen wire format. + pub const fn name(self) -> &'static str { + match self { + ValueKind::Bool => "bool", + ValueKind::Int32 => "int32", + ValueKind::Int64 => "int64", + ValueKind::UInt32 => "uint32", + ValueKind::UInt64 => "uint64", + ValueKind::Float32 => "float32", + ValueKind::Float64 => "float64", + ValueKind::String => "string", + ValueKind::Bytes => "bytes", + ValueKind::Array => "array", + ValueKind::Object => "object", + } + } + + /// Whether `value` is of this kind. Exact: an `Int64` is not held by + /// [`UInt64`](Self::UInt64), and a [`FfiValue::Passthrough`] is held by + /// no kind, whatever it wraps. + pub fn holds(self, value: &FfiValue) -> bool { + value.kind() == Some(self) + } +} + +impl fmt::Display for ValueKind { + /// Writes [`name`](Self::name). + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.name()) + } +} + +impl FromStr for ValueKind { + type Err = ParseValueKindError; + + /// Parses a [`name`](Self::name), exactly: `"uint64"`, not `"UInt64"`, + /// `"u64"` or `" uint64"`. `"null"` and `"undefined"` are not kinds. + fn from_str(name: &str) -> Result { + ValueKind::ALL + .into_iter() + .find(|kind| kind.name() == name) + .ok_or(ParseValueKindError) + } +} + +/// The error from parsing a string that is not a [`ValueKind::name`]. +/// Carries no detail: the caller holds the string it tried to parse. +#[derive(Clone, Copy, PartialEq, Eq, Debug, thiserror::Error)] +#[error("not a value kind")] +pub struct ParseValueKindError; + +impl FfiValue { + /// This value's [`ValueKind`], or `None` for [`Null`](FfiValue::Null), + /// [`Undefined`](FfiValue::Undefined) and + /// [`Passthrough`](FfiValue::Passthrough). + pub fn kind(&self) -> Option { + // Exhaustive on purpose: a new variant must decide its kind here. + match self { + FfiValue::Bool(_) => Some(ValueKind::Bool), + FfiValue::Int32(_) => Some(ValueKind::Int32), + FfiValue::Int64(_) => Some(ValueKind::Int64), + FfiValue::UInt32(_) => Some(ValueKind::UInt32), + FfiValue::UInt64(_) => Some(ValueKind::UInt64), + FfiValue::Float32(_) => Some(ValueKind::Float32), + FfiValue::Float64(_) => Some(ValueKind::Float64), + FfiValue::String(_) => Some(ValueKind::String), + FfiValue::Bytes(_) => Some(ValueKind::Bytes), + FfiValue::Array(_) => Some(ValueKind::Array), + FfiValue::Object(_) => Some(ValueKind::Object), + FfiValue::Null | FfiValue::Undefined | FfiValue::Passthrough(_) => None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn s(value: &str) -> FfiValue { + FfiValue::String(value.into()) + } + + fn every_variant() -> FfiValue { + FfiValue::Object(vec![ + ("n".to_string(), FfiValue::Null), + ("u".to_string(), FfiValue::Undefined), + ("b".to_string(), FfiValue::Bool(true)), + ("i32".to_string(), FfiValue::Int32(-3)), + ("i64".to_string(), FfiValue::Int64(-4)), + ("u32".to_string(), FfiValue::UInt32(34)), + ("u64".to_string(), FfiValue::UInt64(u64::MAX)), + ("f32".to_string(), FfiValue::Float32(-0.0)), + ("f64".to_string(), FfiValue::Float64(f64::NAN)), + ("s".to_string(), s("alice")), + ( + "bytes".to_string(), + FfiValue::Bytes(Protected::new(vec![1, 2, 3])), + ), + ( + "list".to_string(), + FfiValue::Array(vec![s("a"), FfiValue::Passthrough(Box::new(s("p")))]), + ), + ("empty".to_string(), FfiValue::Array(vec![])), + ]) + } + + #[test] + fn every_variant_survives_a_frame_round_trip() { + let frame = every_variant().to_frame().expect("frames"); + let back = FfiValue::from_frame(frame.risky_ref()).expect("reads back"); + assert_eq!(back, every_variant(), "the same value, variant for variant"); + } + + /// The width of an integer is part of the value: an `Int32` frames as a + /// 32-bit int and reads back as one, not as the 64-bit variant. + #[test] + fn integer_widths_are_kept_apart() { + for value in [ + FfiValue::Int32(7), + FfiValue::Int64(7), + FfiValue::UInt32(7), + FfiValue::UInt64(7), + ] { + let frame = value.to_frame().expect("frames"); + let back = FfiValue::from_frame(frame.risky_ref()).expect("reads back"); + assert_eq!(back.kind(), value.kind(), "{value:?} kept its width"); + } + } + + /// A narrower integer reads as the 32-bit variant of its sign, which + /// holds it exactly. + #[test] + fn a_narrower_integer_reads_as_the_32_bit_variant_of_its_sign() { + let read = |frame: Protected>| { + FfiValue::from_frame(frame.risky_ref()).expect("reads back") + }; + assert_eq!( + read(vitaminc_aead::to_frame(-8i8).expect("frames")), + FfiValue::Int32(-8) + ); + assert_eq!( + read(vitaminc_aead::to_frame(i16::MIN).expect("frames")), + FfiValue::Int32(i16::MIN.into()) + ); + assert_eq!( + read(vitaminc_aead::to_frame(u16::MAX).expect("frames")), + FfiValue::UInt32(u16::MAX.into()) + ); + } + + /// A frame holding a scalar no variant holds is refused, not read as + /// the nearest variant. + #[test] + fn a_scalar_no_variant_holds_is_refused() { + for frame in [ + vitaminc_aead::to_frame(7u128).expect("frames"), + vitaminc_aead::to_frame(-7i128).expect("frames"), + ] { + assert!(matches!( + FfiValue::from_frame(frame.risky_ref()), + Err(FromFrameError::Value(DecodeError::Expected(_))) + )); + } + } + + #[test] + fn bytes_that_are_not_one_frame_are_refused() { + assert!(matches!( + FfiValue::from_frame(&[]), + Err(FromFrameError::Frame(_)) + )); + let mut frame = FfiValue::Null.to_frame().expect("frames").risky_unwrap(); + frame.push(0); + assert!(matches!( + FfiValue::from_frame(&frame), + Err(FromFrameError::Frame(_)) + )); + } + + #[test] + fn debug_redacts_text_and_bytes() { + assert_eq!(format!("{:?}", s("secret")), "String()"); + assert_eq!( + format!("{:?}", FfiValue::Bytes(Protected::new(vec![1]))), + "Bytes()" + ); + assert_eq!(format!("{:?}", FfiValue::UInt32(34)), "UInt32(34)"); + } + + #[test] + fn kind_names_parse_back_exactly() { + for kind in ValueKind::ALL { + assert_eq!(kind.name().parse::(), Ok(kind)); + assert_eq!(kind.to_string(), kind.name()); + } + assert_eq!("UInt64".parse::(), Err(ParseValueKindError)); + assert_eq!("null".parse::(), Err(ParseValueKindError)); + assert!(ValueKind::UInt64.holds(&FfiValue::UInt64(34))); + assert!(!ValueKind::UInt64.holds(&FfiValue::Int64(34))); + assert_eq!(FfiValue::Passthrough(Box::new(s("p"))).kind(), None); + } +} diff --git a/packages/stack-encrypt/src/dynamic/kind.rs b/packages/stack-encrypt/src/dynamic/kind.rs index 1b49d3856..4d8ba4ade 100644 --- a/packages/stack-encrypt/src/dynamic/kind.rs +++ b/packages/stack-encrypt/src/dynamic/kind.rs @@ -6,13 +6,11 @@ //! JavaScript number is a float, and match is defined over text alone. So a //! plan field may say what its values are, in its `"type"` key. //! -//! The vocabulary is vitaminc's, not this crate's: a declared type is a -//! [`ValueKind`] (`vitaminc_aead_value::ValueKind`, re-exported here), whose +//! The vocabulary is [`ValueKind`], whose //! [`name`](ValueKind::name)s (`"uint64"`, `"string"`, …, `"array"`, -//! `"object"`) are frozen wire format beside vitaminc's tag table, and whose +//! `"object"`) are frozen wire format, and whose //! [`holds`](ValueKind::holds) and [`FfiValue::kind`] say whether a value is -//! of a kind. What vitaminc deliberately leaves to the declaring layer is the -//! two rules this module adds: +//! of a kind. Beside it, this module adds two rules: //! //! - [`admits`]: which indexes a kind is defined for, so a plan asking for //! match on an integer or equality on a float is refused when it is built; @@ -37,7 +35,7 @@ //! field has none is refused when it is built //! ([`Error::UntypedIndex`], naming the field). A field that only seals, or //! only carries its value through, may leave it out: no term derives from it. -use vitaminc_aead_value::{FfiValue, ValueKind}; +use super::{FfiValue, ValueKind}; use super::{Error, Reason}; use crate::target::IndexSpec; diff --git a/packages/stack-encrypt/src/dynamic/mod.rs b/packages/stack-encrypt/src/dynamic/mod.rs index 8466f6bc9..225b41a7e 100644 --- a/packages/stack-encrypt/src/dynamic/mod.rs +++ b/packages/stack-encrypt/src/dynamic/mod.rs @@ -1,7 +1,7 @@ //! Encrypting values whose type is known only at runtime. //! //! Everything else in this crate is typed: a target names its source type, -//! and the operations it composes are reached through bounds — `Encrypt` for +//! and the operations it composes are reached through bounds — `Plaintext` for //! a ciphertext, [`PrfValue`](vitaminc_prf::PrfValue) for an equality term, //! `AsRef` for a match term. That is what makes a term a cross-language //! contract: `equality_term(34u32)` derives the same bytes wherever it is @@ -13,8 +13,8 @@ //! the typed operation. This module is that something, written once here //! rather than once per language binding. //! -//! The runtime value is [`FfiValue`], vitaminc's language-neutral value tree -//! and the type every binding already funnels through. +//! The runtime value is [`FfiValue`], a language-neutral value tree and +//! the type every binding already funnels through. //! //! # What is here //! @@ -37,7 +37,7 @@ //! //! # What is not here //! -//! Encrypting a whole value is not: [`FfiValue`] implements `Encrypt` +//! Encrypting a whole value is not: [`FfiValue`] implements `Plaintext` //! already, so `keyset.encrypt(value, aad)` is the whole of it and needs //! nothing from this module. Nor is a second executor: nothing here calls //! the term functions or the seal path to produce a record. A capability a @@ -70,6 +70,7 @@ //! tells a binding author that. [`Scope`] is exhaustive for a different //! reason, given on the type. mod context; +mod ffi_value; mod kind; pub mod record; mod target; @@ -79,20 +80,22 @@ mod value; use std::fmt; pub use context::{borrowed, context}; +/// The language-neutral value tree — the runtime value every binding +/// funnels through — and the value kinds, the type a plan field declares +/// in its `"type"` key. A kind's names are frozen wire format; see +/// [`admits`] and [`read`] for what a kind means to this engine. A value +/// crosses a binding's boundary as a transport frame +/// ([`FfiValue::to_frame`], [`FfiValue::from_frame`]); everything else here +/// takes and returns values, never encoded bytes. +pub use ffi_value::{ + Element, Entry, FfiValue, FromFrameError, ParseValueKindError, Put, Root, Utf8String, + ValueKind, ValueMap, ValueSeq, ValueSink, +}; pub use kind::{admits, read}; pub use record::{FieldPlan, Output, Plan}; pub use target::{NoTargets, TargetDescriptor, TargetError, TargetResolver}; pub use term::{term, Scalar, TermBytes}; -pub use value::Value; -/// vitaminc's language-neutral value tree — the runtime value every binding -/// funnels through. Its transport codec is `vitaminc_aead_value::transport`, -/// which stays the binding's: this crate takes and returns values, never -/// encoded bytes. -pub use vitaminc_aead_value::FfiValue; -/// vitaminc's value kinds: the type a plan field declares in its `"type"` -/// key. Its names are frozen wire format; see [`admits`] and [`read`] for -/// what a kind means to this engine. -pub use vitaminc_aead_value::ValueKind; +pub use value::{AsValue, Value}; use crate::target::IndexSpec; use crate::{KeysetCipher, StackCipher}; @@ -137,7 +140,7 @@ impl fmt::Debug for Scope<'_, K> { /// The UTF-8 inside a string leaf. Valid by `Utf8String`'s construction /// invariant; checked rather than assumed because this is boundary code. -fn utf8(s: &vitaminc_aead_value::Utf8String) -> Option<&str> { +fn utf8(s: &Utf8String) -> Option<&str> { std::str::from_utf8(s.risky_ref()).ok() } diff --git a/packages/stack-encrypt/src/dynamic/record.rs b/packages/stack-encrypt/src/dynamic/record.rs index 64794dc87..43b4965a0 100644 --- a/packages/stack-encrypt/src/dynamic/record.rs +++ b/packages/stack-encrypt/src/dynamic/record.rs @@ -103,23 +103,13 @@ //! once checked — to the typed term operation, so they are the bytes a Rust //! `u32` or `String` field derives under the same label. //! -//! The ciphertext is where a data plan and a Rust chain over bare types part: -//! a Rust `u32` field seals four bare bytes and a `String` field its bare -//! UTF-8, a `Value` seals the tagged leaf, and the two encodings cannot be -//! told apart by inspection — a bare string that begins with U+000A is a -//! valid tagged string, and a tagged string is a valid bare one with a line -//! feed in front — so neither reader can refuse the other's leaf and a -//! mis-declared one may read as changed plaintext rather than fail. The -//! lowering therefore never chooses an encoding from the type. A Rust record -//! whose rows a binding must open declares its fields as [`Value`] -//! (`encrypt_index(pick("age", |u: &User| &u.age), (IndexSpec::Equality, -//! IndexSpec::Ore))` over a `Value` field is the same declaration as the -//! data plan's, and the two interchange, leaves included — see -//! `tests/record_lowering.rs`); a Rust `u32` field shares a data field's -//! terms and nothing else. Closing that gap — one leaf encoding both authors -//! read, or the encoding bound into the leaf's context so the wrong reader -//! fails closed — is a change to the Rust chain's bytes, and so a decision -//! tracked in #1118, not something the lowering can take on its own. +//! The ciphertext is where a data plan and a Rust chain meet too. Since +//! vitaminc ADR 0001 every leaf is tagged, a typed Rust value's as much as +//! a [`Value`]'s, so a Rust `u32` field and a `uint32` data field seal the +//! same leaf under the same context, and each opens the other's. A reader +//! of another type refuses a leaf rather than reading it as changed +//! plaintext: the tag is authenticated. The lowering still never chooses an +//! encoding from the type; it no longer has one to choose. //! //! # The stored record is wire format //! @@ -147,7 +137,7 @@ //! reported as a successful decrypt. The encrypt-side refusal is what makes //! that a round-trip invariant rather than data loss. -use vitaminc_aead_value::{FfiValue, ValueKind}; +use super::{FfiValue, ValueKind}; use vitaminc_protected::Controlled; use super::{ @@ -157,7 +147,7 @@ use super::{ use crate::plan::{FieldValues, FieldsBuilder, Opens, Runs}; use crate::target::{CallerContext, DeclaredContext, Decryption, Encrypted, IndexSpec}; use crate::{ - BoxedPassthrough, CipherText, ContextPiece, KeysetCipher, Label, NonEmpty, Pending, + CipherText, ContextPiece, KeysetCipher, Label, NonEmpty, PassthroughFrame, Pending, StackCipherText, }; @@ -1661,7 +1651,6 @@ impl RecordTree for StackCipherText { CipherText::Map(entries) => Children::Map(entries), CipherText::Passthrough(_) | CipherText::Single(_) - | CipherText::None(_) | CipherText::EmptySequence(_) | CipherText::EmptyMap(_) => Children::None, } @@ -1872,10 +1861,26 @@ fn slot(values: &mut FieldValues, name: &str) -> Result StackCipherText { - CipherText::Passthrough(Box::new(FfiValue::Bytes(vitaminc_protected::Protected::new( +fn term_node(term: TermBytes) -> Result { + passthrough(&FfiValue::Bytes(vitaminc_protected::Protected::new( term.into_bytes(), - ))) as BoxedPassthrough) + ))) +} + +/// A value as a passthrough node: the value's frame, in the clear. A value +/// the frame cannot carry (nested deeper than it allows) is refused as a +/// value vitaminc refuses to encrypt is, with [`Aead`](crate::Error::Aead). +fn passthrough(value: &FfiValue) -> Result { + let frame = value.to_frame().map_err(|_| crate::Error::Aead)?; + Ok(CipherText::Passthrough(PassthroughFrame( + frame.risky_unwrap(), + ))) +} + +/// The one value a passthrough node carries, or `None` if its frame holds +/// something no [`FfiValue`] holds. +fn passthrough_value(payload: &PassthroughFrame) -> Option { + FfiValue::from_frame(&payload.0).ok() } /// The record the plan produced, in the stored shape: per field, in plan @@ -1896,9 +1901,7 @@ fn shape_record( let value = eql.next().ok_or(crate::Error::ResponseShape)?; vec![( EQL_KEY.to_string(), - CipherText::Passthrough(Box::new(FfiValue::Bytes( - vitaminc_protected::Protected::new(value), - )) as BoxedPassthrough), + passthrough(&FfiValue::Bytes(vitaminc_protected::Protected::new(value)))?, )] } Verb::Encrypt => { @@ -1915,10 +1918,7 @@ fn shape_record( Verb::Index => keyed_terms(slot(&mut values, &field.name)?, &field.keys)?, Verb::Passthrough => { let value = take_leaf(&mut values, &field.name)?; - vec![( - "passthrough".to_string(), - CipherText::Passthrough(Box::new(value) as BoxedPassthrough), - )] + vec![("passthrough".to_string(), passthrough(&value)?)] } }; fields.push((field.name.clone(), CipherText::Map(outputs))); @@ -1936,11 +1936,10 @@ fn keyed_terms( if terms.len() != keys.len() { return Err(crate::Error::ResponseShape); } - Ok(keys - .iter() + keys.iter() .zip(terms) - .map(|(key, term)| ((*key).to_string(), term_node(term))) - .collect()) + .map(|(key, term)| Ok(((*key).to_string(), term_node(term)?))) + .collect() } /// The field `name` out of the record, as the value it holds. @@ -2035,9 +2034,8 @@ fn record_row(mut row: Vec<(String, StackCipherText)>, plan: &Plan) -> Result() - .map_err(|_| misfit(Reason::NotPassthrough))?; + let value = + passthrough_value(&payload).ok_or_else(|| misfit(Reason::NotPassthrough))?; let FfiValue::Bytes(bytes) = value else { return Err(misfit(Reason::NotPassthrough)); }; @@ -2049,9 +2047,8 @@ fn record_row(mut row: Vec<(String, StackCipherText)>, plan: &Plan) -> Result() - .map_err(|_| misfit(Reason::NotPassthrough))?; + let value = + passthrough_value(&payload).ok_or_else(|| misfit(Reason::NotPassthrough))?; if field.field_type.is_some_and(|kind| !kind.holds(&value)) { return Err(misfit(Reason::FieldType)); } @@ -2274,7 +2271,7 @@ mod tests { fn term_bytes(node: &StackCipherText) -> Vec { match node { - CipherText::Passthrough(payload) => match (**payload).downcast_ref::() { + CipherText::Passthrough(payload) => match passthrough_value(payload) { Some(FfiValue::Bytes(bytes)) => bytes.risky_ref().to_vec(), _ => panic!("a term rides as a passthrough byte node"), }, @@ -2304,7 +2301,7 @@ mod tests { } fn forged(value: FfiValue) -> StackCipherText { - CipherText::Passthrough(Box::new(value) as BoxedPassthrough) + passthrough(&value).unwrap() } fn plan_error(error: crate::Error) -> PlanError { @@ -3199,7 +3196,7 @@ mod tests { panic!("a passthrough field rides as a passthrough node"); }; assert_eq!( - u64_of(payload.downcast_ref::().expect("a value")), + u64_of(&passthrough_value(&payload).expect("a value")), 7, "carrying the value as it is" ); @@ -4256,7 +4253,7 @@ mod tests { let CipherText::Passthrough(payload) = node(&mut tenant, "passthrough") else { panic!("the context field rides as a passthrough"); }; - let FfiValue::String(stored) = *payload.downcast::().expect("a value") else { + let FfiValue::String(stored) = passthrough_value(&payload).expect("a value") else { panic!("the stored context is text"); }; assert_eq!(utf8(&stored), Some("tenants/acme")); @@ -4811,7 +4808,7 @@ mod tests { .expect("the shape fits") .await; assert_eq!( - plan_error(result.err().expect("refused")), + plan_error(result.expect_err("refused")), PlanError::FieldType { field: "age".into(), expected: "int64", @@ -4843,7 +4840,7 @@ mod tests { .expect("the shape fits") .await; assert_eq!( - plan_error(result.err().expect("the batch is refused")), + plan_error(result.expect_err("the batch is refused")), PlanError::FieldType { field: "age".into(), expected: "uint64", @@ -5321,7 +5318,7 @@ mod tests { let CipherText::Passthrough(payload) = node(&mut outputs, EQL_KEY) else { panic!("the eql node is a passthrough"); }; - let FfiValue::Bytes(bytes) = *payload.downcast::().unwrap() else { + let FfiValue::Bytes(bytes) = passthrough_value(&payload).unwrap() else { panic!("the eql node carries bytes"); }; serde_json::from_slice(bytes.risky_ref()).expect("the eql node is JSON") @@ -5916,9 +5913,7 @@ mod tests { let bytes_node = |bytes: &[u8]| { CipherText::Map(vec![( EQL_KEY.to_string(), - CipherText::Passthrough(Box::new(FfiValue::Bytes(Protected::new( - bytes.to_vec(), - ))) as BoxedPassthrough), + passthrough(&FfiValue::Bytes(Protected::new(bytes.to_vec()))).unwrap(), )]) }; // Each misfit is refused naming the field and the reason a @@ -5954,7 +5949,7 @@ mod tests { // A payload that is not bytes. let null = CipherText::Map(vec![( EQL_KEY.to_string(), - CipherText::Passthrough(Box::new(FfiValue::Null) as BoxedPassthrough), + passthrough(&FfiValue::Null).unwrap(), )]); let record = with_email(seal_mixed(&keyset, &plan).await, null); assert_eq!( @@ -6053,9 +6048,7 @@ mod tests { "email".to_string(), CipherText::Map(vec![( EQL_KEY.to_string(), - CipherText::Passthrough(Box::new(FfiValue::Bytes(Protected::new( - b"{}".to_vec(), - ))) as BoxedPassthrough), + passthrough(&FfiValue::Bytes(Protected::new(b"{}".to_vec()))).unwrap(), )]), )]); let error = target_error(refused(decrypt_with( @@ -6074,11 +6067,10 @@ mod tests { } /// The leaf encoding, pinned: every field lowered from data seals the - /// tagged `Value` leaf whatever its `"type"`, so a type declared later - /// changes no bytes; a Rust field of a bare type shares a data field's - /// terms and not its leaf, and the two leaves cannot be told apart by - /// inspection, which is why the lowering never picks an encoding from - /// the type. + /// tagged leaf its value seals as, whatever its `"type"`, so a type + /// declared later changes no bytes; and a Rust field of the same type + /// seals the same leaf, so a derived record and a data plan share + /// terms and leaves. mod given_the_leaf_encoding { use super::*; @@ -6090,8 +6082,8 @@ mod tests { } /// A `uint32` field and a `string` field seal the tagged leaf, as an - /// untyped field does: the leaf opens as a value, and a bare `u32` - /// reader refuses the five bytes it finds. + /// untyped field does: the leaf opens as a value, and as the bare + /// Rust type of the same kind. #[tokio::test] async fn every_field_seals_the_tagged_leaf_whatever_its_type() { let cipher = cipher().await; @@ -6116,9 +6108,15 @@ mod tests { let mut age = map(node(&mut fields, "age")); let as_u32: Result = cipher.decrypt(node(&mut age, "c"), age_label.clone()).await; + assert_eq!(as_u32.ok(), Some(34), "a uint32 leaf is a u32, type {ty:?}"); + let mut fields = + map(seal(&keyset, obj(vec![("age", FfiValue::UInt32(34))]), &plan).await); + let mut age = map(node(&mut fields, "age")); + let as_string: Result = + cipher.decrypt(node(&mut age, "c"), age_label.clone()).await; assert!( - matches!(as_u32, Err(crate::Error::Aead)), - "five tagged bytes are not a bare u32, type {ty:?}: {as_u32:?}" + matches!(as_string, Err(crate::Error::Aead)), + "a uint32 leaf is not text, type {ty:?}: {as_string:?}" ); } let plan = plan(obj(vec![( @@ -6181,14 +6179,10 @@ mod tests { } /// A derived `plaintext = u32` record and a `uint32` data-plan field - /// under one label derive the same term. Their leaves are different - /// encodings, and neither reader opens the other's: the derive's bare - /// four bytes carry no tag for the lowering, the lowering's five - /// tagged bytes are not a `u32`. A Rust record that must interchange - /// leaves with a data plan declares `Value` fields instead - /// (`seals_what_the_chain_seals_under_the_same_declaration`). + /// under one label derive the same term and seal the same leaf, so + /// each reader opens the other's. #[tokio::test] - async fn the_derive_and_a_data_plan_share_terms_not_leaves() { + async fn the_derive_and_a_data_plan_share_terms_and_leaves() { let cipher = cipher().await; let keyset = cipher.default_keyset(); let label_ = Label::parse("users/age").expect("label"); @@ -6211,37 +6205,29 @@ mod tests { "the same equality term" ); - let as_u32: Result = keyset + let as_u32: u32 = keyset .decrypt_as( node(&mut age, "c"), AeadContext::from(NonEmpty::from(label_.clone())), ) - .await; - assert!( - matches!(as_u32, Err(crate::Error::Aead)), - "the derive does not open the lowering's leaf: {as_u32:?}" - ); + .await + .expect("the derive opens the lowering's leaf"); + assert_eq!(as_u32, 34); let stored = CipherText::Map(vec![( "age".to_string(), CipherText::Map(vec![("c".to_string(), derived.c)]), )]); - let result = decrypt(Scope::Client(&cipher), stored, &plan, None) + let opened = decrypt(Scope::Client(&cipher), stored, &plan, None) .expect("the shape fits") - .await; - assert!( - matches!(result, Err(crate::Error::Aead)), - "the lowering does not open the derive's leaf: {:?}", - result.err() - ); + .await + .expect("the lowering opens the derive's leaf"); + assert_eq!(u32_of(&object(opened)[0].1), 34); } - /// The two encodings cannot be told apart by inspection: a bare - /// `String` reader accepts a tagged string leaf, reading the tag as - /// a line feed. Pinned so that it is known, never relied on: the - /// lowering reads only its own encoding, and a Rust record that - /// shares rows with a binding declares `Value` fields. + /// A bare `String` reader opens a data plan's string leaf to the + /// text it holds: the tag is the leaf's type, not part of the text. #[tokio::test] - async fn a_bare_string_reader_cannot_tell_a_tagged_leaf_apart() { + async fn a_bare_string_reader_reads_a_data_plan_string() { let cipher = cipher().await; let keyset = cipher.default_keyset(); let plan = plan(obj(vec![("email", spec(label("email"), &["c"]))])).expect("plan"); @@ -6253,8 +6239,8 @@ mod tests { Label::parse("users/email").expect("label"), ) .await - .expect("valid UTF-8 either way"); - assert_eq!(bare, "\nalice", "the string tag, U+000A, read as text"); + .expect("a string leaf"); + assert_eq!(bare, "alice"); } } diff --git a/packages/stack-encrypt/src/dynamic/target.rs b/packages/stack-encrypt/src/dynamic/target.rs index d533edc43..fe46d87ba 100644 --- a/packages/stack-encrypt/src/dynamic/target.rs +++ b/packages/stack-encrypt/src/dynamic/target.rs @@ -27,7 +27,7 @@ use std::fmt; -use vitaminc_aead_value::{FfiValue, ValueKind}; +use super::{FfiValue, ValueKind}; use crate::{KeysetCipher, Label, Pending, StackCipher}; diff --git a/packages/stack-encrypt/src/dynamic/term.rs b/packages/stack-encrypt/src/dynamic/term.rs index 9d5670e18..de0b79852 100644 --- a/packages/stack-encrypt/src/dynamic/term.rs +++ b/packages/stack-encrypt/src/dynamic/term.rs @@ -14,8 +14,8 @@ //! going to be **stored** should come from the record path, where it shares //! one context with the ciphertext beside it (ADR-0004). +use super::FfiValue; use stack_kms::DataKeySource; -use vitaminc_aead_value::FfiValue; use vitaminc_protected::{Controlled, OpaqueDebug, Protected}; use zeroize::Zeroizing; diff --git a/packages/stack-encrypt/src/dynamic/value.rs b/packages/stack-encrypt/src/dynamic/value.rs index a0b54a5c0..e632d892e 100644 --- a/packages/stack-encrypt/src/dynamic/value.rs +++ b/packages/stack-encrypt/src/dynamic/value.rs @@ -2,10 +2,12 @@ use std::fmt; -use vitaminc_aead::{Cipher, Decipher, Decrypt, Encrypt, IntoAad}; -use vitaminc_aead_value::FfiValue; +use vitaminc_aead::plaintext::Sink; +use vitaminc_aead::{Decrypt, Plaintext}; use vitaminc_protected::{Controlled, Protected}; +use super::{FfiValue, Put, ValueSink}; + use crate::plan::IntoLabel; use crate::{Label, LabelError}; @@ -24,11 +26,9 @@ use crate::{Label, LabelError}; /// each once per operation that consumes it, as the derive's does. /// /// It seals and opens as the [`FfiValue`] it wraps, in vitaminc's -/// self-describing tagged leaf encoding — a different leaf from a bare -/// `u32`'s or `String`'s, and one the other reader cannot tell apart by -/// inspection; [`dynamic::record`](super::record) says what follows from -/// that. Its `Debug` names the type and nothing else: the value is -/// plaintext. +/// self-describing tagged leaf encoding — the same leaf a bare `u32` or +/// `String` of the same value seals as. Its `Debug` names the type and +/// nothing else: the value is plaintext. pub struct Value(FfiValue); impl Value { @@ -105,23 +105,33 @@ fn duplicate(value: &FfiValue) -> FfiValue { } } -impl Encrypt for Value { - fn encrypt_with_aad<'a, C, A>(self, cipher: C, aad: A) -> Result - where - C: Cipher, - A: IntoAad<'a>, - { - self.0.encrypt_with_aad(cipher, aad) +impl Plaintext for Value { + fn visit(self, sink: S) -> Result { + self.0.visit(sink) + } +} + +impl Plaintext for &Value { + fn visit(self, sink: S) -> Result { + (&self.0).visit(sink) } } -impl<'c> Decrypt<'c> for Value { - fn decrypt_with_aad<'a, D, A>(decipher: D, aad: A) -> D::Ok - where - D: Decipher<'c>, - A: IntoAad<'a>, - { - D::map_ok(FfiValue::decrypt_with_aad(decipher, aad), Value) +impl Decrypt for Value { + type Sink = ValueSink; + fn sink() -> Self::Sink { + ValueSink::new(AsValue) + } +} + +/// Where [`Value`]'s [`Decrypt`] sink puts what it builds: the +/// [`FfiValue`] sink's value, wrapped. +pub struct AsValue; + +impl Put for AsValue { + type Ok = Value; + fn put(self, value: FfiValue) -> Value { + Value(value) } } diff --git a/packages/stack-encrypt/src/lib.rs b/packages/stack-encrypt/src/lib.rs index 65059a6ae..264699881 100644 --- a/packages/stack-encrypt/src/lib.rs +++ b/packages/stack-encrypt/src/lib.rs @@ -26,9 +26,10 @@ //! the cipher bound to one of that client's keysets, from //! [`default_keyset`](StackCipher::default_keyset) or //! [`keyset`](StackCipher::keyset) — encrypts any value that implements -//! [`Encrypt`] (`String`, `Vec`, `HashMap`, `Option`, -//! `Protected`, your own types, and any nesting of them). Either cipher -//! decrypts back into any [`Decrypt`] type. Every scalar inside the value is +//! vitaminc's [`Plaintext`] (`String`, `Vec`, `HashMap`, +//! `Option`, `Protected`, your own types through +//! `#[derive(vitaminc_aead::Plaintext)]`, and any nesting of them). Either +//! cipher decrypts back into any [`Decrypt`] type. Every scalar inside the value is //! sealed under its **own** ZeroKMS data key, so each value access is an //! individually auditable key retrieval — there is no long-lived key in your //! process. @@ -225,8 +226,9 @@ endpoint — are `StackKmsBuilder`'s, and the two keyset-cache knobs are //! //! Besides your AAD, the *shape* of a value is authenticated: an element cannot //! be spliced out of a sequence and passed off as a scalar, a map value cannot -//! be moved under a different key, and "absent" / "empty" are themselves -//! sealed markers rather than inferable from structure. A tampered or +//! be moved under a different key, "empty" is itself a sealed marker rather +//! than inferable from structure, and "absent" is a sealed null leaf. Every +//! leaf records its own type, so a leaf cannot be read back as another. A tampered or //! re-homed ciphertext fails with [`Error::Aead`]; a failed or denied key //! retrieval surfaces as [`Error::Kms`]. A ciphertext opened under the //! *wrong context* is refused by ZeroKMS first: every data key is bound to @@ -236,18 +238,14 @@ endpoint — are `StackKmsBuilder`'s, and the two keyset-cache knobs are //! source that ignores descriptors (`FakeDataKeySource`, in tests) lets a //! wrong context reach the AEAD, where it is [`Error::Aead`]. //! -//! For one-row reads of a batch-encrypted collection, decrypt as -//! [`Element`](Element) under the same AAD used for the whole collection: -//! the derivation that binds an element to its position is applied by the -//! type, not by the caller. That is the general rule here — every leaf's AAD -//! is derived from the context its key was minted under, and there is no -//! entry point that lets a caller supply one of its own. +//! Every leaf's AAD is derived from the context its key was minted under, +//! and there is no entry point that lets a caller supply one of its own. //! //! # Relationship to vitaminc //! -//! A `KeysetCipher` is a vitaminc [`Cipher`]; everything a vitaminc cipher can -//! encrypt, it can encrypt, and the AEAD, AAD derivations and leaf wire format -//! are vitaminc's (`vitaminc_encrypt::Aes256Cipher`, AES-256-GCM under a random +//! A `KeysetCipher` seals through a vitaminc `Cipher`; everything vitaminc +//! can encrypt, it can encrypt, and the data model, the AAD derivations and +//! the leaf wire format are vitaminc's (`vitaminc_encrypt::Aes256Cipher`, AES-256-GCM under a random //! per-leaf nonce vitaminc generates itself). The ZeroKMS `iv` a [`SealedValue`] //! carries is *not* that nonce: it identifies the data key, and is sent back to //! ZeroKMS with the key `tag` to re-derive it — under the same [`Descriptor`] @@ -283,8 +281,8 @@ pub mod sem; pub mod target; pub use cipher::{ - BoxedPassthrough, Error, FromEnv, LeafBytesError, PendingStackCipherText, SealedValue, - StackCipher, StackCipherBuilder, StackCipherText, StackDecipher, + Error, FromEnv, LeafBytesError, PendingStackCipherText, SealedValue, StackCipher, + StackCipherBuilder, StackCipherText, }; pub use descriptor::{Describe, Description, Descriptor, Label, LabelError}; pub use keyset::KeysetCipher; @@ -307,13 +305,14 @@ pub use target::{ }; // Re-export the vitaminc AEAD surface callers need to drive the cipher, so they -// don't have to depend on `vitaminc-aead` directly for the common path. A -// context type of your own implements `IntoContext` once; `IntoAad` and -// `IntoPrfContext` are vitaminc's blankets over it, so both derivations see -// the same bytes. +// don't have to depend on `vitaminc-aead` directly for the common path: a +// value describes itself once (`Plaintext`) and names the sink that reads it +// back (`Decrypt`). A context type of your own implements `IntoContext` once; +// `IntoAad` and `IntoPrfContext` are vitaminc's blankets over it, so both +// derivations see the same bytes. pub use vitaminc_aead::{ - Cipher, CipherText, Context, ContextPiece, ContextTag, Decipher, Decrypt, Element, Encrypt, - IntoAad, IntoContext, Unspecified, + CipherText, Context, ContextPiece, ContextTag, Decrypt, IntoAad, IntoContext, Node, + PassthroughFrame, Plaintext, Unspecified, }; // The vitaminc 0.4 names, deprecated there; re-exported for one transition so // a caller that spelled them keeps compiling with a warning. diff --git a/packages/stack-encrypt/src/plan/build.rs b/packages/stack-encrypt/src/plan/build.rs index 5ecc20d1e..b095d3ebf 100644 --- a/packages/stack-encrypt/src/plan/build.rs +++ b/packages/stack-encrypt/src/plan/build.rs @@ -603,7 +603,7 @@ fn stored(field: &str, slot: Slot) -> Result { /// Open a sealed field's ciphertext as an `F`. fn open_sealed(ciphertext: StackCipherText, context: CallerContext) -> Decryption where - F: crate::Decrypt<'static> + Send + 'static, + F: crate::Decrypt + Send + 'static, K: 'static, { open::(ciphertext, AeadContext::from(context)).map(Slot::new) @@ -727,7 +727,7 @@ impl FieldsBuilder { /// `/`. Lowers to [`ciphertext`]`().under(label)`. pub fn encrypt(self, field: impl FieldRef) -> Self where - F: crate::Encrypt + crate::Decrypt<'static> + Clone + Send + 'static, + F: crate::Plaintext + crate::Decrypt + Clone + Send + 'static, { let (name, access) = resolve(field); let read = Arc::clone(&access.read); @@ -764,14 +764,14 @@ impl FieldsBuilder { indexes: impl Indexes + Clone + Send + Sync + 'static, ) -> Self where - F: crate::Encrypt + crate::Decrypt<'static> + Clone + Send + 'static, + F: crate::Plaintext + crate::Decrypt + Clone + Send + 'static, { self.encrypt_index_of(field, indexes) } fn encrypt_index_of(self, field: impl FieldRef, indexes: X) -> Self where - F: crate::Encrypt + crate::Decrypt<'static> + Clone + Send + 'static, + F: crate::Plaintext + crate::Decrypt + Clone + Send + 'static, X: Indexes + Clone + Send + Sync + 'static, X::Terms: Send, { diff --git a/packages/stack-encrypt/src/plan/chain.rs b/packages/stack-encrypt/src/plan/chain.rs index 5e34bc999..8a197668e 100644 --- a/packages/stack-encrypt/src/plan/chain.rs +++ b/packages/stack-encrypt/src/plan/chain.rs @@ -406,7 +406,7 @@ impl<'a, S: ?Sized, K: 'static> EncryptWithContext<'a, S, K> { impl<'a, S, K: 'static> Operation<'a, K> for EncryptWithContext<'a, S, K> where - S: crate::Encrypt + Clone, + S: crate::Plaintext + Clone, { type Output = StackCipherText; fn cipher(&self) -> &'a StackCipher { @@ -445,7 +445,7 @@ pub struct EncryptIndexed<'a, S, X, K> { impl<'a, S, X, K: 'static> Operation<'a, K> for EncryptIndexed<'a, S, X, K> where - S: crate::Encrypt + Clone, + S: crate::Plaintext + Clone, X: Indexes + Clone, X::Terms: MaybeSend, { @@ -483,7 +483,7 @@ impl<'a, S: 'static, K: 'static> EncryptFields<'a, S, K> { /// [`FieldsBuilder::encrypt`]. pub fn encrypt(mut self, field: impl FieldRef) -> Self where - F: crate::Encrypt + crate::Decrypt<'static> + Clone + Send + 'static, + F: crate::Plaintext + crate::Decrypt + Clone + Send + 'static, { self.plan = self.plan.encrypt::(field); self @@ -495,7 +495,7 @@ impl<'a, S: 'static, K: 'static> EncryptFields<'a, S, K> { indexes: impl Indexes + Clone + Send + Sync + 'static, ) -> Self where - F: crate::Encrypt + crate::Decrypt<'static> + Clone + Send + 'static, + F: crate::Plaintext + crate::Decrypt + Clone + Send + 'static, { self.plan = self.plan.encrypt_index::(field, indexes); self diff --git a/packages/stack-encrypt/src/plan/mod.rs b/packages/stack-encrypt/src/plan/mod.rs index 623d3884d..b91f6a0a5 100644 --- a/packages/stack-encrypt/src/plan/mod.rs +++ b/packages/stack-encrypt/src/plan/mod.rs @@ -273,15 +273,10 @@ //! sources only the call has no data form: a binding names the context in //! the declaration it sends, or in the record. //! -//! The engine is shared; the stored leaf is not. A data plan seals every -//! field as the tagged `dynamic::Value` leaf, whatever its type, so a Rust -//! record whose rows a binding also reads must declare those fields as -//! `dynamic::Value`. A bare `String` or `u32` field derives the same terms -//! but encrypts a different leaf. A bare `String` reader accepts a -//! binding's leaf and returns its type tag as a leading line feed -//! (`"\nalice"`), with no error. One leaf encoding for both, or the -//! encoding bound into the leaf's context so the wrong reader fails, is -//! tracked in #1118. +//! The engine is shared, and so is the stored leaf: every leaf is tagged +//! with its type (vitaminc ADR 0001), so a data plan's `uint32` field and a +//! Rust `u32` field seal the same leaf under the same context, and a reader +//! of another type refuses it rather than misreading it. //! //! # How a chain lowers //! diff --git a/packages/stack-encrypt/src/plan/value.rs b/packages/stack-encrypt/src/plan/value.rs index 9ff54b49f..03d6e024b 100644 --- a/packages/stack-encrypt/src/plan/value.rs +++ b/packages/stack-encrypt/src/plan/value.rs @@ -356,7 +356,7 @@ pub trait ValueShape: ValueLayout { impl ValueLayout for Indexed where - S: crate::Encrypt + Clone, + S: crate::Plaintext + Clone, X: Indexes + Clone, { type Output = Encrypted; @@ -367,7 +367,7 @@ where impl ValueShape for Indexed where - S: crate::Encrypt + Clone, + S: crate::Plaintext + Clone, X: Indexes + Clone, { fn lower<'s, K: 'static>(&self) -> Encryption<'s, S, Self::Output, K, CallerContext> @@ -660,7 +660,7 @@ macro_rules! indexed_opens { ($([$($generics:tt)*] $record:ty;)+) => {$( impl Opens<$record, K> for ValuePlan> where - S: crate::Decrypt<'static> + 'static, + S: crate::Decrypt + Send + 'static, K: 'static, { type Output = S; @@ -678,7 +678,7 @@ macro_rules! indexed_opens { } impl Opens, K> for ValuePlan> where - S: crate::Decrypt<'static> + 'static, + S: crate::Decrypt + Send + 'static, K: 'static, { type Output = Vec; diff --git a/packages/stack-encrypt/src/target/core.rs b/packages/stack-encrypt/src/target/core.rs index 7b76831bd..63c3c3921 100644 --- a/packages/stack-encrypt/src/target/core.rs +++ b/packages/stack-encrypt/src/target/core.rs @@ -1,8 +1,8 @@ //! Canonical execution shared by the cipher-directed and declaration APIs. use super::{CipherScope, Pending, Request, Responses}; -use crate::cipher::{bind_keys, PendingStackCipherText, StackDecipher}; +use crate::cipher::{bind_keys, open_keyed, KeyedCipherText, PendingStackCipherText}; use crate::{Descriptor, Error, KeysetCipher, StackCipher, StackCipherText}; -use vitaminc_aead::{CipherText, Decrypt, Encrypt, IntoAad, IntoContext}; +use vitaminc_aead::{CipherText, Decrypt, IntoAad, IntoContext, Plaintext}; use vitaminc_protected::NonEmpty; /// Internal term operation. It is deliberately inaccessible to target authors. @@ -21,10 +21,12 @@ pub(crate) trait Term: Sized { } /// Seal `source` into the native tree. It is consumed, as Vitamin C's -/// `Encrypt` consumes it: a caller holding only a borrow clones before it -/// gets here (see [`ConsumeSource`](super::ConsumeSource)), and one holding -/// the value hands it over without a copy. -pub(crate) fn encrypt_native<'a, 'c, S: Encrypt, K, T: IntoContext<'c>>( +/// by-value [`Plaintext`] walk consumes it: a caller holding only a borrow +/// clones before it gets here (see [`ConsumeSource`](super::ConsumeSource)), +/// and one holding the value hands it over without a copy. The walk happens +/// here, before any request exists, so the pending holds only the planned +/// leaves. +pub(crate) fn encrypt_native<'a, 'c, S: Plaintext, K, T: IntoContext<'c>>( source: S, cipher: &'a KeysetCipher<'_, K>, context: NonEmpty, @@ -35,12 +37,12 @@ pub(crate) fn encrypt_native<'a, 'c, S: Encrypt, K, T: IntoContext<'c>>( return Pending::failed(cipher, error); } let aad = context.into_aad().into_owned(); - match source.encrypt_with_aad(cipher, aad) { + match PendingStackCipherText::plan(source, aad) { Ok(tree) => seal_pending(cipher, tree, descriptor), - Err(_) => Pending::ready(cipher, Err(Error::Aead)), + Err(error) => Pending::ready(cipher, Err(error)), } } -pub(crate) fn open_native<'a, 'c, P: Decrypt<'static> + 'static, K, T: IntoContext<'c>>( +pub(crate) fn open_native<'a, 'c, P: Decrypt + Send + 'static, K, T: IntoContext<'c>>( tree: StackCipherText, cipher: &'a StackCipher, context: NonEmpty, @@ -54,8 +56,8 @@ pub(crate) fn open_native<'a, 'c, P: Decrypt<'static> + 'static, K, T: IntoConte let requests = retrieve_requests(&tree, &descriptor); let aad = context.into_aad().into_owned(); Pending::request(cipher, requests, move |responses| { - let decipher = decipher_from_responses(tree, responses)?; - P::decrypt_with_aad(decipher, aad).map_err(Error::from) + let keyed = keyed_from_responses(tree, responses)?; + open_keyed(keyed, aad) }) } pub(crate) fn seal_pending<'a, K>( @@ -74,30 +76,32 @@ pub(crate) fn seal_pending<'a, K>( .take(tree.key_count()) .collect(); Pending::request(cipher, requests, move |responses| { - let mut keys = responses.drain_generated(); - tree.seal_with(keyset_id, &mut keys).map_err(Error::from) + tree.seal_with(keyset_id, responses.drain_generated()) + .map_err(Error::from) }) } -pub(crate) fn decipher_pending<'a, K>( +/// Retrieve every leaf's data key and bind it onto its leaf, ready for +/// [`open_keyed`](crate::cipher::open_keyed). +pub(crate) fn bind_pending<'a, K>( scope: impl CipherScope<'a, K>, ciphertext: StackCipherText, descriptor: Descriptor, -) -> Pending<'a, StackDecipher, K> { +) -> Pending<'a, KeyedCipherText, K> { // Fast path, as in `seal_pending`; `dispatch` is the gate. if let Err(e) = descriptor.check() { return Pending::ready(scope, Err(e)); } let requests = retrieve_requests(&ciphertext, &descriptor); Pending::request(scope, requests, move |responses| { - decipher_from_responses(ciphertext, responses) + keyed_from_responses(ciphertext, responses) }) } -fn decipher_from_responses( +fn keyed_from_responses( ciphertext: StackCipherText, responses: &mut Responses, -) -> Result { +) -> Result { let mut keys = responses.drain_retrieved(); // Too few keys for the tree, or keys left over once it is bound, both // mean `retrieve_requests` and `bind_keys` disagreed about the tree's @@ -107,7 +111,7 @@ fn decipher_from_responses( if keys.next().is_some() { return Err(Error::ResponseShape); } - Ok(StackDecipher::over(keyed)) + Ok(keyed) } fn retrieve_requests(ciphertext: &StackCipherText, descriptor: &Descriptor) -> Vec { @@ -122,10 +126,7 @@ fn collect_retrieve_requests( out: &mut Vec, ) { match ciphertext { - CipherText::Single(leaf) - | CipherText::None(leaf) - | CipherText::EmptySequence(leaf) - | CipherText::EmptyMap(leaf) => { + CipherText::Single(leaf) | CipherText::EmptySequence(leaf) | CipherText::EmptyMap(leaf) => { out.push(Request::retrieve_under( *leaf.iv(), leaf.tag().to_vec(), diff --git a/packages/stack-encrypt/src/target/index.rs b/packages/stack-encrypt/src/target/index.rs index 7006f16a8..63db8d400 100644 --- a/packages/stack-encrypt/src/target/index.rs +++ b/packages/stack-encrypt/src/target/index.rs @@ -493,7 +493,7 @@ pub struct Encrypted { pub terms: Terms, } -impl + 'static, Terms> DecryptInto

for Encrypted { +impl DecryptInto

for Encrypted { type Context = AeadContext; fn decryption(self, context: Self::Context) -> Decryption { open(self.ciphertext, context) @@ -507,7 +507,7 @@ impl + 'static, Terms> DecryptInto

for Encrypted EncryptFrom for Encrypted where - S: crate::Encrypt + Clone, + S: crate::Plaintext + Clone, Terms: TermSet, { type Context = CallerContext; @@ -621,7 +621,7 @@ pub fn indexed<'s, S, K, M, X>( indexes: X, ) -> Encryption<'s, S, Encrypted, K, CallerContext, M> where - S: crate::Encrypt + 's, + S: crate::Plaintext + 's, K: 'static, M: ConsumeSource<'s, S> + ShareSource<'s, S>, X: Indexes, diff --git a/packages/stack-encrypt/src/target/mod.rs b/packages/stack-encrypt/src/target/mod.rs index 565e370d1..349cc34ce 100644 --- a/packages/stack-encrypt/src/target/mod.rs +++ b/packages/stack-encrypt/src/target/mod.rs @@ -95,7 +95,7 @@ mod source; pub mod transcode; mod tuples; -pub(crate) use self::core::{decipher_pending, seal_pending}; +pub(crate) use self::core::{bind_pending, seal_pending}; pub use context::{AeadContext, CallerContext, DeclaredContext, ExpectedContext, Extends}; pub use index::{ indexed, At, Encrypted, Equality, Index, IndexSpec, Indexes, Match, Ope, Ore, Select, TermSet, diff --git a/packages/stack-encrypt/src/target/operations.rs b/packages/stack-encrypt/src/target/operations.rs index ce772f9a9..eae1d175c 100644 --- a/packages/stack-encrypt/src/target/operations.rs +++ b/packages/stack-encrypt/src/target/operations.rs @@ -449,7 +449,7 @@ where /// `Encrypt` consumes the plaintext. In [`Owned`](super::Owned) mode it is /// handed over, so `S` need not be `Clone`; in the default [`Borrowed`] mode /// it is cloned once, which is what `M: ConsumeSource<'s, S>` asks. -pub fn ciphertext<'s, S: crate::Encrypt + 's, K: 'static, M: ConsumeSource<'s, S>>( +pub fn ciphertext<'s, S: crate::Plaintext + 's, K: 'static, M: ConsumeSource<'s, S>>( ) -> Encryption<'s, S, StackCipherText, K, AeadContext, M> { Encryption { build: Box::new( @@ -647,7 +647,7 @@ impl Decryption { } /// The canonical opening operation: retrieve the tree's keys and decode `P` /// through its own Vitamin C `Decrypt` implementation, under `context`. -pub fn open + 'static, K: 'static>( +pub fn open( tree: StackCipherText, context: impl Into, ) -> Decryption { @@ -878,7 +878,7 @@ impl DecryptFrom for T {} // `Borrowed: ConsumeSource<'s, S>`, spelled out. The constructors themselves // (`ciphertext`, `equality`, `ore`, `ope`) ask only for the scheme's own // capability, and run a non-`Clone` plaintext in `Owned` mode. -impl EncryptFrom for StackCipherText { +impl EncryptFrom for StackCipherText { type Context = AeadContext; fn encryption<'s, K: 'static>() -> Encryption<'s, S, Self, K, Self::Context> where @@ -887,7 +887,7 @@ impl EncryptFrom for StackCipherText { ciphertext() } } -impl + 'static> DecryptInto

for StackCipherText { +impl DecryptInto

for StackCipherText { type Context = AeadContext; fn decryption(self, context: Self::Context) -> Decryption { open(self, context) diff --git a/packages/stack-encrypt/src/target/transcode.rs b/packages/stack-encrypt/src/target/transcode.rs index c99071d09..62a9008c2 100644 --- a/packages/stack-encrypt/src/target/transcode.rs +++ b/packages/stack-encrypt/src/target/transcode.rs @@ -10,7 +10,7 @@ //! leaf or marker is authenticated ciphertext, but passthrough metadata and //! terms are not, and a visitor must not present them as such. use crate::sem::{EqualityTerm, MatchConfig, MatchTerms, OpeTerm, OreTerm}; -use crate::{BoxedPassthrough, CipherText, Error, SealedValue, StackCipherText}; +use crate::{CipherText, Error, PassthroughFrame, SealedValue, StackCipherText}; /// An encrypted output that can drive a destination visitor. /// @@ -36,7 +36,9 @@ pub trait Reader: Sized { pub trait Visitor: Sized { /// The destination this visitor builds. type Value; - /// One sealed leaf: a scalar's ciphertext. + /// One sealed leaf: a scalar's ciphertext. An absent optional is one + /// too, a sealed null, so opening it under the wrong context fails + /// like any leaf. /// /// # Errors /// @@ -60,15 +62,6 @@ pub trait Visitor: Sized { fn map(self, _: R) -> Result { Err(Error::UnsupportedShape) } - /// The sealed marker of an absent optional. It is ciphertext, not a - /// null: opening it under the wrong context fails like any leaf. - /// - /// # Errors - /// - /// Refuses with [`Error::UnsupportedShape`] unless overridden. - fn absent(self, _: SealedValue) -> Result { - Err(Error::UnsupportedShape) - } /// The sealed marker of an empty sequence. /// /// # Errors @@ -86,12 +79,13 @@ pub trait Visitor: Sized { Err(Error::UnsupportedShape) } /// Passthrough metadata the plaintext carried alongside its encrypted - /// fields. Not authenticated: the reader hands it over as it was given. + /// fields, as the frame of its subtree. Not authenticated: the reader + /// hands it over as it was given. /// /// # Errors /// /// Refuses with [`Error::UnsupportedShape`] unless overridden. - fn passthrough(self, _: BoxedPassthrough) -> Result { + fn passthrough(self, _: PassthroughFrame) -> Result { Err(Error::UnsupportedShape) } /// An equality term. One-way, and not authenticated. @@ -183,7 +177,6 @@ impl Reader for StackCipherText { CipherText::Single(leaf) => visitor.sealed(leaf), CipherText::Sequence(items) => visitor.sequence(items.into_iter()), CipherText::Map(entries) => visitor.map(entries.into_iter()), - CipherText::None(marker) => visitor.absent(marker), CipherText::EmptySequence(marker) => visitor.empty_sequence(marker), CipherText::EmptyMap(marker) => visitor.empty_map(marker), CipherText::Passthrough(value) => visitor.passthrough(value), diff --git a/packages/stack-encrypt/tests/common/mod.rs b/packages/stack-encrypt/tests/common/mod.rs index 8f0af0ea9..b41b75ba9 100644 --- a/packages/stack-encrypt/tests/common/mod.rs +++ b/packages/stack-encrypt/tests/common/mod.rs @@ -233,3 +233,20 @@ pub async fn deterministic_cipher(seed: [u8; 32]) -> StackCipher(value: P) -> stack_encrypt::PassthroughFrame { + use vitaminc_protected::Controlled; + stack_encrypt::PassthroughFrame( + vitaminc_aead::to_frame(value) + .expect("a passthrough value frames") + .risky_unwrap(), + ) +} + +/// The `u32` a passthrough payload carries. +pub fn passthrough_u32(payload: &stack_encrypt::PassthroughFrame) -> u32 { + use vitaminc_aead::Decrypt; + let frame = payload.parse().expect("one frame"); + vitaminc_aead::Plaintext::visit(frame, u32::sink()).expect("a u32") +} diff --git a/packages/stack-encrypt/tests/fixtures/README.md b/packages/stack-encrypt/tests/fixtures/README.md index 13c8fa4c4..c7a90d045 100644 --- a/packages/stack-encrypt/tests/fixtures/README.md +++ b/packages/stack-encrypt/tests/fixtures/README.md @@ -11,10 +11,9 @@ same seed. `tests/record_lowering.rs` opens each record with the other author and checks that both derive the terms it holds. That cross-opening is the proof that `dynamic::record` is a lowering into the plan builder and not a second executor (ADR-0007, amended 2026-10-06). A Go test reads the -same file later, once the Go SDK's generated code is the third author. A -chain over bare `u32` or `String` fields derives the same terms but a -different leaf encoding, which the other reader cannot open or tell apart; -`dynamic::record`'s docs say why the lowering does not bridge that. +same file later, once the Go SDK's generated code is the third author. A +chain over bare `u32` or `String` fields seals the same leaves too: every +leaf is tagged with its type (vitaminc ADR 0001), whoever wrote it. Regenerate with `STACK_ENCRYPT_UPDATE_FIXTURES=1 cargo test -p stack-encrypt --all-features --test record_lowering`. Only the sealed bytes diff --git a/packages/stack-encrypt/tests/fixtures/record_lowering.json b/packages/stack-encrypt/tests/fixtures/record_lowering.json index 9aa62a478..b0744958c 100644 --- a/packages/stack-encrypt/tests/fixtures/record_lowering.json +++ b/packages/stack-encrypt/tests/fixtures/record_lowering.json @@ -47,7 +47,7 @@ "passthrough": "tenants/acme" }, "text": { - "c": "0100000000000000000000000000000000cf1a45d1517a08c45d6ae1c989ae99de2000da620d1dbdbba9372d1439b6d44f7cc4169e9a255387aa180d6a95fb17926d7001a7e8ada1c849706c1f1b5c56ad878ea5deb68ebefecb98a8816cbe658c20b33989cc", + "c": "0100000000000000000000000000000000cf1a45d1517a08c45d6ae1c989ae99de2000da620d1dbdbba9372d1439b6d44f7cc4169e9a255387aa180d6a95fb17926d70027eff9bae3dcbbf74666b87b43afcdeeb5d90fe4c4e316142e647677cb9597d6cf9c0", "eq": "0d84fbd44d82b5150351556d6d25a038c70cf0c696e2499a71b659a7732a9d08" } }, @@ -59,7 +59,7 @@ "passthrough": "tenants/acme" }, "text": { - "c": "010000000000000000000000000000000048a99626f697c02001f07da2edb19f4220009868b833b6220fa0d13b612af03af1f38fee082c7555b6e7fbf8e13ccb70f80701dc7cf368a02a3bd466d24b4037597aeae058b641ff5a2c73736f98d8d49ae10a3d9c", + "c": "010000000000000000000000000000000048a99626f697c02001f07da2edb19f4220009868b833b6220fa0d13b612af03af1f38fee082c7555b6e7fbf8e13ccb70f80702c1591a387d3f6fd6c83d4b547b857ec46a3ef86919ace434e5ca69581ce699cf0765", "eq": "0d84fbd44d82b5150351556d6d25a038c70cf0c696e2499a71b659a7732a9d08" } } @@ -125,12 +125,12 @@ "records": { "lowering": { "age": { - "c": "01000000000000000000000000000000004d5626148f77c4c6d9da0ddbdf7663a2200021162641133485216f389886cbd7c6e9b25afa0f5f166f985b23384e81ffcfac010686f7787f57b4c7a2192c3499ea1b890f9f4623d84b72b5f40266b10b7f607a41", + "c": "01000000000000000000000000000000004d5626148f77c4c6d9da0ddbdf7663a2200021162641133485216f389886cbd7c6e9b25afa0f5f166f985b23384e81ffcfac02b8821c190a544161b6ac13825768cd6b494515bdd96c26715ffecd7321277d13d6", "eq": "5433335e609685a33b29c98369e68cd5d92e838aa566a3ff1df5c2d251b63885", "ore": "7ca54e16d7944c0a9d8577d87f3864fdf79e99fea5956ba956a126a605cbf3f1" }, "email": { - "c": "0100000000000000000000000000000000b2afa77357a61488146c0edaa0246f65200043956c4f4bba857028c28165dfce9c63d904b738bbb039b6e11c805112d4a1ee01ec122b5ee8771a6a37fc17312ff3f7a56e93885e19d4bb189950aa37475eb74f63f5758c4f87a3494c5e46ff", + "c": "0100000000000000000000000000000000b2afa77357a61488146c0edaa0246f65200043956c4f4bba857028c28165dfce9c63d904b738bbb039b6e11c805112d4a1ee02d3ce6ecc6cc31f82d07a829142cf39b2fcd9a742f89627f316622416f49df065d381337c5da03a8f3ab5b84d", "eq": "ec534cfbf2336785378c3761ff88623c46361b2ba7c5fa83930ebacae49cf646", "match": "06000d0013001d002200260029002a003000320043005a006000660068006c0077008f00960098009e00c200c500d600d900de00e300e700e900eb00ec00f200fa00" }, @@ -138,17 +138,17 @@ "passthrough": 42 }, "notes": { - "c": "0100000000000000000000000000000000723f7a7f14b07197168aa13bc52010b3200072a73194b45af604cc6201b5a57554b05cc7ed6ad22b9102cbcb71bd9067849601a2ae59c2cfca81b0ecde824090e2331f2c389202676855068652e9e566dfccb9a55e9973a9301c" + "c": "0100000000000000000000000000000000723f7a7f14b07197168aa13bc52010b3200072a73194b45af604cc6201b5a57554b05cc7ed6ad22b9102cbcb71bd90678496020f72b5054a1a4467aa441215b4af28502c55a2590aae2a844ea40392f5fd64e679614a072b8736" } }, "typed_chain": { "age": { - "c": "010000000000000000000000000000000092e427c5e176b60261add6ce80fd02df20008d231453c97fdec97a0b6559ac8f509129d20163f262d40ce5d77cf1ebe0cad501155707def46648a0c3685910bea6d3ec9c63aeb8c1e864092d47a3bd0269d135c9", + "c": "010000000000000000000000000000000092e427c5e176b60261add6ce80fd02df20008d231453c97fdec97a0b6559ac8f509129d20163f262d40ce5d77cf1ebe0cad502c51f3fa24db5e34fadc3553b401d674928b00126a41cb7f30da889259f472223e8", "eq": "5433335e609685a33b29c98369e68cd5d92e838aa566a3ff1df5c2d251b63885", "ore": "7ca54e16d7944c0a9d8577d87f3864fdf79e99fea5956ba956a126a605cbf3f1" }, "email": { - "c": "0100000000000000000000000000000000cf92191950409f9fe82a2b11888ffc1d2000ca4d5bbafbff919af4d3a95c7cc046d632fd4c28785b238f4b6f7ea3aa08482901a3e671679e0e1ac106a28ad8a27e3ae143a6ad5c4b8280dbf6818d1bab32fa45cb0790df8551cec1c9bb3187", + "c": "0100000000000000000000000000000000cf92191950409f9fe82a2b11888ffc1d2000ca4d5bbafbff919af4d3a95c7cc046d632fd4c28785b238f4b6f7ea3aa08482902a44da283fdefa835c68377a191ccf172943720b07e7779cc903619869844eb9cfed80a8a7f9034e69385ddb1", "eq": "ec534cfbf2336785378c3761ff88623c46361b2ba7c5fa83930ebacae49cf646", "match": "06000d0013001d002200260029002a003000320043005a006000660068006c0077008f00960098009e00c200c500d600d900de00e300e700e900eb00ec00f200fa00" }, @@ -156,7 +156,7 @@ "passthrough": 42 }, "notes": { - "c": "0100000000000000000000000000000000562a41e032316ff2d1264a9e9f5c3b842000de33c8d1f2caaab8b90f691161a57eeb1328f7f1c9b5be76dec83e8a53765f11015924202425d1e3be9754306c2e290e350232d5b5623190ebcc69feb55fb566fe4b4db024f1ebcc" + "c": "0100000000000000000000000000000000562a41e032316ff2d1264a9e9f5c3b842000de33c8d1f2caaab8b90f691161a57eeb1328f7f1c9b5be76dec83e8a53765f1102c76bf0a61684b320c316ad94b01eb76bb52b3c8493f21a73012de6289580bf355b7192c61500e0" } } } diff --git a/packages/stack-encrypt/tests/index.rs b/packages/stack-encrypt/tests/index.rs index a61f28078..d5a89ec5b 100644 --- a/packages/stack-encrypt/tests/index.rs +++ b/packages/stack-encrypt/tests/index.rs @@ -622,8 +622,9 @@ mod given_encrypted_terms_as_a_target { /// opens through the other's reader. async fn same_bytes(value: S, indexes: X) where - S: stack_encrypt::Encrypt - + for<'a> stack_encrypt::Decrypt<'a> + S: stack_encrypt::Plaintext + + stack_encrypt::Decrypt + + Send + Clone + PartialEq + std::fmt::Debug diff --git a/packages/stack-encrypt/tests/plan_grammar.rs b/packages/stack-encrypt/tests/plan_grammar.rs index b56486a57..67d76cdd9 100644 --- a/packages/stack-encrypt/tests/plan_grammar.rs +++ b/packages/stack-encrypt/tests/plan_grammar.rs @@ -22,7 +22,7 @@ use stack_encrypt::target::{ EncryptFrom, Encrypted, Encryption, IndexSpec, Indexes, TermSet, }; use stack_encrypt::{ - nonempty, Decrypt, Encrypt, Equality, Error, ErrorPayload, Label, Match, NonEmpty, Ope, Ore, + nonempty, Decrypt, Equality, Error, ErrorPayload, Label, Match, NonEmpty, Ope, Ore, Plaintext, Plan, StackCipherText, }; @@ -960,7 +960,7 @@ struct One { /// other plan. async fn same_field_bytes(value: F, indexes: X) where - F: Encrypt + Decrypt<'static> + Clone + Send + Sync + PartialEq + Debug + 'static, + F: Plaintext + Decrypt + Clone + Send + Sync + PartialEq + Debug + 'static, Terms: TermSet + PartialEq + Debug + Send, X: Indexes + Clone + Send + Sync + 'static, { diff --git a/packages/stack-encrypt/tests/record_lowering.rs b/packages/stack-encrypt/tests/record_lowering.rs index dce2f8a5c..7f84e5999 100644 --- a/packages/stack-encrypt/tests/record_lowering.rs +++ b/packages/stack-encrypt/tests/record_lowering.rs @@ -3,8 +3,8 @@ //! records the other sealed, and both derive the same bytes for each term. //! The chain's fields are `dynamic::Value`s, the plaintext type a binding's //! values have, so the two authors write one declaration over one type; a -//! chain over bare `u32`/`String` fields shares the terms and not the leaves -//! (see `dynamic::record`). `tests/fixtures/record_lowering.json` holds one +//! chain over bare `u32`/`String` fields seals the same tagged leaves (see +//! `dynamic::record`). `tests/fixtures/record_lowering.json` holds one //! record from each author, sealed under a deterministic key source so the //! bytes open in any process, and its README gives the schema a Go test //! reads later. @@ -178,7 +178,7 @@ fn json_of_record(tree: StackCipherText) -> Json { let value = match (key.as_str(), node) { ("c", CipherText::Single(leaf)) => json!(hex(&leaf.to_bytes())), ("passthrough", CipherText::Passthrough(payload)) => { - match *payload.downcast::().expect("a value") { + match FfiValue::from_frame(&payload.0).expect("a value") { FfiValue::UInt32(v) => json!(v), FfiValue::String(s) => { json!(String::from_utf8(s.risky_ref().to_vec()).expect("utf8")) @@ -187,7 +187,7 @@ fn json_of_record(tree: StackCipherText) -> Json { } } (_, CipherText::Passthrough(payload)) => { - match *payload.downcast::().expect("a value") { + match FfiValue::from_frame(&payload.0).expect("a value") { FfiValue::Bytes(bytes) => json!(hex(bytes.risky_ref())), _ => panic!("a term is bytes"), } @@ -201,6 +201,14 @@ fn json_of_record(tree: StackCipherText) -> Json { Json::Object(out) } +/// A value as a passthrough node: its frame, in the clear. +fn passthrough(value: FfiValue) -> StackCipherText { + use vitaminc_protected::Controlled; + CipherText::Passthrough(stack_encrypt::PassthroughFrame( + value.to_frame().expect("a value frames").risky_unwrap(), + )) +} + fn record_of_json(record: &Json) -> StackCipherText { let fields = record.as_object().expect("an object"); CipherText::Map( @@ -216,20 +224,16 @@ fn record_of_json(record: &Json) -> StackCipherText { SealedValue::from_bytes(&unhex(value.as_str().expect("hex"))) .expect("a frozen leaf"), ), - "passthrough" => CipherText::Passthrough(Box::new(match value { + "passthrough" => passthrough(match value { Json::String(text) => FfiValue::String(text.as_str().into()), _ => FfiValue::UInt32( u32::try_from(value.as_u64().expect("an integer")) .expect("u32"), ), - }) - as stack_encrypt::BoxedPassthrough), - _ => CipherText::Passthrough(Box::new(FfiValue::Bytes( - vitaminc_protected::Protected::new(unhex( - value.as_str().expect("hex"), - )), - )) - as stack_encrypt::BoxedPassthrough), + }), + _ => passthrough(FfiValue::Bytes(vitaminc_protected::Protected::new( + unhex(value.as_str().expect("hex")), + ))), }; (key.clone(), node) }) @@ -248,10 +252,7 @@ fn record_of_typed(mut values: FieldValues) -> StackCipherText { let notes: StackCipherText = values.take("notes").expect("notes"); let id: u32 = values.take("id").expect("id"); let term = |bytes: Vec| -> StackCipherText { - CipherText::Passthrough( - Box::new(FfiValue::Bytes(vitaminc_protected::Protected::new(bytes))) - as stack_encrypt::BoxedPassthrough, - ) + passthrough(FfiValue::Bytes(vitaminc_protected::Protected::new(bytes))) }; CipherText::Map(vec![ ( @@ -275,9 +276,7 @@ fn record_of_typed(mut values: FieldValues) -> StackCipherText { "id".into(), CipherText::Map(vec![( "passthrough".into(), - CipherText::Passthrough( - Box::new(FfiValue::UInt32(id)) as stack_encrypt::BoxedPassthrough - ), + passthrough(FfiValue::UInt32(id)), )]), ), ]) @@ -303,7 +302,7 @@ fn typed_of_record(tree: StackCipherText) -> FieldValues { let CipherText::Passthrough(payload) = node else { panic!("a passthrough node"); }; - let FfiValue::UInt32(id) = *payload.downcast::().expect("a value") + let FfiValue::UInt32(id) = FfiValue::from_frame(&payload.0).expect("a value") else { panic!("a u32"); }; @@ -576,9 +575,6 @@ fn record_of_note(mut values: FieldValues) -> StackCipherText { let tenant: Value = values.take("tenant").expect("tenant"); let text: Encrypted = values.take("text").expect("text"); let id: u32 = values.take("id").expect("id"); - let passthrough = |value: FfiValue| -> StackCipherText { - CipherText::Passthrough(Box::new(value) as stack_encrypt::BoxedPassthrough) - }; CipherText::Map(vec![ ( "tenant".into(), @@ -628,7 +624,7 @@ fn note_of_record(tree: StackCipherText) -> FieldValues { let CipherText::Passthrough(payload) = node else { panic!("a passthrough node"); }; - match *payload.downcast::().expect("a value") { + match FfiValue::from_frame(&payload.0).expect("a value") { FfiValue::UInt32(id) => { let _ = values.insert(&name, id); } diff --git a/packages/stack-encrypt/tests/roundtrip.rs b/packages/stack-encrypt/tests/roundtrip.rs index 985f65792..96b115d01 100644 --- a/packages/stack-encrypt/tests/roundtrip.rs +++ b/packages/stack-encrypt/tests/roundtrip.rs @@ -7,11 +7,11 @@ use std::collections::HashMap; use stack_encrypt::{ - BoxedPassthrough, Cipher, CipherText, ContextTag, Element, Encrypt, IntoAad, SealedValue, - StackCipher, + CipherText, ContextTag, PassthroughFrame, Plaintext, SealedValue, StackCipher, }; use stack_kms::FakeDataKeySource; -use vitaminc_aead::{MapCipher, Passthrough}; +use vitaminc_aead::plaintext::{MapSink, Sink}; +use vitaminc_aead::Decrypt; use vitaminc_protected::{Controlled, Protected}; async fn cipher() -> StackCipher { @@ -126,9 +126,8 @@ async fn option_none_roundtrips() { #[tokio::test] async fn protected_roundtrip() { - // Exercises the `Protected` Decrypt impl, the sole user of `Decipher::map_ok`. - // (`Vec` would encrypt element-wise as a sequence of `u8`, not as bytes, - // so a string leaf is used here.) + // Exercises the `Protected` Decrypt impl, which builds the value inside + // the wrapper. let cipher = cipher().await; let keyset = cipher.default_keyset(); let secret = Protected::new("classified".to_string()); @@ -150,12 +149,15 @@ async fn nested_vec_roundtrips() { assert_eq!(pt, nested); } +/// The context a `ContextTag` binds is an AAD like any other: a value +/// sealed under `ContextTag::aad(tag)` opens under the same, and only the +/// same, tag. #[tokio::test] async fn context_tag_binds_and_roundtrips() { let cipher = cipher().await; let keyset = cipher.default_keyset(); let ct = keyset - .encrypt(ContextTag::new("token".to_string(), "user:42"), ()) + .encrypt("token".to_string(), ContextTag::aad("user:42")) .await .expect("encrypt"); @@ -172,7 +174,7 @@ async fn context_tag_wrong_context_fails() { let cipher = cipher().await; let keyset = cipher.default_keyset(); let ct = keyset - .encrypt(ContextTag::new("token".to_string(), "user:42"), ()) + .encrypt("token".to_string(), ContextTag::aad("user:42")) .await .expect("encrypt"); @@ -218,8 +220,7 @@ async fn empty_marker_does_not_decode_under_wrong_aad() { } /// A hand-written record carrying one field in the clear, through the -/// type-erased `passthrough_entry_boxed` a cipher-generic `Encrypt` has to -/// use. +/// sink's passthrough. struct Row { id: u32, email: String, @@ -243,22 +244,27 @@ impl Row { } } -impl Encrypt for Row { - fn encrypt_with_aad<'a, C, A>(self, cipher: C, aad: A) -> Result - where - C: Cipher, - A: IntoAad<'a>, - { - let mut map = cipher - .encrypt_map(aad) - .passthrough_entry_boxed("id", Box::new(self.id))?; +impl Plaintext for Row { + fn visit(self, sink: S) -> Result { + let mut map = sink.map(None)?; + map.entry("id")?.passthrough(self.id)?; if self.repeat_id { - map = map.passthrough_entry_boxed("id", Box::new(self.id))?; + map.entry("id")?.passthrough(self.id)?; } - map.encrypt_entry("email", self.email)?.end() + self.email.visit(map.entry("email")?)?; + map.end() } } +/// A passthrough payload: the frame of `value`, in the clear. +fn clear(value: u32) -> PassthroughFrame { + PassthroughFrame( + vitaminc_aead::to_frame(value) + .expect("a u32 frames") + .risky_unwrap(), + ) +} + /// A passthrough entry lands in the map as given, beside the sealed ones; /// a key given twice is refused like any repeated map key, since a map the /// cipher would refuse to open must never be produced. @@ -271,10 +277,15 @@ async fn a_passthrough_map_entry_is_stored_in_the_clear_once() { panic!("a record encrypts as a map: {ct:?}"); }; let entry = |name: &str| entries.iter().find(|(key, _)| key == name).map(|(_, v)| v); - assert!( - matches!(entry("id"), Some(CipherText::Passthrough(v)) if v.downcast_ref::() == Some(&7)), - "{entries:?}" - ); + let Some(CipherText::Passthrough(id)) = entry("id") else { + panic!("the id rides in the clear: {entries:?}"); + }; + let id = id + .parse() + .expect("one frame") + .visit(u32::sink()) + .expect("a u32"); + assert_eq!(id, 7, "the passthrough carries the value as given"); assert!( matches!(entry("email"), Some(CipherText::Single(_))), "{entries:?}" @@ -288,12 +299,11 @@ async fn a_passthrough_map_entry_is_stored_in_the_clear_once() { /// none — no entries at all, or only passthroughs, which carry no tag — /// would verify under *any* AAD. The encrypt side never produces one /// (emptiness is the authenticated `Empty*` marker); the decrypt side must -/// refuse to open one, even for a type that reads passthroughs back. +/// refuse to open one, on the tree's shape, before any sink sees it. #[tokio::test] async fn a_container_with_nothing_sealed_in_it_does_not_open() { let cipher = cipher().await; let aad = b"users".as_slice(); - let clear = |value: u32| CipherText::Passthrough(Box::new(value) as BoxedPassthrough); let result: Result, _> = cipher.decrypt(CipherText::Sequence(vec![]), aad).await; assert!(result.is_err(), "an entry-less sequence proves nothing"); @@ -301,15 +311,24 @@ async fn a_container_with_nothing_sealed_in_it_does_not_open() { cipher.decrypt(CipherText::Map(vec![]), aad).await; assert!(result.is_err(), "an entry-less map proves nothing"); - let result: Result>, _> = cipher - .decrypt(CipherText::Sequence(vec![clear(1), clear(2)]), aad) + let result: Result, _> = cipher + .decrypt( + CipherText::Sequence(vec![ + CipherText::Passthrough(clear(1)), + CipherText::Passthrough(clear(2)), + ]), + aad, + ) .await; assert!( result.is_err(), "an all-passthrough sequence proves nothing" ); - let result: Result>, _> = cipher - .decrypt(CipherText::Map(vec![("id".to_string(), clear(1))]), aad) + let result: Result, _> = cipher + .decrypt( + CipherText::Map(vec![("id".to_string(), CipherText::Passthrough(clear(1)))]), + aad, + ) .await; assert!(result.is_err(), "an all-passthrough map proves nothing"); } @@ -360,94 +379,6 @@ async fn sequence_element_cannot_be_rehomed_as_scalar() { ); } -#[tokio::test] -async fn element_roundtrips_under_bare_caller_aad() { - // `Element` derives `for_sequence_element` inside its own Encrypt/Decrypt - // impls. Both sides must honour that derivation: the decipher opens the leaf - // under the AAD the Decrypt drive supplies, not a pre-derived one. - let cipher = cipher().await; - let keyset = cipher.default_keyset(); - let ct = keyset - .encrypt(Element("row".to_string()), b"users".as_slice()) - .await - .expect("encrypt"); - let pt: Element = cipher - .decrypt(ct, b"users".as_slice()) - .await - .expect("Element must round-trip under the bare caller AAD"); - assert_eq!(pt.into_inner(), "row"); -} - -#[tokio::test] -async fn element_interchanges_with_vec_element() { - // A row sealed as one element of a `Vec` decrypts alone as `Element` - // under the same caller AAD (Element's documented use-case), and a lone - // `Element` ciphertext decrypts as a one-element `Vec`. - let cipher = cipher().await; - let keyset = cipher.default_keyset(); - let aad = b"users".as_slice(); - - let ct = keyset - .encrypt(vec!["a".to_string(), "b".to_string()], aad) - .await - .expect("encrypt"); - let second = match ct { - CipherText::Sequence(mut items) => items.remove(1), - other => other, - }; - let pt: Element = cipher - .decrypt(second, aad) - .await - .expect("spliced element must decrypt as Element"); - assert_eq!(pt.into_inner(), "b"); - - let lone = keyset - .encrypt(Element("c".to_string()), aad) - .await - .expect("encrypt"); - let wrapped = CipherText::Sequence(vec![lone]); - let pt: Vec = cipher - .decrypt(wrapped, aad) - .await - .expect("lone Element must decrypt as a one-element Vec"); - assert_eq!(pt, vec!["c".to_string()]); -} - -#[tokio::test] -async fn element_fails_under_wrong_caller_aad() { - let cipher = cipher().await; - let keyset = cipher.default_keyset(); - let ct = keyset - .encrypt(Element("row".to_string()), b"users".as_slice()) - .await - .expect("encrypt"); - let result: Result, _> = cipher.decrypt(ct, b"orders".as_slice()).await; - assert!( - result.is_err(), - "Element under the wrong caller AAD must not decrypt" - ); -} - -/// The derivation that binds a sequence element to its position is the -/// library's, applied by `Element` itself — there is no entry point that -/// lets a caller retrieve keys under one context and authenticate under -/// another, so a batched row is read back by naming the type, not by -/// reconstructing the AAD. -#[tokio::test] -async fn a_batched_element_opens_by_naming_the_type() { - let cipher = cipher().await; - let keyset = cipher.default_keyset(); - let ct = keyset - .encrypt(Element("row".to_string()), b"users".as_slice()) - .await - .expect("encrypt"); - let opened: Element = cipher - .decrypt(ct, b"users".as_slice()) - .await - .expect("Element applies its own derivation on open"); - assert_eq!(opened.into_inner(), "row"); -} - #[tokio::test] async fn wrong_shape_fails() { // A scalar ciphertext must not decode as a sequence. diff --git a/packages/stack-encrypt/tests/source_mode.rs b/packages/stack-encrypt/tests/source_mode.rs index 0489248b0..6780d5c67 100644 --- a/packages/stack-encrypt/tests/source_mode.rs +++ b/packages/stack-encrypt/tests/source_mode.rs @@ -1,6 +1,6 @@ //! How a description receives its plaintext (`target::{Borrowed, Owned}`). //! -//! Vitamin C's `Encrypt` and `PrfValue` consume their input. A description +//! Vitamin C's by-value `Plaintext` walk and `PrfValue` consume their input. A description //! handed a borrow (the default, and what every `EncryptFrom` declaration //! runs in) clones before an operation consumes; one handed the value //! (`Owned`) gives it to the operation. These tests hold the two claims that @@ -21,9 +21,9 @@ use stack_encrypt::sem::{EqualityTerm, MatchTerms}; use stack_encrypt::target::{ ciphertext, equality, matching, AeadContext, Borrowed, CallerContext, Encryption, Owned, }; -use stack_encrypt::{nonempty, Encrypt, NonEmpty, StackCipherText}; +use stack_encrypt::{nonempty, NonEmpty, Plaintext, StackCipherText}; use stack_kms::FakeDataKeySource; -use vitaminc_aead::{Cipher, IntoAad}; +use vitaminc_aead::plaintext::Sink; use vitaminc_prf::{IntoPrfContext, Prf, PrfValue, PrfVisitor}; use vitaminc_protected::{Controlled, Protected}; @@ -52,13 +52,9 @@ impl Secret { Self(Protected::new(text.to_string())) } } -impl Encrypt for Secret { - fn encrypt_with_aad<'a, C, A>(self, cipher: C, aad: A) -> Result - where - C: Cipher, - A: IntoAad<'a>, - { - self.0.encrypt_with_aad(cipher, aad) +impl Plaintext for Secret { + fn visit(self, sink: S) -> Result { + self.0.visit(sink) } } impl PrfValue for Secret { @@ -103,13 +99,9 @@ impl Clone for Counted { } } } -impl Encrypt for Counted { - fn encrypt_with_aad<'a, C, A>(self, cipher: C, aad: A) -> Result - where - C: Cipher, - A: IntoAad<'a>, - { - self.text.encrypt_with_aad(cipher, aad) +impl Plaintext for Counted { + fn visit(self, sink: S) -> Result { + self.text.visit(sink) } } impl PrfValue for Counted { @@ -186,7 +178,7 @@ async fn a_plaintext_that_is_not_clone_derives_a_match_term_alone() { #[cfg(feature = "dynamic")] #[tokio::test] async fn a_zeroizing_ffi_value_seals_through_ciphertext_alone() { - use vitaminc_aead_value::FfiValue; + use stack_encrypt::dynamic::FfiValue; let cipher = stack_cipher().await; let keyset = cipher.default_keyset(); @@ -237,7 +229,7 @@ async fn one_owned_operation_makes_no_copy() { fn sealed_and_indexed<'s, S, M>( ) -> Encryption<'s, S, (StackCipherText, EqualityTerm), FakeDataKeySource, CallerContext, M> where - S: Encrypt + PrfValue + 's, + S: Plaintext + PrfValue + 's, M: stack_encrypt::target::ConsumeSource<'s, S> + stack_encrypt::target::ShareSource<'s, S>, { ciphertext().accepting().zip(equality()) @@ -350,13 +342,9 @@ impl Drop for Dropped { let _ = self.drops.fetch_add(1, Ordering::SeqCst); } } -impl Encrypt for Dropped { - fn encrypt_with_aad<'a, C, A>(mut self, cipher: C, aad: A) -> Result - where - C: Cipher, - A: IntoAad<'a>, - { - std::mem::take(&mut self.text).encrypt_with_aad(cipher, aad) +impl Plaintext for Dropped { + fn visit(mut self, sink: S) -> Result { + std::mem::take(&mut self.text).visit(sink) } } impl PrfValue for Dropped { diff --git a/packages/stack-encrypt/tests/transcode.rs b/packages/stack-encrypt/tests/transcode.rs index 23c011cef..8f9033f90 100644 --- a/packages/stack-encrypt/tests/transcode.rs +++ b/packages/stack-encrypt/tests/transcode.rs @@ -5,11 +5,12 @@ use stack_encrypt::sem::EqualityTerm; use stack_encrypt::target::transcode::{MapReader, Reader, SequenceReader, Transcode, Visitor}; use stack_encrypt::target::{self, CallerContext, ExpectedContext}; use stack_encrypt::{ - nonempty, Cipher, CipherText, ContextPiece, DecryptField, DecryptInto, Decryptable, Decryption, - Encrypt, EncryptFrom, Encryption, Error, IntoAad, IntoContext, MaybeEmpty, NonEmpty, + nonempty, CipherText, ContextPiece, DecryptField, DecryptInto, Decryptable, Decryption, + EncryptFrom, Encryption, Error, IntoContext, MaybeEmpty, NonEmpty, PassthroughFrame, Plaintext, SealedValue, StackCipherText, }; use std::sync::atomic::Ordering; +use vitaminc_aead::plaintext::Sink; #[derive(Clone, Debug, PartialEq)] struct Identifier { @@ -50,7 +51,7 @@ impl Transcode for StoredLeaf { LeafVisitor } } -impl EncryptFrom for StoredLeaf { +impl EncryptFrom for StoredLeaf { type Context = CallerContext; fn encryption<'s, K: 'static>() -> Encryption<'s, S, Self, K, Self::Context> where @@ -59,7 +60,7 @@ impl EncryptFrom for StoredLeaf { target::ciphertext().accepting().transcode() } } -impl + 'static> DecryptInto

for StoredLeaf { +impl DecryptInto

for StoredLeaf { type Context = CallerContext; fn decryption(self, context: Self::Context) -> Decryption { match SealedValue::from_bytes(&self.0) { @@ -222,13 +223,14 @@ async fn empty_stored_identifier_is_refused_before_any_key_is_retrieved() { #[derive(Clone)] struct WithoutSerde(String); -impl Encrypt for WithoutSerde { - fn encrypt_with_aad<'a, C: Cipher, A: IntoAad<'a>>( - self, - cipher: C, - aad: A, - ) -> Result { - self.0.encrypt_with_aad(cipher, aad) +impl Plaintext for WithoutSerde { + fn visit(self, sink: S) -> Result { + self.0.visit(sink) + } +} +impl Plaintext for &WithoutSerde { + fn visit(self, sink: S) -> Result { + (&self.0).visit(sink) } } #[tokio::test] @@ -248,7 +250,7 @@ async fn ciphertext_uses_plaintexts_native_contract_without_serde() { .unwrap(); assert_eq!( opened, "native", - "a plaintext without Serde seals through its own Encrypt impl and opens canonically" + "a plaintext without Serde seals through its own Plaintext impl and opens canonically" ); let text = String::from("borrowed"); let result: StoredLeaf = keyset @@ -283,7 +285,7 @@ async fn scalar_destination_refuses_a_sequence() { // A target whose declaration carries every context it needs asks its caller // for none: `Context = ()`. struct FixedLeaf(StoredLeaf); -impl EncryptFrom for FixedLeaf { +impl EncryptFrom for FixedLeaf { type Context = (); fn encryption<'s, K: 'static>() -> Encryption<'s, S, Self, K, Self::Context> where @@ -298,7 +300,7 @@ impl EncryptFrom for FixedLeaf { .accepting() } } -impl + 'static> DecryptInto

for FixedLeaf { +impl DecryptInto

for FixedLeaf { type Context = (); fn decryption(self, (): ()) -> Decryption { self.0.decryption(nonempty!("fixed/leaf").into()) @@ -337,10 +339,9 @@ enum Stored { Value(SealedValue), List(Vec), Object(Vec<(String, Stored)>), - Absent(SealedValue), EmptyList(SealedValue), EmptyObject(SealedValue), - Metadata(stack_encrypt::BoxedPassthrough), + Metadata(PassthroughFrame), } struct TreeVisitor; impl Visitor for TreeVisitor { @@ -362,16 +363,13 @@ impl Visitor for TreeVisitor { } Ok(Stored::Object(output)) } - fn absent(self, marker: SealedValue) -> Result { - Ok(Stored::Absent(marker)) - } fn empty_sequence(self, marker: SealedValue) -> Result { Ok(Stored::EmptyList(marker)) } fn empty_map(self, marker: SealedValue) -> Result { Ok(Stored::EmptyObject(marker)) } - fn passthrough(self, value: stack_encrypt::BoxedPassthrough) -> Result { + fn passthrough(self, value: PassthroughFrame) -> Result { Ok(Stored::Metadata(value)) } } @@ -383,7 +381,6 @@ impl Stored { Self::Object(v) => { CipherText::Map(v.into_iter().map(|(k, v)| (k, v.native())).collect()) } - Self::Absent(v) => CipherText::None(v), Self::EmptyList(v) => CipherText::EmptySequence(v), Self::EmptyObject(v) => CipherText::EmptyMap(v), Self::Metadata(v) => CipherText::Passthrough(v), @@ -441,7 +438,7 @@ fn native_readers_report_the_number_of_remaining_entries() { } } - let value = || CipherText::Passthrough(Box::new(7_u32) as stack_encrypt::BoxedPassthrough); + let value = || CipherText::Passthrough(common::passthrough(7_u32)); let sequence: StackCipherText = CipherText::Sequence(vec![value(), value(), value()]); assert_eq!( sequence.read(LengthVisitor).unwrap(), @@ -521,14 +518,14 @@ async fn native_readers_preserve_map_keys_and_authenticated_markers() { opened.is_err(), "an empty-map marker is sealed under its context, not an unauthenticated empty container" ); - let stored = CipherText::Passthrough(Box::new(17u32) as stack_encrypt::BoxedPassthrough) + let stored = CipherText::Passthrough(common::passthrough(17u32)) .read(TreeVisitor) .unwrap(); let Stored::Metadata(value) = stored else { panic!("metadata") }; assert_eq!( - *value.downcast::().unwrap(), + common::passthrough_u32(&value), 17, "passthrough metadata reaches the destination as it was given" ); diff --git a/packages/stack-encrypt/tests/ui/decrypt_field_not_decryptable.stderr b/packages/stack-encrypt/tests/ui/decrypt_field_not_decryptable.stderr index 4a434533e..68a157b03 100644 --- a/packages/stack-encrypt/tests/ui/decrypt_field_not_decryptable.stderr +++ b/packages/stack-encrypt/tests/ui/decrypt_field_not_decryptable.stderr @@ -14,7 +14,7 @@ help: the trait `DecryptField` is not implemented for `Opaqu `(A, B, C)` implements `DecryptField` `(A, B, C, D)` implements `DecryptField` `(A, B, C, D, E)` implements `DecryptField` - `CipherText>` implements `DecryptField` + `CipherText` implements `DecryptField` `Encrypted` implements `DecryptField` `EqualityTerm` implements `DecryptField` `MatchTerms` implements `DecryptField` @@ -38,7 +38,7 @@ help: the trait `Decryptable` is not implemented for `Opaque` (A, B, C) (A, B, C, D) (A, B, C, D, E) - CipherText> + CipherText Encrypted EqualityTerm MatchTerms diff --git a/packages/stack-encrypt/tests/ui/drop_record.stderr b/packages/stack-encrypt/tests/ui/drop_record.stderr index acf842fcd..c3667678c 100644 --- a/packages/stack-encrypt/tests/ui/drop_record.stderr +++ b/packages/stack-encrypt/tests/ui/drop_record.stderr @@ -5,4 +5,4 @@ error[E0509]: cannot move out of type `Rec`, which implements the `Drop` trait | ^^^^^^^^^^^^^^^^^^ | | | cannot move out of here - | move occurs because value has type `CipherText>`, which does not implement the `Copy` trait + | move occurs because value has type `CipherText`, which does not implement the `Copy` trait diff --git a/packages/stack-encrypt/tests/ui/owned_fan_out_needs_clone.rs b/packages/stack-encrypt/tests/ui/owned_fan_out_needs_clone.rs index 8c808dca1..2573c44ca 100644 --- a/packages/stack-encrypt/tests/ui/owned_fan_out_needs_clone.rs +++ b/packages/stack-encrypt/tests/ui/owned_fan_out_needs_clone.rs @@ -3,10 +3,10 @@ //! one place a copy is unavoidable — the first side gets a clone, the last //! the value — and the refusal names `zip`, not the operations. use stack_encrypt::target::{ciphertext, equality, CallerContext, Owned}; -use stack_encrypt::{nonempty, Encrypt, KeysetCipher}; +use stack_encrypt::{nonempty, KeysetCipher, Plaintext}; use stack_kms::FakeDataKeySource; -fn seal_and_index( +fn seal_and_index( keyset: &KeysetCipher<'_, FakeDataKeySource>, value: S, ) { diff --git a/packages/stack-encrypt/tests/ui/owned_fan_out_needs_clone.stderr b/packages/stack-encrypt/tests/ui/owned_fan_out_needs_clone.stderr index aca006b38..ca42a4fa4 100644 --- a/packages/stack-encrypt/tests/ui/owned_fan_out_needs_clone.stderr +++ b/packages/stack-encrypt/tests/ui/owned_fan_out_needs_clone.stderr @@ -15,5 +15,5 @@ note: required by a bound in `Encryption::<'s, S, T, K, Ctx, M>::zip` | ^^^^^^^^^^^^^^^^^^ required by this bound in `Encryption::<'s, S, T, K, Ctx, M>::zip` help: consider further restricting type parameter `S` with trait `Clone` | - 9 | fn seal_and_index( - | +++++++++++++++++++ + 9 | fn seal_and_index( + | +++++++++++++++++++ diff --git a/packages/stack-encrypt/tests/ui/pass/owned_without_clone.rs b/packages/stack-encrypt/tests/ui/pass/owned_without_clone.rs index 4ae01d79f..f15dcfedd 100644 --- a/packages/stack-encrypt/tests/ui/pass/owned_without_clone.rs +++ b/packages/stack-encrypt/tests/ui/pass/owned_without_clone.rs @@ -5,10 +5,10 @@ use stack_encrypt::sem::{CllwOpeEncrypt, CllwOreEncrypt, EqualityTerm, MatchTerm use stack_encrypt::target::{ ciphertext, equality, matching, ope, ore, CallerContext, Owned, Pending, }; -use stack_encrypt::{nonempty, Encrypt, KeysetCipher, StackCipherText}; +use stack_encrypt::{nonempty, KeysetCipher, Plaintext, StackCipherText}; use stack_kms::FakeDataKeySource; -fn seal<'a, S: Encrypt>( +fn seal<'a, S: Plaintext>( keyset: &'a KeysetCipher<'_, FakeDataKeySource>, value: S, ) -> Pending<'a, StackCipherText, FakeDataKeySource> { diff --git a/packages/stack-encrypt/tests/ui/pass/query_only_and_borrowed.rs b/packages/stack-encrypt/tests/ui/pass/query_only_and_borrowed.rs index 3ff7a83e1..78f5c82f6 100644 --- a/packages/stack-encrypt/tests/ui/pass/query_only_and_borrowed.rs +++ b/packages/stack-encrypt/tests/ui/pass/query_only_and_borrowed.rs @@ -5,7 +5,7 @@ use stack_kms::FakeDataKeySource; #[derive(EncryptFrom)] struct Probe { hm: EqualityTerm } -// No Encrypt bound: producing a term requires only the PRF capability. +// No Plaintext bound: producing a term requires only the PRF capability. fn query(cipher:&StackCipher, value:&S) { let keyset=cipher.default_keyset(); let _ = keyset.encrypt_as::<_,Probe>(value,nonempty!("column").into()); diff --git a/packages/stack-encrypt/tests/ui/plaintext_needs_encrypt.stderr b/packages/stack-encrypt/tests/ui/plaintext_needs_encrypt.stderr index dd4c11198..749218206 100644 --- a/packages/stack-encrypt/tests/ui/plaintext_needs_encrypt.stderr +++ b/packages/stack-encrypt/tests/ui/plaintext_needs_encrypt.stderr @@ -1,4 +1,4 @@ -error[E0277]: the trait bound `SerdeOnly: stack_encrypt::Encrypt` is not satisfied +error[E0277]: the trait bound `SerdeOnly: stack_encrypt::Plaintext` is not satisfied --> tests/ui/plaintext_needs_encrypt.rs:9:33 | 9 | let _=keyset.encrypt_as::<_,Target>(&SerdeOnly {value:"x".into()},nonempty!("column").into()); @@ -6,22 +6,22 @@ error[E0277]: the trait bound `SerdeOnly: stack_encrypt::Encrypt` is not satisfi | | | required by a bound introduced by this call | -help: the trait `stack_encrypt::Encrypt` is not implemented for `SerdeOnly` +help: the trait `stack_encrypt::Plaintext` is not implemented for `SerdeOnly` --> tests/ui/plaintext_needs_encrypt.rs:4:1 | 4 | struct SerdeOnly { value: String } | ^^^^^^^^^^^^^^^^ - = help: the following other types implement trait `stack_encrypt::Encrypt`: - &str - ContextTag - FfiValue - HashMap - Vec - Vec - [u8; N] - stack_encrypt::Element + = help: the following other types implement trait `stack_encrypt::Plaintext`: + &&'b T + &'a BTreeMap + &'a HashMap + &'a Vec + &'a [T] + &'a std::option::Option + &'a vitaminc_protected::protected::Protected + &FfiValue and $N others - = note: required for `CipherText>` to implement `EncryptFrom` + = note: required for `CipherText` to implement `EncryptFrom` = note: 1 redundant requirement hidden = note: required for `Target` to implement `EncryptFrom` note: required by a bound in `target::operations::>::encrypt_as` diff --git a/packages/stack-encrypt/tests/ui/struct_field_must_open.rs b/packages/stack-encrypt/tests/ui/struct_field_must_open.rs index 5394b539f..cf829679f 100644 --- a/packages/stack-encrypt/tests/ui/struct_field_must_open.rs +++ b/packages/stack-encrypt/tests/ui/struct_field_must_open.rs @@ -1,13 +1,29 @@ //! Every field of a `struct = ..` record opens back into the plaintext //! field it is derived from (`DecryptField`), even when //! the record derives `EncryptFrom` alone: the record's plan seals and -//! opens each field. A borrowed plaintext field cannot be opened into, and -//! a target with no `DecryptField` cannot open. +//! opens each field. A plaintext field with no `Decrypt`, such as one that +//! borrows its text, cannot be opened into, and a target with no +//! `DecryptField` cannot open. use stack_encrypt::target::{Decryptable, Encryption, AeadContext}; -use stack_encrypt::{EncryptFrom, StackCipherText}; +use stack_encrypt::{EncryptFrom, Plaintext, StackCipherText}; +use vitaminc_aead::plaintext::Sink; + +/// Text it borrows: it seals, and there is nothing to open it into. +#[derive(Clone)] +struct Name(&'static str); +impl Plaintext for Name { + fn visit(self, sink: S) -> Result { + self.0.visit(sink) + } +} +impl Plaintext for &Name { + fn visit(self, sink: S) -> Result { + self.0.visit(sink) + } +} struct Profile { - name: &'static str, + name: Name, bio: String, } diff --git a/packages/stack-encrypt/tests/ui/struct_field_must_open.stderr b/packages/stack-encrypt/tests/ui/struct_field_must_open.stderr index 40c9d95bf..59383c476 100644 --- a/packages/stack-encrypt/tests/ui/struct_field_must_open.stderr +++ b/packages/stack-encrypt/tests/ui/struct_field_must_open.stderr @@ -1,18 +1,27 @@ -error[E0277]: the trait bound `&str: Decrypt<'static>` is not satisfied - --> tests/ui/struct_field_must_open.rs:29:11 - | -29 | name: StackCipherText, - | ^^^^^^^^^^^^^^^ the trait `Decrypt<'static>` is not implemented for `&str` - | -help: the trait `Decrypt<'static>` is not implemented for `&str` - but trait `Decrypt<'_>` is implemented for `std::string::String` - --> $CARGO/vitaminc-aead-$VERSION/src/decrypt/impls.rs - | - | impl<'c> Decrypt<'c> for String { - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - = help: for that trait implementation, expected `std::string::String`, found `&str` - = note: required for `CipherText>` to implement `DecryptInto<&str>` - = note: required for `CipherText>` to implement `DecryptField<&str, CallerContext>` +error[E0277]: the trait bound `Name: FromScalar` is not satisfied + --> tests/ui/struct_field_must_open.rs:45:11 + | +45 | name: StackCipherText, + | ^^^^^^^^^^^^^^^ unsatisfied trait bound + | +help: the trait `FromScalar` is not implemented for `Name` + --> tests/ui/struct_field_must_open.rs:13:1 + | +13 | struct Name(&'static str); + | ^^^^^^^^^^^ + = help: the following other types implement trait `FromScalar`: + Vec + [u8; N] + bool + f32 + f64 + i128 + i16 + i32 + and $N others + = note: required for `Name` to implement `Decrypt` + = note: required for `CipherText` to implement `DecryptInto` + = note: required for `CipherText` to implement `DecryptField` note: required by a bound in `FieldsBuilder::::encrypt_into` --> src/plan/build.rs | @@ -23,22 +32,22 @@ note: required by a bound in `FieldsBuilder::::encrypt_into` | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `FieldsBuilder::::encrypt_into` error[E0277]: the trait bound `Opaque: DecryptField` is not satisfied - --> tests/ui/struct_field_must_open.rs:30:10 + --> tests/ui/struct_field_must_open.rs:46:10 | -30 | bio: Opaque, +46 | bio: Opaque, | ^^^^^^ unsatisfied trait bound | help: the trait `DecryptField` is not implemented for `Opaque` - --> tests/ui/struct_field_must_open.rs:15:1 + --> tests/ui/struct_field_must_open.rs:31:1 | -15 | struct Opaque(#[allow(dead_code)] StackCipherText); +31 | struct Opaque(#[allow(dead_code)] StackCipherText); | ^^^^^^^^^^^^^ = help: the following other types implement trait `DecryptField`: `(A, B)` implements `DecryptField` `(A, B, C)` implements `DecryptField` `(A, B, C, D)` implements `DecryptField` `(A, B, C, D, E)` implements `DecryptField` - `CipherText>` implements `DecryptField` + `CipherText` implements `DecryptField` `Encrypted` implements `DecryptField` `EqualityTerm` implements `DecryptField` `MatchTerms` implements `DecryptField` diff --git a/packages/stack-guest-abi/Cargo.toml b/packages/stack-guest-abi/Cargo.toml index 3d3179922..a7ed2db77 100644 --- a/packages/stack-guest-abi/Cargo.toml +++ b/packages/stack-guest-abi/Cargo.toml @@ -16,18 +16,18 @@ publish = false [dependencies] zeroize = { workspace = true } # `last_error`: an error is any miette diagnostic plus its structured fields -# (a serde_json map, as `ErrorPayload::payload` gives them), encoded in the -# one transport codec every input and output already uses. +# (a serde_json map, as `ErrorPayload::payload` gives them), encoded as a +# `HostValue` in the one transport frame every input and output uses. miette = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } -vitaminc-aead-value = { workspace = true } - -# Only `transport` (wasm32-only) needs these: the import's error type and -# `OpaqueDebug` for its response, whose body can carry wrapped key material -# or a credential and so never prints. Declared for that target alone, so a -# native build carries nothing it does not use and `cargo udeps` sees none. -[target.'cfg(target_arch = "wasm32")'.dependencies] +# `value`: a `HostValue` describes itself to vitaminc's plaintext model and +# crosses to the host as a transport frame. +vitaminc-aead = { workspace = true } +vitaminc-aead-frame = { workspace = true } +# `HostValue::Bytes` holds its bytes in `Protected`, and `transport` +# (wasm32-only) uses `OpaqueDebug` for its response, whose body can carry +# wrapped key material or a credential and so never prints. vitaminc-protected = { workspace = true } [dev-dependencies] diff --git a/packages/stack-guest-abi/src/abi.rs b/packages/stack-guest-abi/src/abi.rs index 90d363e35..bc04e172e 100644 --- a/packages/stack-guest-abi/src/abi.rs +++ b/packages/stack-guest-abi/src/abi.rs @@ -71,7 +71,7 @@ pub fn export(f: impl FnOnce() -> Result, u32>) -> u64 { } /// The full error behind the most recent failed export, as a packed buffer -/// result: a transport-codec object of `code`, `message`, `help`, `url` +/// result: a transport-frame map of `code`, `message`, `help`, `url` /// (when set), `severity`, `fields` and `causes` (see /// [`last_error::encode`]). Zero when there is none — no export has failed /// since the last success, or the error was already handed over. diff --git a/packages/stack-guest-abi/src/call.rs b/packages/stack-guest-abi/src/call.rs index a215404c0..c1b5bdc49 100644 --- a/packages/stack-guest-abi/src/call.rs +++ b/packages/stack-guest-abi/src/call.rs @@ -93,7 +93,7 @@ pub fn take_last_error() -> Option> { mod tests { use super::*; use crate::status::{STATUS_ENCODING, STATUS_INTERNAL}; - use vitaminc_aead_value::{transport as codec, FfiValue}; + use crate::value::HostValue; /// A packed result, read the way the Go host reads it. #[derive(Debug, PartialEq)] @@ -117,16 +117,8 @@ mod tests { /// The recorded error's code, handed over as the host would get it. fn last_code() -> Option { let bytes = take_last_error()?; - let value = codec::decode_value(&mut codec::Reader::new(&bytes)).expect("decodes"); - let FfiValue::Object(entries) = value else { - return None; - }; - entries.into_iter().find_map(|(key, value)| match value { - FfiValue::String(code) if key == "code" => { - String::from_utf8(code.risky_ref().to_vec()).ok() - } - _ => None, - }) + let value = HostValue::decode(&bytes).expect("decodes"); + value.get("code")?.as_text().map(str::to_owned) } #[test] diff --git a/packages/stack-guest-abi/src/last_error.rs b/packages/stack-guest-abi/src/last_error.rs index ff8ed8c1d..89db704f4 100644 --- a/packages/stack-guest-abi/src/last_error.rs +++ b/packages/stack-guest-abi/src/last_error.rs @@ -5,7 +5,7 @@ //! the fast path a host acts on, byte-compatible with the vitaminc guest. //! Beside it, the export records the error that produced the number — its //! miette code, message, help, URL, severity, structured fields and causes — -//! as one value in the transport codec every input and output already uses +//! as one value in the transport frame every input and output already uses //! ([`encode`]). A host that wants the detail calls `se_last_error` after a //! non-zero status; a call that succeeds pays nothing. //! @@ -43,10 +43,10 @@ use std::cell::Cell; use std::error::Error; use miette::{Diagnostic, Severity}; -use vitaminc_aead_value::{transport as codec, FfiValue}; use crate::buffers; use crate::status::{STATUS_ENCODING, STATUS_INTERNAL, STATUS_STATE}; +use crate::value::HostValue; thread_local! { /// The registered buffer holding the encoded last error, if any, and @@ -82,7 +82,7 @@ pub const UNDESCRIBED_CAUSE: &str = "an error from another library"; #[non_exhaustive] pub enum GuestError { /// The input failed the export's own validation before any library - /// saw it: bytes that are not the transport codec, a pointer/length + /// saw it: bytes that are not a transport frame, a pointer/length /// pair outside linear memory, a config or option of the wrong shape. #[error("malformed input: {0}")] #[diagnostic(code(stack_guest_abi::malformed_input))] @@ -191,7 +191,7 @@ pub fn encode( error: &dyn Diagnostic, fields: serde_json::Map, describe: &dyn Fn(&(dyn Error + 'static)) -> Option, -) -> FfiValue { +) -> HostValue { let mut entries = Vec::with_capacity(7); if let Some(code) = error.code() { entries.push(("code".to_owned(), text(code.to_string()))); @@ -211,18 +211,13 @@ pub fn encode( entries.push(("severity".to_owned(), text(severity))); entries.push(( "fields".to_owned(), - FfiValue::Object( - fields - .into_iter() - .map(|(key, value)| (key, json_value(value))) - .collect(), - ), + HostValue::from_json(serde_json::Value::Object(fields)), )); entries.push(( "causes".to_owned(), - FfiValue::Array(causes(error, describe)), + HostValue::List(causes(error, describe)), )); - FfiValue::Object(entries) + HostValue::Map(entries) } /// One link of the cause chain. @@ -239,7 +234,7 @@ enum Cause<'a> { fn causes( error: &dyn Diagnostic, describe: &dyn Fn(&(dyn Error + 'static)) -> Option, -) -> Vec { +) -> Vec { let mut out = Vec::new(); let mut next = next_of(error); while let Some(cause) = next { @@ -263,7 +258,7 @@ fn causes( entry.push(("code".to_owned(), text(code))); } entry.push(("message".to_owned(), text(message))); - out.push(FfiValue::Object(entry)); + out.push(HostValue::Map(entry)); next = after; } out @@ -279,35 +274,8 @@ fn next_of(diagnostic: &dyn Diagnostic) -> Option> { } /// A string leaf. -fn text(value: impl Into) -> FfiValue { - FfiValue::String(value.into().into()) -} - -/// A structured field as a transport value. -fn json_value(value: serde_json::Value) -> FfiValue { - match value { - serde_json::Value::Null => FfiValue::Null, - serde_json::Value::Bool(value) => FfiValue::Bool(value), - serde_json::Value::Number(number) => { - if let Some(value) = number.as_u64() { - FfiValue::UInt64(value) - } else if let Some(value) = number.as_i64() { - FfiValue::Int64(value) - } else { - FfiValue::Float64(number.as_f64().unwrap_or(f64::NAN)) - } - } - serde_json::Value::String(value) => text(value), - serde_json::Value::Array(items) => { - FfiValue::Array(items.into_iter().map(json_value).collect()) - } - serde_json::Value::Object(entries) => FfiValue::Object( - entries - .into_iter() - .map(|(key, value)| (key, json_value(value))) - .collect(), - ), - } +fn text(value: impl Into) -> HostValue { + HostValue::text(value) } /// Record `error` as the last error for `status`, with its structured @@ -331,14 +299,14 @@ pub fn record_with( describe: &dyn Fn(&(dyn Error + 'static)) -> Option, status: u32, ) -> u32 { - let mut bytes = Vec::new(); // An error that does not encode (it cannot: every leaf is a string, a - // number or a container) leaves the last error empty rather than half - // written; the status still says what happened. - if codec::encode_value(encode(error, fields, describe), &mut bytes).is_err() { + // number or a container, and every key is distinct) leaves the last + // error empty rather than half written; the status still says what + // happened. + let Ok(bytes) = encode(error, fields, describe).encode() else { clear(); return status; - } + }; store(bytes, status); status } @@ -417,22 +385,16 @@ pub fn take() -> Option> { mod tests { use super::*; - fn decode(bytes: &[u8]) -> FfiValue { - codec::decode_value(&mut codec::Reader::new(bytes)).expect("an encoded error decodes") + fn decode(bytes: &[u8]) -> HostValue { + HostValue::decode(bytes).expect("an encoded error decodes") } - fn get<'a>(value: &'a FfiValue, key: &str) -> Option<&'a FfiValue> { - match value { - FfiValue::Object(entries) => entries.iter().find(|(k, _)| k == key).map(|(_, v)| v), - _ => None, - } + fn get<'a>(value: &'a HostValue, key: &str) -> Option<&'a HostValue> { + value.get(key) } - fn string(value: Option<&FfiValue>) -> Option { - match value? { - FfiValue::String(s) => String::from_utf8(s.risky_ref().to_vec()).ok(), - _ => None, - } + fn string(value: Option<&HostValue>) -> Option { + value?.as_text().map(str::to_owned) } #[derive(Debug, thiserror::Error, Diagnostic)] @@ -473,9 +435,9 @@ mod tests { assert_eq!(string(get(&value, "severity")).as_deref(), Some("error")); assert!(matches!( get(get(&value, "fields").expect("fields"), "count"), - Some(FfiValue::UInt64(2)) + Some(HostValue::UInt(2)) )); - let Some(FfiValue::Array(causes)) = get(&value, "causes") else { + let Some(HostValue::List(causes)) = get(&value, "causes") else { panic!("causes is a list"); }; assert_eq!(causes.len(), 2); @@ -504,7 +466,7 @@ mod tests { struct Wrapper(#[source] Foreign); let _ = record(&Wrapper(Foreign), serde_json::Map::new(), STATUS_INTERNAL); let value = decode(&take().expect("recorded")); - let Some(FfiValue::Array(causes)) = get(&value, "causes") else { + let Some(HostValue::List(causes)) = get(&value, "causes") else { panic!("causes is a list"); }; assert_eq!( @@ -550,7 +512,7 @@ mod tests { ); assert!(matches!( get(get(&value, "fields").expect("fields"), "status"), - Some(FfiValue::UInt64(42)) + Some(HostValue::UInt(42)) )); } diff --git a/packages/stack-guest-abi/src/lib.rs b/packages/stack-guest-abi/src/lib.rs index f635c2855..6b4db1a3f 100644 --- a/packages/stack-guest-abi/src/lib.rs +++ b/packages/stack-guest-abi/src/lib.rs @@ -54,8 +54,8 @@ //! both are tested natively and the guests' leak tests run the real //! lifecycle. //! - [`last_error`] — the full error behind the most recent failed export: -//! its code, message, help, fields and causes, encoded once in the -//! transport codec for `se_last_error`. The status number stays the fast +//! its code, message, help, fields and causes, encoded once as a +//! transport frame for `se_last_error`. The status number stays the fast //! path; this is the detail a host asks for after it. //! - [`status`] — the status table. **One numbering for every guest**: the //! codes below keep their values for good, and a guest that needs more @@ -65,6 +65,8 @@ //! call and gets both response buffers back through the registry. //! - [`headers`] — the `name: value` line format request and response //! headers cross the import in. +//! - [`value`] — [`HostValue`](value::HostValue), a value a guest hands its +//! host, and the transport frame it crosses as. //! //! What is deliberately *not* here: anything that names a crate a guest is //! built over. The ZeroKMS connection over the transport, the token import @@ -102,6 +104,7 @@ pub mod call; pub mod headers; pub mod last_error; pub mod status; +pub mod value; #[cfg(target_arch = "wasm32")] pub mod abi; diff --git a/packages/stack-guest-abi/src/value.rs b/packages/stack-guest-abi/src/value.rs new file mode 100644 index 000000000..013013566 --- /dev/null +++ b/packages/stack-guest-abi/src/value.rs @@ -0,0 +1,350 @@ +//! [`HostValue`]: a value a guest hands its host, and how it crosses the +//! boundary. +//! +//! Every structured output a guest returns (a list of workspace ids, the +//! fields of a token, the last error) crosses as one transport frame, the +//! encoding vitaminc's `Frame` and `FrameWriter` define and the Go module +//! reads with `vcffi`. A `HostValue` describes itself to vitaminc's +//! [`Plaintext`] model, so [`encode`](HostValue::encode) is vitaminc's own +//! frame writer, and [`decode`](HostValue::decode) reads a frame back +//! through a [`Sink`], for the tests that check what a host is given. + +use vitaminc_aead::plaintext::{ + visit_map, visit_seq, Float, Logical, MapSink, Physical, Plaintext, SeqSink, Sink, SourceError, +}; +use vitaminc_aead_frame::{to_frame, Frame, FrameError, FrameWriteError}; +use vitaminc_protected::{Controlled, Protected}; + +/// A value a guest hands its host: a scalar, a list or a map with string +/// keys. +/// +/// Integers are 64-bit, unsigned where the value cannot be negative, so the +/// host reads one integer type per field. Bytes are held in [`Protected`], +/// as a client key is, and wiped when the value drops. +pub enum HostValue { + /// No value. + Null, + /// A boolean. + Bool(bool), + /// An unsigned integer. + UInt(u64), + /// A signed integer. + Int(i64), + /// A float. + Float(f64), + /// Text. + Text(String), + /// Bytes. + Bytes(Protected>), + /// A list. + List(Vec), + /// A map, in the order its entries were given. + Map(Vec<(String, HostValue)>), +} + +impl HostValue { + /// Text, from anything that converts to a `String`. + pub fn text(value: impl Into) -> Self { + Self::Text(value.into()) + } + + /// A structured field as a host value: a JSON number becomes an + /// unsigned integer if it is one, then a signed integer, then a float. + pub fn from_json(value: serde_json::Value) -> Self { + match value { + serde_json::Value::Null => Self::Null, + serde_json::Value::Bool(value) => Self::Bool(value), + serde_json::Value::Number(number) => { + if let Some(value) = number.as_u64() { + Self::UInt(value) + } else if let Some(value) = number.as_i64() { + Self::Int(value) + } else { + Self::Float(number.as_f64().unwrap_or(f64::NAN)) + } + } + serde_json::Value::String(value) => Self::Text(value), + serde_json::Value::Array(items) => { + Self::List(items.into_iter().map(Self::from_json).collect()) + } + serde_json::Value::Object(entries) => Self::Map( + entries + .into_iter() + .map(|(key, value)| (key, Self::from_json(value))) + .collect(), + ), + } + } + + /// This value as a transport frame. + /// + /// The frame is returned bare, as every guest output is: it is copied + /// into a registered buffer, which the registry wipes when the host + /// releases it. + /// + /// # Errors + /// + /// [`FrameWriteError`] for a map with a repeated key or a value nested + /// deeper than a frame carries. A guest builds its outputs itself, so + /// either is a bug in the guest. + pub fn encode(&self) -> Result, FrameWriteError> { + to_frame(self).map(Protected::risky_unwrap) + } + + /// Read one value from a transport frame. + /// + /// # Errors + /// + /// [`DecodeError`] when the bytes are not exactly one frame, or the + /// frame holds a scalar a `HostValue` does not (a 32-bit float, a date). + pub fn decode(bytes: &[u8]) -> Result { + let frame = Frame::parse(bytes).map_err(DecodeError::Frame)?; + frame.visit(ValueSink(Root)) + } + + /// The entry under `key`, if this is a map that has one. + pub fn get(&self, key: &str) -> Option<&HostValue> { + match self { + Self::Map(entries) => entries.iter().find(|(k, _)| k == key).map(|(_, v)| v), + _ => None, + } + } + + /// The text, if this is text. + pub fn as_text(&self) -> Option<&str> { + match self { + Self::Text(text) => Some(text), + _ => None, + } + } +} + +/// Why [`HostValue::decode`] refused its bytes. +#[derive(Debug, thiserror::Error)] +pub enum DecodeError { + /// The bytes are not exactly one well-formed frame. + #[error(transparent)] + Frame(FrameError), + /// The frame holds a scalar no `HostValue` holds. + #[error("the frame holds a scalar a host value does not")] + Scalar, + /// The frame's source failed (it cannot: a parsed frame is complete). + #[error("the frame could not be read")] + Source, +} + +impl vitaminc_aead::plaintext::Error for DecodeError { + fn source(_: SourceError) -> Self { + Self::Source + } +} + +/// Walks the value, leaving it where it is. +impl Plaintext for &HostValue { + fn visit(self, sink: S) -> Result { + match self { + HostValue::Null => sink.scalar(None, Physical::Null), + HostValue::Bool(b) => sink.scalar(None, Physical::Bool(*b)), + HostValue::UInt(u) => sink.scalar(None, Physical::Int((*u).into())), + HostValue::Int(i) => sink.scalar(None, Physical::Int((*i).into())), + HostValue::Float(f) => sink.scalar(None, Physical::Float(Float::F64(*f))), + HostValue::Text(text) => sink.scalar(None, Physical::Text(text)), + HostValue::Bytes(bytes) => sink.scalar(None, Physical::Bytes(bytes.risky_ref())), + HostValue::List(items) => visit_seq(sink, Some(items.len()), items), + HostValue::Map(entries) => visit_map( + sink, + Some(entries.len()), + entries.iter().map(|(key, value)| (key.as_str(), value)), + ), + } + } +} + +/// Where a finished value goes: back to the caller, or into its parent. +trait Put { + type Ok; + fn put(self, value: HostValue) -> Self::Ok; +} + +struct Root; + +impl Put for Root { + type Ok = HostValue; + fn put(self, value: HostValue) -> HostValue { + value + } +} + +struct Element<'s>(&'s mut Vec); + +impl Put for Element<'_> { + type Ok = (); + fn put(self, value: HostValue) { + self.0.push(value); + } +} + +struct Entry<'s>(&'s mut Vec<(String, HostValue)>, String); + +impl Put for Entry<'_> { + type Ok = (); + fn put(self, value: HostValue) { + self.0.push((self.1, value)); + } +} + +/// Builds the value a frame describes. +struct ValueSink(O); + +impl Sink for ValueSink { + type Ok = O::Ok; + type Error = DecodeError; + type Seq = ValueSeq; + type Map = ValueMap; + + fn scalar(self, ty: Option, value: Physical<'_>) -> Result { + let value = match (ty, value) { + (None, Physical::Null) => HostValue::Null, + (None, Physical::Bool(b)) => HostValue::Bool(b), + (None, Physical::Int(int)) => { + if let Some(u) = int.to_u128().and_then(|u| u64::try_from(u).ok()) { + HostValue::UInt(u) + } else if let Some(i) = int.to_i128().and_then(|i| i64::try_from(i).ok()) { + HostValue::Int(i) + } else { + return Err(DecodeError::Scalar); + } + } + (None, Physical::Float(Float::F64(f))) => HostValue::Float(f), + (None, Physical::Text(text)) => HostValue::Text(text.to_owned()), + (None, Physical::Bytes(bytes)) => HostValue::Bytes(Protected::new(bytes.to_vec())), + _ => return Err(DecodeError::Scalar), + }; + Ok(self.0.put(value)) + } + + fn seq(self, _: Option) -> Result, DecodeError> { + Ok(ValueSeq { + out: self.0, + items: Vec::new(), + }) + } + + fn map(self, _: Option) -> Result, DecodeError> { + Ok(ValueMap { + out: self.0, + entries: Vec::new(), + }) + } + + /// A guest never hands its host a passthrough. + fn passthrough(self, _: P) -> Result { + Err(DecodeError::Scalar) + } +} + +struct ValueSeq { + out: O, + items: Vec, +} + +impl SeqSink for ValueSeq { + type Ok = O::Ok; + type Error = DecodeError; + type Element<'s> + = ValueSink> + where + Self: 's; + + fn element(&mut self) -> Result>, DecodeError> { + Ok(ValueSink(Element(&mut self.items))) + } + + fn end(self) -> Result { + Ok(self.out.put(HostValue::List(self.items))) + } +} + +struct ValueMap { + out: O, + entries: Vec<(String, HostValue)>, +} + +impl MapSink for ValueMap { + type Ok = O::Ok; + type Error = DecodeError; + type Entry<'s> + = ValueSink> + where + Self: 's; + + fn entry(&mut self, key: &str) -> Result>, DecodeError> { + Ok(ValueSink(Entry(&mut self.entries, key.to_owned()))) + } + + fn end(self) -> Result { + Ok(self.out.put(HostValue::Map(self.entries))) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_value_reads_back_as_it_was_written() { + let value = HostValue::Map(vec![ + ("null".into(), HostValue::Null), + ("yes".into(), HostValue::Bool(true)), + ("big".into(), HostValue::UInt(u64::MAX)), + ("negative".into(), HostValue::Int(-2)), + ("half".into(), HostValue::Float(0.5)), + ("name".into(), HostValue::text("alice")), + ("key".into(), HostValue::Bytes(Protected::new(vec![1, 2]))), + ( + "ids".into(), + HostValue::List(vec![HostValue::text("a"), HostValue::List(vec![])]), + ), + ]); + let bytes = value.encode().expect("encodes"); + let back = HostValue::decode(&bytes).expect("decodes"); + assert!(matches!(back.get("null"), Some(HostValue::Null))); + assert!(matches!(back.get("yes"), Some(HostValue::Bool(true)))); + assert!(matches!(back.get("big"), Some(HostValue::UInt(u64::MAX)))); + assert!(matches!(back.get("negative"), Some(HostValue::Int(-2)))); + assert!(matches!(back.get("half"), Some(HostValue::Float(f)) if *f == 0.5)); + assert_eq!(back.get("name").and_then(HostValue::as_text), Some("alice")); + assert!(matches!(back.get("key"), Some(HostValue::Bytes(b)) if b.risky_ref() == &[1, 2])); + assert!(matches!(back.get("ids"), Some(HostValue::List(items)) if items.len() == 2)); + } + + #[test] + fn a_map_with_a_repeated_key_does_not_encode() { + let value = HostValue::Map(vec![ + ("a".into(), HostValue::Null), + ("a".into(), HostValue::Null), + ]); + assert!(value.encode().is_err()); + } + + #[test] + fn bytes_that_are_not_one_frame_do_not_decode() { + assert!(matches!(HostValue::decode(&[]), Err(DecodeError::Frame(_)))); + } + + #[test] + fn json_numbers_keep_their_integer_type() { + assert!(matches!( + HostValue::from_json(serde_json::json!(2)), + HostValue::UInt(2) + )); + assert!(matches!( + HostValue::from_json(serde_json::json!(-2)), + HostValue::Int(-2) + )); + assert!(matches!( + HostValue::from_json(serde_json::json!(0.5)), + HostValue::Float(f) if f == 0.5 + )); + } +} From ea275c5c56b59682a8d81f1a4243aec88efc52ad Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Fri, 16 Oct 2026 01:01:34 +1100 Subject: [PATCH 02/15] test(stack-encrypt): build the fuzz crate against the same vitaminc, with no absent marker to fuzz The fuzz crate is its own workspace and compiles stack-encrypt by path, so it carries the same temporary vitaminc pin. check_record's model loses the None node, which ADR 0001 retires, and builds its passthroughs as frames. --- packages/stack-encrypt/fuzz/Cargo.lock | 96 +++++++++++++------ packages/stack-encrypt/fuzz/Cargo.toml | 12 +++ .../fuzz/fuzz_targets/check_record.rs | 22 +++-- 3 files changed, 91 insertions(+), 39 deletions(-) diff --git a/packages/stack-encrypt/fuzz/Cargo.lock b/packages/stack-encrypt/fuzz/Cargo.lock index 086027d53..635728183 100644 --- a/packages/stack-encrypt/fuzz/Cargo.lock +++ b/packages/stack-encrypt/fuzz/Cargo.lock @@ -1520,6 +1520,15 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro-error-attr2" version = "2.0.0" @@ -2033,7 +2042,7 @@ dependencies = [ "thiserror 1.0.69", "uuid", "vitaminc-aead", - "vitaminc-aead-value", + "vitaminc-aead-frame", "vitaminc-encrypt", "vitaminc-hmac", "vitaminc-prf", @@ -2283,6 +2292,36 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "toml_datetime" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.17+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" +dependencies = [ + "winnow", +] + [[package]] name = "tracing" version = "0.1.44" @@ -2474,8 +2513,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vitaminc" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35042fb7a81d72dc43c53fec835e61027f18b4d01cc0f7e12d6c6a6ffbf3bfb9" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "vitaminc-aead", "vitaminc-context", @@ -2488,11 +2526,12 @@ dependencies = [ [[package]] name = "vitaminc-aead" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62cacd4dae485e98cfe5407e919187a8468dd933ea4234be5115b7d13a30f3c6" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "bytes", + "futures-util", "serde", + "thiserror 2.0.18", "vitaminc-aead-derive", "vitaminc-context", "vitaminc-protected", @@ -2503,8 +2542,7 @@ dependencies = [ [[package]] name = "vitaminc-aead-derive" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ff34056b70a8417ad3bf9db73b2bd3be97f448794566c6de2080bd8a7cd9b6b" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "proc-macro2", "quote", @@ -2512,21 +2550,17 @@ dependencies = [ ] [[package]] -name = "vitaminc-aead-value" +name = "vitaminc-aead-frame" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d78cdadbc6374c3bd7c19fb4d99abca693b96953b18fef8c608efbe092cf3ea" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "vitaminc-aead", - "vitaminc-protected", - "zeroize", ] [[package]] name = "vitaminc-context" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7df52501fe33cdb3ee9e155b5242fac184c037d147e58c96e5143ed011252a8" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "mutants", "vitaminc-protected", @@ -2535,8 +2569,7 @@ dependencies = [ [[package]] name = "vitaminc-encrypt" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbd0e1748055e381e02033e08af385de879c4dcf32e0f6739ac0c0cf0c6f13d" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "aes-gcm", "aws-lc-rs", @@ -2549,8 +2582,7 @@ dependencies = [ [[package]] name = "vitaminc-hmac" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15fe558a5e92d4f2b3f811c5b67d863d9255da5f66d08a71d4f0dd3d0011c563" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "hmac", "sha2 0.11.0", @@ -2562,8 +2594,7 @@ dependencies = [ [[package]] name = "vitaminc-prf" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712d31d46d76b38c6b62deb35f516fc5636a35f90f39c0213d0ca9a052b4400b" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "mutants", "thiserror 2.0.18", @@ -2574,8 +2605,7 @@ dependencies = [ [[package]] name = "vitaminc-protected" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7b514f605a63f88874ba46ffea81775fad64994475ab239cb19da159e884b7b" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "bitvec", "digest 0.11.3", @@ -2591,9 +2621,9 @@ dependencies = [ [[package]] name = "vitaminc-protected-derive" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cb0a3d11afbe2c909f0cfc9996fe37fd89b1196eb0b8d89a66e28716360ee09" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ + "proc-macro-crate", "proc-macro2", "quote", "syn 3.0.3", @@ -2602,8 +2632,7 @@ dependencies = [ [[package]] name = "vitaminc-random" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7b1bfab35ee322de37269c6fd94d8cd94047a66f97cf6722e53e3321482167" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "chacha20", "getrandom 0.4.2", @@ -2617,8 +2646,7 @@ dependencies = [ [[package]] name = "vitaminc-random-derives" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0915e2bfb417ce11e23123fb7ec122af5d27d8cfd22d6ad6a2cf7a1c2dbb4712" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "proc-macro2", "quote", @@ -2628,8 +2656,7 @@ dependencies = [ [[package]] name = "vitaminc-traits" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b31ec300e351a7a59e64899d0a2d7e7227cfa74ac0514f4e745acb2cc9335cee" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "anyhow", "bytes", @@ -2991,6 +3018,15 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.51.0" diff --git a/packages/stack-encrypt/fuzz/Cargo.toml b/packages/stack-encrypt/fuzz/Cargo.toml index 5a3bca512..8db1c8589 100644 --- a/packages/stack-encrypt/fuzz/Cargo.toml +++ b/packages/stack-encrypt/fuzz/Cargo.toml @@ -63,3 +63,15 @@ bench = false [workspace] resolver = "2" + +# Temporary, as in the root Cargo.toml: the vitaminc crates stack-encrypt +# builds on come from the branch that implements vitaminc ADR 0001 +# (cipherstash/vitaminc PRs #439 to #445) until vitaminc releases it. +[patch.crates-io] +vitaminc = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } +vitaminc-aead = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } +vitaminc-aead-frame = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } +vitaminc-encrypt = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } +vitaminc-hmac = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } +vitaminc-prf = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } +vitaminc-protected = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } diff --git a/packages/stack-encrypt/fuzz/fuzz_targets/check_record.rs b/packages/stack-encrypt/fuzz/fuzz_targets/check_record.rs index 7b486afc5..72e53aeae 100644 --- a/packages/stack-encrypt/fuzz/fuzz_targets/check_record.rs +++ b/packages/stack-encrypt/fuzz/fuzz_targets/check_record.rs @@ -35,9 +35,11 @@ use stack_encrypt::dynamic::record::{check_record, plan_with, Plan}; use stack_encrypt::dynamic::{ FfiValue, Output, TargetDescriptor, TargetError, TargetResolver, ValueKind, }; -use stack_encrypt::{KeysetCipher, Label, Pending, SealedValue, StackCipher, StackCipherText}; +use stack_encrypt::{ + KeysetCipher, Label, PassthroughFrame, Pending, SealedValue, StackCipher, StackCipherText, +}; use uuid::Uuid; -use vitaminc_protected::Protected; +use vitaminc_protected::{Controlled, Protected}; /// The resolver plans parse under: one producible type, `TextEq` over /// strings, and nothing that runs — `check_record` never reaches a cipher. @@ -264,7 +266,6 @@ impl PlanSpec { #[derive(Arbitrary, Debug)] enum Tree { Single, - None, EmptySequence, EmptyMap, Passthrough, @@ -284,17 +285,20 @@ fn leaf() -> SealedValue { .clone() } +/// A value as a passthrough node: its frame, in the clear. +fn passthrough(value: FfiValue) -> StackCipherText { + let frame = value.to_frame().expect("a scalar frames"); + StackCipherText::Passthrough(PassthroughFrame(frame.risky_unwrap())) +} + impl Tree { fn into_ciphertext(self) -> StackCipherText { match self { Tree::Single => StackCipherText::Single(leaf()), - Tree::None => StackCipherText::None(leaf()), Tree::EmptySequence => StackCipherText::EmptySequence(leaf()), Tree::EmptyMap => StackCipherText::EmptyMap(leaf()), - Tree::Passthrough => StackCipherText::Passthrough(Box::new(FfiValue::Null)), - Tree::PassthroughBytes => StackCipherText::Passthrough(Box::new(FfiValue::Bytes( - Protected::new(b"{}".to_vec()), - ))), + Tree::Passthrough => passthrough(FfiValue::Null), + Tree::PassthroughBytes => passthrough(FfiValue::Bytes(Protected::new(b"{}".to_vec()))), Tree::Sequence(items) => { StackCipherText::Sequence(items.into_iter().map(Tree::into_ciphertext).collect()) } @@ -320,7 +324,7 @@ impl Tree { .all(|(at, (name, _))| !entries[..at].iter().any(|(prior, _)| prior == name)); unique && entries.iter().all(|(_, node)| node.is_clean()) } - Tree::Single | Tree::None | Tree::EmptySequence | Tree::EmptyMap => true, + Tree::Single | Tree::EmptySequence | Tree::EmptyMap => true, } } } From a285ffcac7ecfbf37b9a0fd2869e23581879a3f7 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Fri, 16 Oct 2026 01:01:34 +1100 Subject: [PATCH 03/15] feat(golang)!: carry transport frames between the Go module and both guests Both WASI guests build against the same temporary vitaminc pin as the root workspace, and every value crosses as a transport frame. - The encrypt guest decodes inputs with `FfiValue::from_frame`, writes outputs through vitaminc's `FrameWriter` into a buffer still sized before the first byte, and reads and writes ciphertext trees with `vitaminc_aead_frame`'s codec, a passthrough as its frame. - The credential guest's outputs are `HostValue`s. - The Go module moves vcffi and vcvalue to the same vitaminc commit, whose container tags and leaf kinds match the frame. The absent leaf kind is gone from the binding's leaf set. vcffi sends an 8- or 16-bit integer at its own width, which the engine reads as the 32-bit integer of its sign, so a generated field seals as it did. --- languages/golang/auth/guest/Cargo.lock | 92 +++++++++++----- languages/golang/auth/guest/Cargo.toml | 15 ++- languages/golang/auth/guest/src/lib.rs | 4 +- languages/golang/auth/guest/src/ops.rs | 56 +++++----- languages/golang/auth/guest/tests/leak.rs | 32 +++--- languages/golang/encrypt/ciphertext.go | 5 +- languages/golang/encrypt/guest/Cargo.lock | 102 +++++++++++++----- languages/golang/encrypt/guest/Cargo.toml | 24 +++-- languages/golang/encrypt/guest/src/abi.rs | 9 +- languages/golang/encrypt/guest/src/config.rs | 2 +- languages/golang/encrypt/guest/src/lib.rs | 5 +- languages/golang/encrypt/guest/src/ops.rs | 98 ++++++++--------- languages/golang/encrypt/guest/src/options.rs | 2 +- languages/golang/encrypt/guest/src/status.rs | 2 +- languages/golang/encrypt/guest/src/targets.rs | 14 +-- languages/golang/encrypt/guest/tests/leak.rs | 37 ++++--- .../golang/encrypt/guest/tests/native_ops.rs | 96 +++++++++-------- languages/golang/encrypt/unit_test.go | 1 - languages/golang/go.mod | 4 +- languages/golang/go.sum | 4 + languages/golang/stashgen/emit.go | 7 +- 21 files changed, 360 insertions(+), 251 deletions(-) diff --git a/languages/golang/auth/guest/Cargo.lock b/languages/golang/auth/guest/Cargo.lock index 1616b6d77..141489710 100644 --- a/languages/golang/auth/guest/Cargo.lock +++ b/languages/golang/auth/guest/Cargo.lock @@ -1365,6 +1365,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro-error-attr2" version = "2.0.0" @@ -1858,7 +1867,6 @@ dependencies = [ "stack-profile", "tempfile", "url", - "vitaminc-aead-value", "vitaminc-protected", "zeroize", ] @@ -1870,7 +1878,8 @@ dependencies = [ "miette", "serde_json", "thiserror 1.0.69", - "vitaminc-aead-value", + "vitaminc-aead", + "vitaminc-aead-frame", "vitaminc-protected", "zeroize", ] @@ -2066,6 +2075,36 @@ dependencies = [ "syn 3.0.6", ] +[[package]] +name = "toml_datetime" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.17+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" +dependencies = [ + "winnow", +] + [[package]] name = "tracing" version = "0.1.44" @@ -2247,8 +2286,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vitaminc" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35042fb7a81d72dc43c53fec835e61027f18b4d01cc0f7e12d6c6a6ffbf3bfb9" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "vitaminc-aead", "vitaminc-context", @@ -2261,11 +2299,12 @@ dependencies = [ [[package]] name = "vitaminc-aead" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62cacd4dae485e98cfe5407e919187a8468dd933ea4234be5115b7d13a30f3c6" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "bytes", + "futures-util", "serde", + "thiserror 2.0.20", "vitaminc-aead-derive", "vitaminc-context", "vitaminc-protected", @@ -2276,8 +2315,7 @@ dependencies = [ [[package]] name = "vitaminc-aead-derive" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ff34056b70a8417ad3bf9db73b2bd3be97f448794566c6de2080bd8a7cd9b6b" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "proc-macro2", "quote", @@ -2285,21 +2323,17 @@ dependencies = [ ] [[package]] -name = "vitaminc-aead-value" +name = "vitaminc-aead-frame" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d78cdadbc6374c3bd7c19fb4d99abca693b96953b18fef8c608efbe092cf3ea" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "vitaminc-aead", - "vitaminc-protected", - "zeroize", ] [[package]] name = "vitaminc-context" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7df52501fe33cdb3ee9e155b5242fac184c037d147e58c96e5143ed011252a8" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "mutants", "vitaminc-protected", @@ -2308,8 +2342,7 @@ dependencies = [ [[package]] name = "vitaminc-encrypt" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbd0e1748055e381e02033e08af385de879c4dcf32e0f6739ac0c0cf0c6f13d" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "aes-gcm", "aws-lc-rs", @@ -2322,8 +2355,7 @@ dependencies = [ [[package]] name = "vitaminc-protected" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7b514f605a63f88874ba46ffea81775fad64994475ab239cb19da159e884b7b" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "bitvec", "digest 0.11.3", @@ -2339,9 +2371,9 @@ dependencies = [ [[package]] name = "vitaminc-protected-derive" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cb0a3d11afbe2c909f0cfc9996fe37fd89b1196eb0b8d89a66e28716360ee09" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ + "proc-macro-crate", "proc-macro2", "quote", "syn 3.0.6", @@ -2350,8 +2382,7 @@ dependencies = [ [[package]] name = "vitaminc-random" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7b1bfab35ee322de37269c6fd94d8cd94047a66f97cf6722e53e3321482167" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "chacha20", "getrandom 0.4.3", @@ -2365,8 +2396,7 @@ dependencies = [ [[package]] name = "vitaminc-random-derives" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0915e2bfb417ce11e23123fb7ec122af5d27d8cfd22d6ad6a2cf7a1c2dbb4712" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "proc-macro2", "quote", @@ -2376,8 +2406,7 @@ dependencies = [ [[package]] name = "vitaminc-traits" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b31ec300e351a7a59e64899d0a2d7e7227cfa74ac0514f4e745acb2cc9335cee" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "anyhow", "bytes", @@ -2622,6 +2651,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.57.1" diff --git a/languages/golang/auth/guest/Cargo.toml b/languages/golang/auth/guest/Cargo.toml index d3928e995..39973dab4 100644 --- a/languages/golang/auth/guest/Cargo.toml +++ b/languages/golang/auth/guest/Cargo.toml @@ -18,6 +18,17 @@ publish = false [workspace] +# Temporary, as in the root Cargo.toml: the vitaminc crates stack-guest-abi +# and the stack crates build on come from the branch that implements +# vitaminc ADR 0001 (cipherstash/vitaminc PRs #439 to #445) until vitaminc +# releases it. +[patch.crates-io] +vitaminc = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } +vitaminc-aead = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } +vitaminc-aead-frame = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } +vitaminc-encrypt = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } +vitaminc-protected = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } + [lib] # cdylib: the .wasm guest module. rlib: lets the ops/status modules # unit-test natively (`cargo test` here, no wasm toolchain needed). @@ -41,10 +52,6 @@ stack-guest-abi = { path = "../../../../packages/stack-guest-abi" } # hostname and the process id are native-only there, and creating a device # identity is CLI territory. stack-profile = { path = "../../../../packages/stack-profile" } -# The FFI codec for the structured results (a list of workspace ids, the -# fields of a token): the one codec every binding carries. Same vitaminc -# version the crypto guest builds against. -vitaminc-aead-value = "0.5.0" # The deserialized form of `secretkey.json` is wiped when it drops. zeroize = { version = "1", features = ["derive"] } diff --git a/languages/golang/auth/guest/src/lib.rs b/languages/golang/auth/guest/src/lib.rs index fb9986d4c..a95e5c511 100644 --- a/languages/golang/auth/guest/src/lib.rs +++ b/languages/golang/auth/guest/src/lib.rs @@ -58,8 +58,8 @@ //! //! Profile inputs are UTF-8 strings: the store directory (a guest path under //! the mount), a workspace id, a filename. Outputs are either a UTF-8 string -//! (an id, a path) or a value in vitaminc's FFI codec -//! (`vitaminc_aead_value::transport`): a list of ids, or the fields of +//! (an id, a path) or a vitaminc transport frame +//! ([`HostValue`](stack_guest_abi::value::HostValue)): a list of ids, or the fields of //! `secretkey.json`, `auth.json` or `device.json` as an object. The client //! key in `secretkey.json` crosses as the text the file holds; the Go side //! wraps it in its opaque `ClientKey` and wipes its transport copy. A diff --git a/languages/golang/auth/guest/src/ops.rs b/languages/golang/auth/guest/src/ops.rs index 6a32bfa5d..ab8bb36ed 100644 --- a/languages/golang/auth/guest/src/ops.rs +++ b/languages/golang/auth/guest/src/ops.rs @@ -30,8 +30,8 @@ use serde::Deserialize; use stack_auth::Token; +use stack_guest_abi::value::HostValue; use stack_profile::{DeviceIdentity, ProfileStore}; -use vitaminc_aead_value::{transport as codec, FfiValue}; use zeroize::{Zeroize, ZeroizeOnDrop}; use stack_guest_abi::last_error::{internal, malformed}; @@ -76,18 +76,18 @@ fn text(bytes: &[u8]) -> Result<&str, u32> { std::str::from_utf8(bytes).map_err(|_| malformed("an input is not UTF-8")) } -fn string(value: impl Into) -> FfiValue { - FfiValue::String(value.into().into()) +fn string(value: impl Into) -> HostValue { + HostValue::text(value) } -fn optional(value: Option<&str>) -> FfiValue { - value.map_or(FfiValue::Null, string) +fn optional(value: Option<&str>) -> HostValue { + value.map_or(HostValue::Null, string) } -fn encode(value: FfiValue) -> Result, u32> { - let mut out = Vec::new(); - codec::encode_value(value, &mut out).map_err(|_| internal("an output did not encode"))?; - Ok(out) +fn encode(value: HostValue) -> Result, u32> { + value + .encode() + .map_err(|_| internal("an output did not encode")) } /// The current workspace id, as text. @@ -122,7 +122,7 @@ pub fn list_workspaces(dir: &[u8]) -> Result, u32> { let ids = store(dir)? .list_workspaces() .map_err(|e| fail_profile(&e))?; - encode(FfiValue::Array(ids.into_iter().map(string).collect())) + encode(HostValue::List(ids.into_iter().map(string).collect())) } /// The directory of the store scoped to workspace `id`, as text: what @@ -165,11 +165,11 @@ pub fn secret_key(dir: &[u8]) -> Result, u32> { // fields cannot be moved out of it directly. let client_id = std::mem::take(&mut file.client_id); let client_key = std::mem::take(&mut file.client_key); - encode(FfiValue::Object(vec![ + encode(HostValue::Map(vec![ ("client_id".to_string(), string(client_id)), ( "client_key".to_string(), - FfiValue::Bytes(client_key.into_bytes().into()), + HostValue::Bytes(client_key.into_bytes().into()), ), ])) } @@ -184,7 +184,7 @@ pub fn token(dir: &[u8]) -> Result, u32> { let token: Token = store(dir)? .load(AUTH_FILENAME) .map_err(|e| fail_profile(&e))?; - encode(FfiValue::Object(vec![ + encode(HostValue::Map(vec![ ( "access_token".to_string(), string(token.access_token().as_str()), @@ -192,7 +192,7 @@ pub fn token(dir: &[u8]) -> Result, u32> { ("token_type".to_string(), string(token.token_type())), ( "expires_at".to_string(), - FfiValue::UInt64(token.expires_at()), + HostValue::UInt(token.expires_at()), ), ("region".to_string(), optional(token.region())), ("client_id".to_string(), optional(token.client_id())), @@ -214,7 +214,7 @@ pub fn has_token(dir: &[u8]) -> Result, u32> { /// `{device_instance_id, device_name}`. Creating one is the CLI's. pub fn device_identity(dir: &[u8]) -> Result, u32> { let identity = DeviceIdentity::load(&store(dir)?).map_err(|e| fail_profile(&e))?; - encode(FfiValue::Object(vec![ + encode(HostValue::Map(vec![ ( "device_instance_id".to_string(), string(identity.device_instance_id.to_string()), @@ -235,12 +235,12 @@ mod tests { t.path().to_str().unwrap().as_bytes().to_vec() } - fn decode(bytes: &[u8]) -> FfiValue { - codec::decode_value(&mut codec::Reader::new(bytes)).unwrap() + fn decode(bytes: &[u8]) -> HostValue { + HostValue::decode(bytes).unwrap() } - fn field<'a>(value: &'a FfiValue, key: &str) -> &'a FfiValue { - let FfiValue::Object(entries) = value else { + fn field<'a>(value: &'a HostValue, key: &str) -> &'a HostValue { + let HostValue::Map(entries) = value else { panic!("not an object"); }; entries @@ -250,16 +250,16 @@ mod tests { .unwrap_or_else(|| panic!("no field {key}")) } - fn as_text(value: &FfiValue) -> String { - let FfiValue::String(s) = value else { + fn as_text(value: &HostValue) -> String { + let HostValue::Text(s) = value else { panic!("not a string"); }; - String::from_utf8(s.risky_ref().to_vec()).unwrap() + s.clone() } - fn as_bytes(value: &FfiValue) -> Vec { + fn as_bytes(value: &HostValue) -> Vec { use vitaminc_protected::Controlled; - let FfiValue::Bytes(b) = value else { + let HostValue::Bytes(b) = value else { panic!("not bytes"); }; b.risky_ref().to_vec() @@ -314,7 +314,7 @@ mod tests { assert_eq!(current_workspace(&d).unwrap(), WS_A.as_bytes()); let listed = decode(&list_workspaces(&d).unwrap()); - let FfiValue::Array(items) = listed else { + let HostValue::List(items) = listed else { panic!("not an array"); }; let ids: Vec = items.iter().map(as_text).collect(); @@ -411,11 +411,11 @@ mod tests { assert_eq!(as_text(field(&tok, "token_type")), "Bearer"); assert!(matches!( field(&tok, "expires_at"), - FfiValue::UInt64(1_800_000_000) + HostValue::UInt(1_800_000_000) )); assert_eq!(as_text(field(&tok, "region")), "ap-southeast-2"); - assert!(matches!(field(&tok, "client_id"), FfiValue::Null)); - let FfiValue::Object(entries) = tok else { + assert!(matches!(field(&tok, "client_id"), HostValue::Null)); + let HostValue::Map(entries) = tok else { panic!("not an object"); }; assert!( diff --git a/languages/golang/auth/guest/tests/leak.rs b/languages/golang/auth/guest/tests/leak.rs index 6d08d4cf5..2b9f23082 100644 --- a/languages/golang/auth/guest/tests/leak.rs +++ b/languages/golang/auth/guest/tests/leak.rs @@ -25,7 +25,7 @@ use stack_auth::AuthError; use stack_auth_guest::ops; use stack_auth_guest::status::fail_auth; use stack_guest_abi::call; -use vitaminc_aead_value::{transport as codec, FfiValue}; +use stack_guest_abi::value::HostValue; const ACCESS_TOKEN: &str = "leak-marker-access-token"; const ACCESS_KEY: &str = "leak-marker-access-key"; @@ -43,18 +43,18 @@ fn failure( what: &str, expect: &[&str], step: impl FnOnce() -> Result, -) -> FfiValue { +) -> HostValue { let _ = call::run(step).expect_err(what); let bytes = call::take_last_error().unwrap_or_else(|| panic!("{what}: no error recorded")); - let error = codec::decode_value(&mut codec::Reader::new(&bytes)).expect("an error decodes"); + let error = HostValue::decode(&bytes).expect("an error decodes"); assert_eq!(codes(&error), expect, "{what}: the codes it reaches"); error } /// An error's code, then its causes' codes, in order. -fn codes(value: &FfiValue) -> Vec { +fn codes(value: &HostValue) -> Vec { let mut out: Vec = text(get(value, "code")).into_iter().collect(); - if let Some(FfiValue::Array(causes)) = get(value, "causes") { + if let Some(HostValue::List(causes)) = get(value, "causes") { out.extend(causes.iter().filter_map(|cause| text(get(cause, "code")))); } out @@ -71,19 +71,19 @@ fn check(text: &[u8], at: &str, found: &mut Vec) { } } -fn leaks(value: &FfiValue, path: &str, found: &mut Vec) { +fn leaks(value: &HostValue, path: &str, found: &mut Vec) { match value { - FfiValue::String(text) => check(text.risky_ref(), path, found), - FfiValue::Bytes(bytes) => { + HostValue::Text(text) => check(text.as_bytes(), path, found), + HostValue::Bytes(bytes) => { use vitaminc_protected::Controlled; check(bytes.risky_ref(), path, found); } - FfiValue::Array(items) => { + HostValue::List(items) => { for (at, item) in items.iter().enumerate() { leaks(item, &format!("{path}[{at}]"), found); } } - FfiValue::Object(entries) => { + HostValue::Map(entries) => { for (key, item) in entries { check(key.as_bytes(), &format!("{path} key"), found); leaks(item, &format!("{path}.{key}"), found); @@ -93,16 +93,16 @@ fn leaks(value: &FfiValue, path: &str, found: &mut Vec) { } } -fn get<'a>(value: &'a FfiValue, key: &str) -> Option<&'a FfiValue> { +fn get<'a>(value: &'a HostValue, key: &str) -> Option<&'a HostValue> { match value { - FfiValue::Object(entries) => entries.iter().find(|(k, _)| k == key).map(|(_, v)| v), + HostValue::Map(entries) => entries.iter().find(|(k, _)| k == key).map(|(_, v)| v), _ => None, } } -fn text(value: Option<&FfiValue>) -> Option { +fn text(value: Option<&HostValue>) -> Option { match value? { - FfiValue::String(s) => String::from_utf8(s.risky_ref().to_vec()).ok(), + HostValue::Text(s) => Some(s.clone()), _ => None, } } @@ -111,7 +111,7 @@ fn dir(path: &Path) -> Vec { path.to_str().expect("utf8 temp dir").as_bytes().to_vec() } -fn scenarios() -> Vec<(&'static str, FfiValue)> { +fn scenarios() -> Vec<(&'static str, HostValue)> { let root = tempfile::tempdir().expect("temp dir"); let store = dir(root.path()); let workspace = root.path().join("workspaces").join("AAAAAAAAAAAAAAAA"); @@ -268,7 +268,7 @@ fn a_profile_json_error_points_at_the_line() { Some("stack_profile::json") ); let fields = get(&error, "fields").expect("fields"); - assert!(matches!(get(fields, "line"), Some(FfiValue::UInt64(2)))); + assert!(matches!(get(fields, "line"), Some(HostValue::UInt(2)))); assert!( text(get(&error, "help")).is_some(), "the help says how to fix it" diff --git a/languages/golang/encrypt/ciphertext.go b/languages/golang/encrypt/ciphertext.go index 242d85d67..7d70475d8 100644 --- a/languages/golang/encrypt/ciphertext.go +++ b/languages/golang/encrypt/ciphertext.go @@ -50,8 +50,9 @@ func scanBytes(kind string, src any) ([]byte, error) { } // otherLeaf is a leaf of a kind the SDK does not store: the authenticated -// markers for an absent value, an empty sequence and an empty map, which a -// record field never produces. Decoding one is an error at the use site. +// markers for an empty sequence and an empty map, which a record field never +// produces. Decoding one is an error at the use site. An absent value is a +// sealed null, a Ciphertext like any other value. type otherLeaf struct { kind vcffi.LeafKind bytes []byte diff --git a/languages/golang/encrypt/guest/Cargo.lock b/languages/golang/encrypt/guest/Cargo.lock index f9ce384e9..48458d72d 100644 --- a/languages/golang/encrypt/guest/Cargo.lock +++ b/languages/golang/encrypt/guest/Cargo.lock @@ -1488,6 +1488,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro-error-attr2" version = "2.0.0" @@ -2078,7 +2087,7 @@ dependencies = [ "thiserror 1.0.69", "uuid", "vitaminc-aead", - "vitaminc-aead-value", + "vitaminc-aead-frame", "vitaminc-encrypt", "vitaminc-hmac", "vitaminc-prf", @@ -2111,7 +2120,7 @@ dependencies = [ "stack-guest-abi", "stack-kms", "uuid", - "vitaminc-aead-value", + "vitaminc-aead-frame", "vitaminc-protected", "zeroize", "zerokms-protocol", @@ -2124,7 +2133,8 @@ dependencies = [ "miette", "serde_json", "thiserror 1.0.69", - "vitaminc-aead-value", + "vitaminc-aead", + "vitaminc-aead-frame", "vitaminc-protected", "zeroize", ] @@ -2359,6 +2369,36 @@ dependencies = [ "syn 3.0.4", ] +[[package]] +name = "toml_datetime" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.17+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" +dependencies = [ + "winnow", +] + [[package]] name = "tracing" version = "0.1.44" @@ -2572,8 +2612,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vitaminc" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35042fb7a81d72dc43c53fec835e61027f18b4d01cc0f7e12d6c6a6ffbf3bfb9" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "vitaminc-aead", "vitaminc-context", @@ -2586,11 +2625,12 @@ dependencies = [ [[package]] name = "vitaminc-aead" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62cacd4dae485e98cfe5407e919187a8468dd933ea4234be5115b7d13a30f3c6" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "bytes", + "futures-util", "serde", + "thiserror 2.0.20", "vitaminc-aead-derive", "vitaminc-context", "vitaminc-protected", @@ -2601,14 +2641,21 @@ dependencies = [ [[package]] name = "vitaminc-aead-derive" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ff34056b70a8417ad3bf9db73b2bd3be97f448794566c6de2080bd8a7cd9b6b" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "proc-macro2", "quote", "syn 3.0.4", ] +[[package]] +name = "vitaminc-aead-frame" +version = "0.5.1" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +dependencies = [ + "vitaminc-aead", +] + [[package]] name = "vitaminc-aead-value" version = "0.5.1" @@ -2623,8 +2670,7 @@ dependencies = [ [[package]] name = "vitaminc-context" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7df52501fe33cdb3ee9e155b5242fac184c037d147e58c96e5143ed011252a8" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "mutants", "vitaminc-protected", @@ -2633,8 +2679,7 @@ dependencies = [ [[package]] name = "vitaminc-encrypt" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbd0e1748055e381e02033e08af385de879c4dcf32e0f6739ac0c0cf0c6f13d" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "aes-gcm", "aws-lc-rs", @@ -2647,8 +2692,7 @@ dependencies = [ [[package]] name = "vitaminc-hmac" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15fe558a5e92d4f2b3f811c5b67d863d9255da5f66d08a71d4f0dd3d0011c563" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "hmac", "sha2 0.11.0", @@ -2660,8 +2704,7 @@ dependencies = [ [[package]] name = "vitaminc-prf" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712d31d46d76b38c6b62deb35f516fc5636a35f90f39c0213d0ca9a052b4400b" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "mutants", "thiserror 2.0.20", @@ -2672,8 +2715,7 @@ dependencies = [ [[package]] name = "vitaminc-protected" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7b514f605a63f88874ba46ffea81775fad64994475ab239cb19da159e884b7b" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "bitvec", "digest 0.11.3", @@ -2689,9 +2731,9 @@ dependencies = [ [[package]] name = "vitaminc-protected-derive" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cb0a3d11afbe2c909f0cfc9996fe37fd89b1196eb0b8d89a66e28716360ee09" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ + "proc-macro-crate", "proc-macro2", "quote", "syn 3.0.4", @@ -2700,8 +2742,7 @@ dependencies = [ [[package]] name = "vitaminc-random" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7b1bfab35ee322de37269c6fd94d8cd94047a66f97cf6722e53e3321482167" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "chacha20", "getrandom 0.4.3", @@ -2715,8 +2756,7 @@ dependencies = [ [[package]] name = "vitaminc-random-derives" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0915e2bfb417ce11e23123fb7ec122af5d27d8cfd22d6ad6a2cf7a1c2dbb4712" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "proc-macro2", "quote", @@ -2726,8 +2766,7 @@ dependencies = [ [[package]] name = "vitaminc-traits" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b31ec300e351a7a59e64899d0a2d7e7227cfa74ac0514f4e745acb2cc9335cee" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "anyhow", "bytes", @@ -2981,6 +3020,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen" version = "0.57.1" diff --git a/languages/golang/encrypt/guest/Cargo.toml b/languages/golang/encrypt/guest/Cargo.toml index ef9814ecf..4cef89332 100644 --- a/languages/golang/encrypt/guest/Cargo.toml +++ b/languages/golang/encrypt/guest/Cargo.toml @@ -17,6 +17,18 @@ publish = false [workspace] +# Temporary, as in the root Cargo.toml: the vitaminc crates this guest and +# stack-encrypt build on come from the branch that implements vitaminc ADR +# 0001 (cipherstash/vitaminc PRs #439 to #445) until vitaminc releases it. +[patch.crates-io] +vitaminc = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } +vitaminc-aead = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } +vitaminc-aead-frame = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } +vitaminc-encrypt = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } +vitaminc-hmac = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } +vitaminc-prf = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } +vitaminc-protected = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } + [lib] # cdylib: the .wasm guest module. rlib: lets the ops/config/status modules # unit-test natively (`cargo test` here, no wasm toolchain needed). @@ -61,12 +73,12 @@ stack-kms = { path = "../../../../packages/stack-kms", default-features = false eql-bindings = { path = "../../../../packages/eql/crates/eql-bindings", features = ["stack-encrypt"], optional = true } zerokms-protocol = "=0.12.31" -# The FFI codec + `FfiValue` tree, shared with vitaminc's own guest — one -# codec, not a fork. Same vitaminc version stack-encrypt builds against -# (see the comment in packages/stack-encrypt/Cargo.toml). A different -# version here fails at the `FfiValue: Decrypt` bound, since the aead crate -# would be duplicated. -vitaminc-aead-value = "0.5.1" +# The transport frame and ciphertext codec, shared with vitaminc's own +# guest — one codec, not a fork. Same vitaminc version stack-encrypt builds +# against (see the comment in packages/stack-encrypt/Cargo.toml). A +# different version here fails at the `FfiValue: Decrypt` bound, since the +# aead crate would be duplicated. +vitaminc-aead-frame = "0.5.1" vitaminc-protected = "0.5.1" futures = { version = "0.3", default-features = false, features = ["executor"] } diff --git a/languages/golang/encrypt/guest/src/abi.rs b/languages/golang/encrypt/guest/src/abi.rs index 5e5d1c675..7abbdd660 100644 --- a/languages/golang/encrypt/guest/src/abi.rs +++ b/languages/golang/encrypt/guest/src/abi.rs @@ -72,12 +72,11 @@ use std::cell::{Cell, RefCell}; use std::panic::{catch_unwind, AssertUnwindSafe}; use futures::executor::block_on; +use stack_encrypt::dynamic::FfiValue; use stack_encrypt::{KeysetCipher, StackCipher}; use stack_guest_abi::abi::{export, input, take_plaintext, wipe_input}; use stack_guest_abi::buffers; use stack_guest_abi::last_error::{self, malformed, out_of_order}; -use vitaminc_aead_value::transport as codec; -use vitaminc_aead_value::FfiValue; use crate::ops; use crate::options::{parse_options, parse_selector, scope_for, KeysetSelector, Side}; @@ -111,8 +110,8 @@ thread_local! { /// Decode one codec-encoded input. fn decode(bytes: &[u8]) -> Result { - codec::decode_value(&mut codec::Reader::new(bytes)) - .map_err(|_| malformed("an input is not a value in the transport codec")) + FfiValue::from_frame(bytes) + .map_err(|_| malformed("an input is not a value in the transport frame")) } /// Run `f` with the instance's cipher, or report `STATUS_STATE` when there @@ -336,7 +335,7 @@ pub unsafe extern "C" fn se_keyset(sel_ptr: *const u8, sel_len: u32) -> u64 { /// /// The context is one part — a string, bytes, or an `i32`/`i64`/`u32`/`u64` /// — or an array of parts, which may nest as deep as the transport codec -/// allows (`vitaminc_aead_value::transport::MAX_DEPTH` levels, counted from +/// allows (`vitaminc_aead_frame::MAX_DEPTH` levels, counted from /// the root of the encoded value; deeper is refused as `STATUS_ENCODING` /// before the context is parsed). [`stack_encrypt::dynamic::context`] is the one home of /// that grammar and of which Rust context each shape spells. diff --git a/languages/golang/encrypt/guest/src/config.rs b/languages/golang/encrypt/guest/src/config.rs index 800461444..e31f1e7d7 100644 --- a/languages/golang/encrypt/guest/src/config.rs +++ b/languages/golang/encrypt/guest/src/config.rs @@ -25,9 +25,9 @@ use std::num::NonZeroUsize; +use stack_encrypt::dynamic::FfiValue; use stack_kms::{ClientKey, ZeroKmsEndpoint}; use uuid::Uuid; -use vitaminc_aead_value::FfiValue; use zeroize::Zeroizing; /// A parse failure, carrying which key was at fault. Maps to diff --git a/languages/golang/encrypt/guest/src/lib.rs b/languages/golang/encrypt/guest/src/lib.rs index dfe7fbbdb..e402e14d0 100644 --- a/languages/golang/encrypt/guest/src/lib.rs +++ b/languages/golang/encrypt/guest/src/lib.rs @@ -78,8 +78,9 @@ //! //! On wasm32 `vitaminc-encrypt` uses its pure-Rust (RustCrypto `aes-gcm`) //! backend; the trade-offs are documented there. Values cross the boundary -//! in the vitaminc FFI codec (`vitaminc_aead_value::transport` — an FFI -//! encoding, not a storage format); the leaves inside a ciphertext tree are +//! as vitaminc transport frames and ciphertext trees in vitaminc's +//! ciphertext codec (`vitaminc_aead_frame` — FFI encodings, not a storage +//! format); the leaves inside a ciphertext tree are //! the *frozen* `SealedValue` byte encoding from Phase 2, so a leaf lifted //! out of a tree is exactly what a database column holds. diff --git a/languages/golang/encrypt/guest/src/ops.rs b/languages/golang/encrypt/guest/src/ops.rs index c33466ee7..30d5008a9 100644 --- a/languages/golang/encrypt/guest/src/ops.rs +++ b/languages/golang/encrypt/guest/src/ops.rs @@ -4,9 +4,10 @@ //! module wires them to the session table and the packed ABI; nothing in //! here knows about linear memory. //! -//! Values and ciphertext trees cross the boundary in the vitaminc FFI codec -//! (`vitaminc_aead_value::transport`) — the same codec the vitaminc guest -//! uses, so the Go side carries exactly one codec. A ciphertext tree's +//! Values cross the boundary as vitaminc transport frames, and ciphertext +//! trees in vitaminc's ciphertext codec (`vitaminc_aead_frame`) — the same +//! encodings the vitaminc guest uses, so the Go side carries exactly one +//! codec. A ciphertext tree's //! leaves are re-encoded through [`SealedValue::to_bytes`] / //! [`SealedValue::from_bytes`]: the codec sees an opaque byte-string leaf, //! and the bytes inside it are the frozen storage encoding a database column @@ -35,15 +36,15 @@ //! ABI's numeric term kinds, and the mapping from a library error to a //! status code. -use stack_encrypt::dynamic::{self, Scalar, Scope}; +use stack_encrypt::dynamic::{self, FfiValue, Scalar, Scope}; use stack_encrypt::sem::MatchOptions; use stack_encrypt::target::IndexSpec; use stack_encrypt::ErrorPayload; use stack_encrypt::{ - BoxedPassthrough, CipherText, KeysetCipher, Label, SealedValue, StackCipherText, + CipherText, KeysetCipher, Label, Node, Plaintext, SealedValue, StackCipherText, }; use stack_kms::DataKeySource; -use vitaminc_aead_value::{transport as codec, FfiValue}; +use vitaminc_aead_frame::{decode_ciphertext, encode_ciphertext, FrameWriter}; use vitaminc_protected::Controlled; use stack_guest_abi::last_error; @@ -63,7 +64,7 @@ pub const TERM_OPE: u32 = 4; /// A ciphertext tree whose leaves are the frozen [`SealedValue`] byte /// encoding — the shape that crosses the FFI codec. -type BytesTree = CipherText, BoxedPassthrough>; +type BytesTree = Node>; // ============================================================================= // Terms @@ -78,7 +79,7 @@ type BytesTree = CipherText, BoxedPassthrough>; /// /// The context is one part — a string, bytes, or an `i32`/`i64`/`u32`/`u64` /// — or an array of parts, nested as deep as the transport codec allows -/// ([`codec::MAX_DEPTH`] levels from the root of the encoded value; deeper +/// ([`MAX_DEPTH`](vitaminc_aead_frame::MAX_DEPTH) levels from the root of the encoded value; deeper /// is [`STATUS_ENCODING`] before the context is parsed), exactly as a plan /// field's; [`dynamic::context`] is the one home of that grammar. Shape is identity: /// `[x]` is a PAE-framed list and `x` is not, so a probe takes the context @@ -374,31 +375,32 @@ pub mod validate { // ============================================================================= fn decode_value(bytes: &[u8]) -> Result { - codec::decode_value(&mut codec::Reader::new(bytes)) - .map_err(|_| last_error::malformed("an input is not a value in the transport codec")) + FfiValue::from_frame(bytes) + .map_err(|_| last_error::malformed("an input is not a value in the transport frame")) } /// Encode a value tree into a buffer sized **before** the first byte is /// written. /// /// This buffer is plaintext on the decrypt path, and a `Vec` grown by the -/// codec's pushes would leave partial plaintext in every abandoned +/// frame writer's pushes would leave partial plaintext in every abandoned /// allocation a reallocation could not extend in place — memory nothing /// wipes, undercutting the guarantee the registry makes about the buffer it /// eventually hands the host. Reserving the exact encoded length up front -/// means the encoder never reallocates, and `register`'s `into_boxed_slice` +/// means the writer never reallocates, and `register`'s `into_boxed_slice` /// (capacity == length) does not copy either. fn encode_value(value: FfiValue) -> Result, u32> { let mut out = exact_buffer(value_encoded_len(&value))?; - codec::encode_value(value, &mut out).map_err(|_| STATUS_ENCODING)?; + (&value) + .visit(FrameWriter::new(&mut out)) + .map_err(|_| STATUS_ENCODING)?; Ok(out) } fn decode_tree(bytes: &[u8]) -> Result { - let tree: BytesTree = - codec::decode_ciphertext_boxed(&mut codec::Reader::new(bytes)).map_err(|_| { - last_error::malformed("the record is not a ciphertext tree in the transport codec") - })?; + let tree: BytesTree = decode_ciphertext(bytes).map_err(|_| { + last_error::malformed("the record is not a ciphertext tree in the transport codec") + })?; // Structural only — a decoded leaf proves nothing until its AEAD opens // (see the `SealedValue` docs). map_leaves(tree, &mut |l: Vec| { @@ -413,20 +415,19 @@ fn decode_tree(bytes: &[u8]) -> Result { fn encode_tree(tree: StackCipherText) -> Result, u32> { let tree = map_leaves(tree, &mut |l: SealedValue| Ok::<_, u32>(l.to_bytes()))?; let mut out = exact_buffer(tree_encoded_len(&tree))?; - codec::encode_ciphertext_boxed(tree, &mut out).map_err(|_| STATUS_ENCODING)?; + encode_ciphertext(&tree, &mut out).map_err(|_| STATUS_ENCODING)?; Ok(out) } /// Rebuild a ciphertext tree with every leaf run through `leaf`, keeping the -/// structure (and the passthrough payloads) untouched. One definition for +/// structure (and the passthrough frames) untouched. One definition for /// both directions of the frozen [`SealedValue`] leaf encoding. fn map_leaves( - tree: CipherText, + tree: Node, leaf: &mut impl FnMut(A) -> Result, -) -> Result, E> { +) -> Result, E> { Ok(match tree { CipherText::Single(l) => CipherText::Single(leaf(l)?), - CipherText::None(l) => CipherText::None(leaf(l)?), CipherText::EmptySequence(l) => CipherText::EmptySequence(leaf(l)?), CipherText::EmptyMap(l) => CipherText::EmptyMap(leaf(l)?), CipherText::Sequence(items) => CipherText::Sequence( @@ -460,9 +461,9 @@ fn exact_buffer(len: Option) -> Result, u32> { Ok(out) } -/// Exact byte length of the codec's encoding of `value`. `None` on overflow -/// or on a length the codec's `u32` frames cannot express — the encode would -/// fail on those anyway, so the caller reports an encoding error. +/// Exact byte length of the frame of `value`. `None` on overflow or on a +/// length the frame's `u32` lengths cannot express — the encode would fail +/// on those anyway, so the caller reports an encoding error. fn value_encoded_len(value: &FfiValue) -> Option { // tag byte + fixed payload, or tag + u32 length prefix + payload. let framed = |len: usize| u32::try_from(len).ok().and_then(|_| len.checked_add(5)); @@ -483,18 +484,14 @@ fn value_encoded_len(value: &FfiValue) -> Option { } } -/// Exact byte length of the codec's encoding of a ciphertext tree. The -/// passthrough payloads are read (not consumed) through `Any::downcast_ref`, -/// matching what `encode_ciphertext_boxed` will re-home them to; a payload -/// that is not an [`FfiValue`] is `None`, which is the same rejection the -/// encoder would make. +/// Exact byte length of the codec's encoding of a ciphertext tree. A +/// passthrough is its node byte and its frame, written as it is. fn tree_encoded_len(tree: &BytesTree) -> Option { let framed = |len: usize| u32::try_from(len).ok().and_then(|_| len.checked_add(5)); match tree { - CipherText::Single(l) - | CipherText::None(l) - | CipherText::EmptySequence(l) - | CipherText::EmptyMap(l) => framed(l.len()), + CipherText::Single(l) | CipherText::EmptySequence(l) | CipherText::EmptyMap(l) => { + framed(l.len()) + } CipherText::Sequence(items) => items .iter() .try_fold(5usize, |acc, item| acc.checked_add(tree_encoded_len(item)?)), @@ -502,9 +499,7 @@ fn tree_encoded_len(tree: &BytesTree) -> Option { acc.checked_add(framed(key.len())?.checked_sub(1)?)? .checked_add(tree_encoded_len(value)?) }), - CipherText::Passthrough(p) => { - value_encoded_len((**p).downcast_ref::()?)?.checked_add(1) - } + CipherText::Passthrough(p) => p.0.len().checked_add(1), } } @@ -513,8 +508,8 @@ mod tests { use super::*; use vitaminc_protected::Protected; - // `value_encoded_len` / `tree_encoded_len` re-derive the codec's framing - // arithmetic; the codec exports no `encoded_len` of its own, so these + // `value_encoded_len` / `tree_encoded_len` re-derive the frame's and the + // codec's arithmetic; neither exports an `encoded_len` of its own, so these // pins are the only thing that fails if the two drift. Drift is not a // cosmetic bug: an undersized reservation makes `encode_value` // reallocate mid-encode, leaving unwiped partial plaintext in the @@ -556,9 +551,8 @@ mod tests { fn value_encoded_len_matches_the_codec_exactly() { for (i, value) in every_value_shape().into_iter().enumerate() { let expected = value_encoded_len(&value).expect("encodable shape"); - let mut out = Vec::new(); - codec::encode_value(value, &mut out).expect("codec encode"); - assert_eq!(out.len(), expected, "shape {i}"); + let out = value.to_frame().expect("frame encode"); + assert_eq!(out.risky_ref().len(), expected, "shape {i}"); } } @@ -568,10 +562,9 @@ mod tests { let trees: Vec = vec![ leaf(b""), leaf(&[7u8; 40]), - CipherText::None(vec![1, 2]), CipherText::EmptySequence(vec![3]), CipherText::EmptyMap(Vec::new()), - CipherText::Sequence(vec![leaf(b"a"), CipherText::None(vec![9])]), + CipherText::Sequence(vec![leaf(b"a"), CipherText::EmptyMap(vec![9])]), CipherText::Map(vec![ ("name".to_string(), leaf(b"ct")), ( @@ -579,14 +572,17 @@ mod tests { CipherText::Map(vec![("x".to_string(), leaf(b"y"))]), ), ]), - CipherText::Passthrough( - Box::new(FfiValue::Bytes(Protected::new(vec![1, 2, 3]))) as BoxedPassthrough - ), + CipherText::Passthrough(stack_encrypt::PassthroughFrame( + FfiValue::Bytes(Protected::new(vec![1, 2, 3])) + .to_frame() + .expect("a frame") + .risky_unwrap(), + )), ]; for (i, tree) in trees.into_iter().enumerate() { let expected = tree_encoded_len(&tree).expect("encodable shape"); let mut out = Vec::new(); - codec::encode_ciphertext_boxed(tree, &mut out).expect("codec encode"); + encode_ciphertext(&tree, &mut out).expect("codec encode"); assert_eq!(out.len(), expected, "tree {i}"); } } @@ -594,9 +590,7 @@ mod tests { // ---- the EQL target path, native, over FakeDataKeySource ----------------- fn encoded(value: FfiValue) -> Vec { - let mut out = Vec::new(); - codec::encode_value(value, &mut out).expect("encode"); - out + value.to_frame().expect("encode").risky_unwrap() } fn text(value: &str) -> FfiValue { @@ -749,7 +743,7 @@ mod tests { let CipherText::Passthrough(payload) = &outputs[0].1 else { panic!("a passthrough") }; - let FfiValue::Bytes(bytes) = payload.downcast_ref::().expect("a value") else { + let FfiValue::Bytes(bytes) = FfiValue::from_frame(&payload.0).expect("a value") else { panic!("bytes") }; let eql: serde_json::Value = serde_json::from_slice(bytes.risky_ref()).expect("JSON"); diff --git a/languages/golang/encrypt/guest/src/options.rs b/languages/golang/encrypt/guest/src/options.rs index 02db276e1..569b4d05c 100644 --- a/languages/golang/encrypt/guest/src/options.rs +++ b/languages/golang/encrypt/guest/src/options.rs @@ -41,11 +41,11 @@ //! it is objects, strings, bytes and nothing else, and this module is its //! one home. The Go bindings plan points here. +use stack_encrypt::dynamic::FfiValue; use stack_encrypt::dynamic::Scope; use stack_encrypt::{KeysetCipher, Label, StackCipher}; use stack_kms::{IdentifiedBy, IndexKeySource}; use uuid::Uuid; -use vitaminc_aead_value::FfiValue; use vitaminc_protected::Controlled; use zerokms_protocol::Name; diff --git a/languages/golang/encrypt/guest/src/status.rs b/languages/golang/encrypt/guest/src/status.rs index 8425272e4..649ecddc8 100644 --- a/languages/golang/encrypt/guest/src/status.rs +++ b/languages/golang/encrypt/guest/src/status.rs @@ -414,8 +414,8 @@ mod tests { /// such. Swapping the arms, or dropping the `Other` one, fails here. #[test] fn target_refusals_are_encoding_and_a_resolver_failure_is_internal() { + use stack_encrypt::dynamic::ValueKind; use stack_encrypt::dynamic::{Error, TargetError}; - use vitaminc_aead_value::ValueKind; let refusals = [ TargetError::NoTargets { name: "TextEq".into(), diff --git a/languages/golang/encrypt/guest/src/targets.rs b/languages/golang/encrypt/guest/src/targets.rs index 3451b9176..be15da540 100644 --- a/languages/golang/encrypt/guest/src/targets.rs +++ b/languages/golang/encrypt/guest/src/targets.rs @@ -60,7 +60,7 @@ impl TargetResolver for EqlTargets { name: &str, keyset: &'a stack_encrypt::KeysetCipher<'_, K>, label: &stack_encrypt::Label, - plaintext: vitaminc_aead_value::FfiValue, + plaintext: stack_encrypt::dynamic::FfiValue, ) -> Result, K>, TargetError> { eql_bindings::encryption::targets::encrypt(name, keyset, label, plaintext).map_err(convert) } @@ -71,7 +71,7 @@ impl TargetResolver for EqlTargets { cipher: &'a stack_encrypt::StackCipher, label: &stack_encrypt::Label, stored: &[u8], - ) -> Result, TargetError> { + ) -> Result, TargetError> { eql_bindings::encryption::targets::decrypt(name, cipher, label, stored).map_err(convert) } @@ -80,7 +80,7 @@ impl TargetResolver for EqlTargets { name: &str, keyset: &'a stack_encrypt::KeysetCipher<'_, K>, label: &stack_encrypt::Label, - plaintext: vitaminc_aead_value::FfiValue, + plaintext: stack_encrypt::dynamic::FfiValue, ) -> Result, K>, TargetError> { eql_bindings::encryption::targets::query(name, keyset, label, plaintext).map_err(convert) } @@ -169,7 +169,7 @@ mod tests { assert!(text_eq.producible); assert_eq!( text_eq.plaintext, - Some(vitaminc_aead_value::ValueKind::String) + Some(stack_encrypt::dynamic::ValueKind::String) ); assert_eq!(text_eq.indexes, ["eq"]); assert_eq!(text_eq.query.as_deref(), Some("TextEqQuery")); @@ -216,10 +216,10 @@ mod tests { assert!(matches!( convert(Eql::Plaintext { target: "TextEq", - expected: vitaminc_aead_value::ValueKind::String, - found: Some(vitaminc_aead_value::ValueKind::UInt64), + expected: stack_encrypt::dynamic::ValueKind::String, + found: Some(stack_encrypt::dynamic::ValueKind::UInt64), }), - TargetError::Plaintext { target, expected: Some(vitaminc_aead_value::ValueKind::String), .. } if target == "TextEq" + TargetError::Plaintext { target, expected: Some(stack_encrypt::dynamic::ValueKind::String), .. } if target == "TextEq" )); } diff --git a/languages/golang/encrypt/guest/tests/leak.rs b/languages/golang/encrypt/guest/tests/leak.rs index 286520f51..46cb0816d 100644 --- a/languages/golang/encrypt/guest/tests/leak.rs +++ b/languages/golang/encrypt/guest/tests/leak.rs @@ -21,8 +21,9 @@ use std::borrow::Cow; use std::future::IntoFuture; use stack_auth::{SecretToken, ServiceToken}; +use stack_encrypt::dynamic::FfiValue; use stack_encrypt::dynamic::Scope; -use stack_encrypt::{CipherText, Descriptor, StackCipher}; +use stack_encrypt::{CipherText, Descriptor, Node, PassthroughFrame, StackCipher}; use stack_encrypt_guest::options::{parse_options, Side}; use stack_encrypt_guest::status::fail_error; use stack_encrypt_guest::{config, ops}; @@ -32,7 +33,7 @@ use stack_kms::{ GenerateKeyPayload, IndexKeySource, LoadKeysetError, RetrieveKeyError, RetrieveKeyPayload, }; use uuid::Uuid; -use vitaminc_aead_value::{transport as codec, FfiValue}; +use vitaminc_aead_frame::encode_ciphertext; use zerokms_protocol::{IdentifiedBy, UnverifiedContext, ViturRequestError}; const PLAINTEXT: &str = "leak-marker-plaintext"; @@ -127,9 +128,13 @@ fn failing() -> StackCipher { // ============================================================================= fn encode(value: FfiValue) -> Vec { - let mut out = Vec::new(); - codec::encode_value(value, &mut out).expect("encode"); - out + use vitaminc_protected::Controlled; + value.to_frame().expect("encode").risky_unwrap() +} + +/// A value as a passthrough node: its frame, in the clear. +fn clear(value: FfiValue) -> Node> { + CipherText::Passthrough(PassthroughFrame(encode(value))) } fn s(value: &str) -> FfiValue { @@ -191,13 +196,13 @@ fn sealed(cipher: &StackCipher, plan: &[u8]) -> Vec { } /// A one-leaf ciphertext tree whose leaf is `leaf`, under `email`/`c`. -fn tree_with_leaf(leaf: CipherText, Box>) -> Vec { - let tree: CipherText, Box> = CipherText::Map(vec![( +fn tree_with_leaf(leaf: Node>) -> Vec { + let tree: Node> = CipherText::Map(vec![( "email".to_owned(), CipherText::Map(vec![("c".to_owned(), leaf)]), )]); let mut out = Vec::new(); - codec::encode_ciphertext_boxed(tree, &mut out).expect("encode tree"); + encode_ciphertext(&tree, &mut out).expect("encode tree"); out } @@ -218,7 +223,7 @@ fn failure( ) -> FfiValue { let _ = call::run(step).expect_err(what); let bytes = call::take_last_error().unwrap_or_else(|| panic!("{what}: no error recorded")); - let error = codec::decode_value(&mut codec::Reader::new(&bytes)).expect("an error decodes"); + let error = FfiValue::from_frame(&bytes).expect("an error decodes"); assert_eq!(codes(&error), expect, "{what}: the codes it reaches"); error } @@ -488,8 +493,8 @@ fn scenarios() -> Vec<(&'static str, FfiValue)> { "record passthrough", &["stack_encrypt::dynamic_record"], || { - let record = tree_with_leaf(CipherText::Passthrough(Box::new( - FfiValue::Bytes(vitaminc_protected::Protected::new(CIPHERTEXT.to_vec())), + let record = tree_with_leaf(clear(FfiValue::Bytes( + vitaminc_protected::Protected::new(CIPHERTEXT.to_vec()), ))); block_on(ops::decrypt_record( Scope::Client(&cipher), @@ -651,19 +656,17 @@ fn scenarios() -> Vec<(&'static str, FfiValue)> { "email", obj(vec![("context", label("email")), ("target", s("TextEq"))]), )])); - let tree: CipherText, Box> = CipherText::Map(vec![( + let tree: Node> = CipherText::Map(vec![( "email".to_owned(), CipherText::Map(vec![( "eql".to_owned(), - CipherText::Passthrough(Box::new(FfiValue::Bytes( - vitaminc_protected::Protected::new( - [&b"\""[..], CIPHERTEXT, &b"\""[..]].concat(), - ), + clear(FfiValue::Bytes(vitaminc_protected::Protected::new( + [&b"\""[..], CIPHERTEXT, &b"\""[..]].concat(), ))), )]), )]); let mut record = Vec::new(); - codec::encode_ciphertext_boxed(tree, &mut record).expect("encode tree"); + encode_ciphertext(&tree, &mut record).expect("encode tree"); block_on(ops::decrypt_record( Scope::Client(&cipher), &record, diff --git a/languages/golang/encrypt/guest/tests/native_ops.rs b/languages/golang/encrypt/guest/tests/native_ops.rs index 0e653f452..bcd86402d 100644 --- a/languages/golang/encrypt/guest/tests/native_ops.rs +++ b/languages/golang/encrypt/guest/tests/native_ops.rs @@ -17,9 +17,13 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use std::future::IntoFuture; +use stack_encrypt::dynamic::FfiValue; use stack_encrypt::dynamic::Scope; use stack_encrypt::sem::DefaultMatch; -use stack_encrypt::{nonempty, CipherText, Encrypt, Label, SealedValue, StackCipher}; +use stack_encrypt::{ + nonempty, CipherText, Label, Node, PassthroughFrame, PendingStackCipherText, SealedValue, + StackCipher, +}; use stack_encrypt_guest::ops::{self, TERM_EQUALITY, TERM_MATCH, TERM_OPE, TERM_ORE}; use stack_encrypt_guest::status::{ STATUS_AUTH, STATUS_CONTEXT_MISMATCH, STATUS_ENCODING, STATUS_FOREIGN_KEYSET, @@ -29,7 +33,7 @@ use stack_kms::{ RetrieveKeyPayload, }; use uuid::Uuid; -use vitaminc_aead_value::{transport as codec, FfiValue}; +use vitaminc_aead_frame::{decode_ciphertext, encode_ciphertext}; use vitaminc_protected::{Controlled, Protected}; use zerokms_protocol::{IdentifiedBy, UnverifiedContext}; @@ -93,19 +97,29 @@ fn cipher() -> StackCipher { } fn encode(value: FfiValue) -> Vec { - let mut out = Vec::new(); - codec::encode_value(value, &mut out).expect("encode value"); - out + value.to_frame().expect("encode value").risky_unwrap() } fn decode(bytes: &[u8]) -> FfiValue { - codec::decode_value(&mut codec::Reader::new(bytes)).expect("decode value") + FfiValue::from_frame(bytes).expect("decode value") } -/// Decode a guest ciphertext buffer with the passthrough payload kept as a -/// plain [`FfiValue`], so tests can inspect term nodes directly. -fn decode_tree(bytes: &[u8]) -> CipherText, FfiValue> { - codec::decode_ciphertext(&mut codec::Reader::new(bytes)).expect("decode ciphertext tree") +/// Decode a guest ciphertext buffer with its leaves as bytes, so tests can +/// inspect the nodes directly. +fn decode_tree(bytes: &[u8]) -> Node> { + decode_ciphertext(bytes).expect("decode ciphertext tree") +} + +/// Encode a ciphertext tree as the guest reads it. +fn encode_tree(tree: &Node>) -> Vec { + let mut out = Vec::new(); + encode_ciphertext(tree, &mut out).expect("encode ciphertext tree"); + out +} + +/// A value as a passthrough node: its frame, in the clear. +fn clear(value: FfiValue) -> Node> { + CipherText::Passthrough(PassthroughFrame(encode(value))) } fn text(value: &FfiValue) -> &str { @@ -194,8 +208,11 @@ fn single_field_plan(field: &str, context: FfiValue) -> Vec { } /// The bytes of a term node in a decoded record tree. -fn term_bytes(node: &CipherText, FfiValue>) -> Vec { - let CipherText::Passthrough(FfiValue::Bytes(b)) = node else { +fn term_bytes(node: &Node>) -> Vec { + let CipherText::Passthrough(frame) = node else { + panic!("expected a passthrough term node"); + }; + let FfiValue::Bytes(b) = decode(&frame.0) else { panic!("expected a passthrough bytes term node"); }; b.risky_ref().to_vec() @@ -456,12 +473,11 @@ fn a_forged_passthrough_ciphertext_slot_is_rejected_not_decrypted() { }; for (key, slot) in outputs.iter_mut() { if key == "c" { - *slot = CipherText::Passthrough(FfiValue::UInt32(99)); + *slot = clear(FfiValue::UInt32(99)); } } } - let mut forged = Vec::new(); - codec::encode_ciphertext(&CipherText::Map(fields), &mut forged).expect("re-encode"); + let forged = encode_tree(&CipherText::Map(fields)); assert_eq!( block_on(ops::decrypt_record( @@ -666,9 +682,8 @@ fn a_natively_sealed_field_under_an_extended_context_opens_through_a_plan() { let keyset = cipher.default_keyset(); let native = nonempty!("users").with("age").with(7u64); let sealed = block_on( - FfiValue::UInt32(34) - .encrypt_with_aad(&keyset, native) - .expect("encrypt") + PendingStackCipherText::plan(FfiValue::UInt32(34), native) + .expect("plan") .seal(&keyset, native), ) .expect("seal"); @@ -677,12 +692,11 @@ fn a_natively_sealed_field_under_an_extended_context_opens_through_a_plan() { }; // Shape the tree the way `encrypt_record` writes it: field → { c: leaf }. - let tree: CipherText, FfiValue> = CipherText::Map(vec![( + let tree: Node> = CipherText::Map(vec![( "age".to_string(), CipherText::Map(vec![("c".to_string(), CipherText::Single(leaf.to_bytes()))]), )]); - let mut record = Vec::new(); - codec::encode_ciphertext(&tree, &mut record).expect("encode tree"); + let record = encode_tree(&tree); let plan_with = |context: FfiValue| single_field_plan("age", context); @@ -954,21 +968,15 @@ fn a_mixed_keyset_record_batch_opens_through_any_one_call_per_keyset() { )) .expect("encrypt globex row"); let batch = { - let (CipherText::Map(a), CipherText::Map(g)) = ( - codec::decode_ciphertext_boxed::>(&mut codec::Reader::new(&acme_rows)) - .expect("decode"), - codec::decode_ciphertext_boxed::>(&mut codec::Reader::new(&globex_rows)) - .expect("decode"), - ) else { + let (CipherText::Map(a), CipherText::Map(g)) = + (decode_tree(&acme_rows), decode_tree(&globex_rows)) + else { panic!("a single record is a map"); }; - let mut out = Vec::new(); - codec::encode_ciphertext_boxed( - CipherText::Sequence(vec![CipherText::Map(a), CipherText::Map(g)]), - &mut out, - ) - .expect("encode batch"); - out + encode_tree(&CipherText::Sequence(vec![ + CipherText::Map(a), + CipherText::Map(g), + ])) }; let before = cipher.kms().retrieve_calls.load(Ordering::SeqCst); @@ -1198,7 +1206,7 @@ fn record_tree_validation_refuses_what_decrypt_record_refuses() { .expect("encrypt record"); assert_eq!(ops::validate::record_tree(&record, &plan, None), Ok(())); - type Node = CipherText, FfiValue>; + type Node = stack_encrypt::Node>; // The tree is not `Clone`; every variant decodes the record afresh. let fields = || { let CipherText::Map(fields) = decode_tree(&record) else { @@ -1206,11 +1214,7 @@ fn record_tree_validation_refuses_what_decrypt_record_refuses() { }; fields }; - let re_encode = |tree: Node| { - let mut out = Vec::new(); - codec::encode_ciphertext(&tree, &mut out).expect("re-encode"); - out - }; + let re_encode = |tree: Node| encode_tree(&tree); let with_age = |edit: &dyn Fn(&mut Node)| { let mut fields = fields(); for (field, node) in &mut fields { @@ -1227,7 +1231,7 @@ fn record_tree_validation_refuses_what_decrypt_record_refuses() { }; for (key, slot) in outputs.iter_mut() { if key == "c" { - *slot = CipherText::Passthrough(FfiValue::UInt32(99)); + *slot = clear(FfiValue::UInt32(99)); } } }); @@ -1237,16 +1241,14 @@ fn record_tree_validation_refuses_what_decrypt_record_refuses() { }; outputs.retain(|(key, _)| key != "c"); }); - let not_a_map = with_age(&|node| *node = CipherText::Passthrough(FfiValue::Null)); + let not_a_map = with_age(&|node| *node = clear(FfiValue::Null)); let missing_field = { let mut fields = fields(); fields.retain(|(field, _)| field != "age"); CipherText::Map(fields) }; - let batch_of_non_records = CipherText::Sequence(vec![ - CipherText::Map(fields()), - CipherText::Passthrough(FfiValue::Null), - ]); + let batch_of_non_records = + CipherText::Sequence(vec![CipherText::Map(fields()), clear(FfiValue::Null)]); for (label, tree) in [ ("a forged passthrough under c", forged_c), @@ -1436,7 +1438,7 @@ fn a_context_field_record_opens_under_the_context_it_stores() { let CipherText::Sequence(rows) = decode_tree(&record) else { panic!("a batch is a sequence"); }; - let eq_of = |row: &CipherText, FfiValue>| { + let eq_of = |row: &Node>| { let CipherText::Map(fields) = row else { panic!("a row is a map"); }; diff --git a/languages/golang/encrypt/unit_test.go b/languages/golang/encrypt/unit_test.go index d016d8e6b..23083a74f 100644 --- a/languages/golang/encrypt/unit_test.go +++ b/languages/golang/encrypt/unit_test.go @@ -150,7 +150,6 @@ func TestTermsAndLeavesScanAndValue(t *testing.T) { func TestLeafSetKeepsStackEncryptLeavesDistinct(t *testing.T) { ct := map[string]any{ "a": Ciphertext{1}, - "b": otherLeaf{kind: vcffi.LeafNone, bytes: []byte{2}}, "c": otherLeaf{kind: vcffi.LeafEmptySeq, bytes: []byte{3}}, "d": otherLeaf{kind: vcffi.LeafEmptyMap, bytes: []byte{4}}, "p": vcvalue.Plain{V: "clear"}, diff --git a/languages/golang/go.mod b/languages/golang/go.mod index 751d6f869..52569fa25 100644 --- a/languages/golang/go.mod +++ b/languages/golang/go.mod @@ -3,8 +3,8 @@ module github.com/cipherstash/stack/languages/golang go 1.26.0 require ( - github.com/cipherstash/vitaminc/bindings/go/vcffi v0.0.0-20260902024806-f2c7f7d17fbc - github.com/cipherstash/vitaminc/bindings/go/vcvalue v0.0.0-20260902024806-f2c7f7d17fbc + github.com/cipherstash/vitaminc/bindings/go/vcffi v0.0.0-20261015123909-2e895cb9caed + github.com/cipherstash/vitaminc/bindings/go/vcvalue v0.0.0-20261015123909-2e895cb9caed github.com/tetratelabs/wazero v1.12.0 golang.org/x/oauth2 v0.37.0 ) diff --git a/languages/golang/go.sum b/languages/golang/go.sum index d9d2a09ce..e8211d9d1 100644 --- a/languages/golang/go.sum +++ b/languages/golang/go.sum @@ -1,7 +1,11 @@ github.com/cipherstash/vitaminc/bindings/go/vcffi v0.0.0-20260902024806-f2c7f7d17fbc h1:iGbTgv9Kx5SaTI2tPxaxwDvS5BXZUO565N00cxRVwC8= github.com/cipherstash/vitaminc/bindings/go/vcffi v0.0.0-20260902024806-f2c7f7d17fbc/go.mod h1:6jpaqAo6f7rjWuxoti0Ymoi9DQZyxpnieWVudhXv38Y= +github.com/cipherstash/vitaminc/bindings/go/vcffi v0.0.0-20261015123909-2e895cb9caed h1:supKop+UADso55MvBLebaBjP2vyilWmbAxOtFZJOnQ8= +github.com/cipherstash/vitaminc/bindings/go/vcffi v0.0.0-20261015123909-2e895cb9caed/go.mod h1:6jpaqAo6f7rjWuxoti0Ymoi9DQZyxpnieWVudhXv38Y= github.com/cipherstash/vitaminc/bindings/go/vcvalue v0.0.0-20260902024806-f2c7f7d17fbc h1:vlrjoILAURGfpBWucVZEywK22k4lfky1xGDIKV6kqNg= github.com/cipherstash/vitaminc/bindings/go/vcvalue v0.0.0-20260902024806-f2c7f7d17fbc/go.mod h1:RJODA1DCSm4H+1pbmCzDdT2pAIkxhinwIu6ewjh5sPs= +github.com/cipherstash/vitaminc/bindings/go/vcvalue v0.0.0-20261015123909-2e895cb9caed h1:do5JX9ovxiHzLc9G4wcybbr4f3xaVAYeIKFnOJk9NKw= +github.com/cipherstash/vitaminc/bindings/go/vcvalue v0.0.0-20261015123909-2e895cb9caed/go.mod h1:RJODA1DCSm4H+1pbmCzDdT2pAIkxhinwIu6ewjh5sPs= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU= diff --git a/languages/golang/stashgen/emit.go b/languages/golang/stashgen/emit.go index 1cd90257b..f72ea02b3 100644 --- a/languages/golang/stashgen/emit.go +++ b/languages/golang/stashgen/emit.go @@ -208,9 +208,10 @@ func (w *writer) shapeStruct(name string, fields []shapeField) { } // kindExpr is the gensupport kind of a Go type: the wire type the field -// seals as. The mapping is vcffi's: int8, int16 and int32 travel as int32, -// int and int64 as int64, the unsigned ones likewise, []byte as bytes. A -// composite is untyped and seals as one self-describing value. +// seals as. int8, int16 and int32 seal as int32, int and int64 as int64, the +// unsigned ones likewise, []byte as bytes: vcffi sends a narrower integer at +// its own width, and the engine reads it as the 32-bit integer of its sign. +// A composite is untyped and seals as one self-describing value. func kindExpr(t GoType) string { switch t.Kind { case KindString: From 962816763a5f1fe1cebc0ae22f0cf8489a759087 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Fri, 16 Oct 2026 01:41:22 +1100 Subject: [PATCH 04/15] test(stack-encrypt): pin each cipher half's refusal and a host's u8, and drop the unpinnable capacity hint cargo-mutants found the unused half of MintedKeys and BoundKeys, and the UINT8 arm of FfiValue's sink, untested: no Rust source walks a u8 as an int, so the test reads a host's frame of one. The sink's capacity hint changed nothing a test can see, so it is gone. --- packages/stack-encrypt/src/cipher.rs | 23 +++++++++++++++++++ .../stack-encrypt/src/dynamic/ffi_value.rs | 20 ++++++++-------- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/packages/stack-encrypt/src/cipher.rs b/packages/stack-encrypt/src/cipher.rs index 716f7d019..7f94d9664 100644 --- a/packages/stack-encrypt/src/cipher.rs +++ b/packages/stack-encrypt/src/cipher.rs @@ -1549,6 +1549,29 @@ mod tests { } } + /// Each half of the cipher does one thing: a minted key never opens a + /// leaf, and a key bound to a leaf for opening never seals one. + #[test] + fn each_half_of_the_cipher_refuses_the_other_halfs_work() { + let leaf = + SealedValue::from_parts(Uuid::nil(), [0; 16], vec![1; 4], vec![2; 32]).expect("a leaf"); + let mut keys = std::iter::empty::(); + let minted = MintedKeys { + keyset_id: Uuid::nil(), + keys: RefCell::new(&mut keys), + }; + let aad = Context::from_encoded(b"aad"); + assert!(minted.open(leaf, &aad).is_err(), "a minted key never opens"); + assert!( + minted.seal(Protected::new(vec![1]), &aad).is_err(), + "and seals nothing once the batch is spent" + ); + assert!( + BoundKeys.seal(Protected::new(vec![1]), &aad).is_err(), + "a bound key never seals" + ); + } + /// Byte-level pin for the [`leaf_aad`] derivation. This is part of the /// frozen leaf format: a change to the domain label, the version byte, /// the keyset id's place, the piece order, or the PAE framing makes diff --git a/packages/stack-encrypt/src/dynamic/ffi_value.rs b/packages/stack-encrypt/src/dynamic/ffi_value.rs index d3d1795e5..231d5db48 100644 --- a/packages/stack-encrypt/src/dynamic/ffi_value.rs +++ b/packages/stack-encrypt/src/dynamic/ffi_value.rs @@ -290,17 +290,17 @@ impl Sink for ValueSink { Ok(self.0.put(value)) } - fn seq(self, len: Option) -> Result, DecodeError> { + fn seq(self, _: Option) -> Result, DecodeError> { Ok(ValueSeq { out: self.0, - items: Vec::with_capacity(capacity(len)), + items: Vec::new(), }) } - fn map(self, len: Option) -> Result, DecodeError> { + fn map(self, _: Option) -> Result, DecodeError> { Ok(ValueMap { out: self.0, - entries: Vec::with_capacity(capacity(len)), + entries: Vec::new(), }) } @@ -310,12 +310,6 @@ impl Sink for ValueSink { } } -/// A length hint is the source's word, so it only seeds a capacity, and -/// a bounded one. -fn capacity(len: Option) -> usize { - len.unwrap_or(0).min(1024) -} - /// The sequence sink of [`ValueSink`]. pub struct ValueSeq { out: O, @@ -619,6 +613,12 @@ mod tests { read(vitaminc_aead::to_frame(u16::MAX).expect("frames")), FfiValue::UInt32(u16::MAX.into()) ); + // No Rust source walks a `u8` as an int (`Vec` is bytes), but a + // host's frame can carry one: the `UINT8` tag and its byte. + assert_eq!( + FfiValue::from_frame(&[vitaminc_aead::tags::UINT8, 200]).expect("reads back"), + FfiValue::UInt32(200) + ); } /// A frame holding a scalar no variant holds is refused, not read as From d03f8aab0967362149ea3a71793304c7acc96504 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Fri, 16 Oct 2026 23:40:57 +1100 Subject: [PATCH 05/15] feat(eql)!: build eql-bindings' stack-encrypt feature on vitaminc ADR 0001 so the EQL jobs and the EQL guest stay green eql-bindings' `stack-encrypt` feature and tests/encryption compile stack-encrypt by path, so they move with it. - The EQL workspace carries the same temporary vitaminc pin as the root, the fuzz crate and both guests: every workspace that compiles stack-encrypt now resolves one vitaminc. - `encryption::targets` and the generated `v3::targets` take `FfiValue` and `ValueKind` from `stack_encrypt::dynamic` (the feature turns on stack-encrypt's `dynamic`); the generator emits the new import. `vitaminc-aead-value` is gone from both manifests. - The `Ciphertext` target takes `Plaintext` in place of `Encrypt` and `Decrypt + Send + 'static` in place of `Decrypt<'static>`. - The encrypt guest's lockfile drops `vitaminc-aead-value`, which only its `eql` build still pulled in. No other EQL code names a vitaminc API. Rebased onto next, which carries the EQL v4 types: - `encryption::plaintext`'s `Int32`, `Int64`, `Bool`, `Float32` and `Float64` sealed vitaminc 0.5's tagged leaves through `Encrypt` and `DecipherVisitor`, which ADR 0001 removes. They now describe themselves as the primitive they wrap (`Plaintext`), so they seal the leaf a data plan's field of that type seals, and read back through `FromScalar` only a scalar of their own kind and width, as before: vitaminc's primitive targets would convert any integer that fits and widen an f32. `vitaminc-aead-value` and `vitaminc-protected` leave eql-bindings; `vitaminc-aead` stays for the scalar kinds. - `tests/encryption/tests/v4_types.rs` takes `FfiValue` and `ValueKind` from `stack_encrypt::dynamic`. - `v4_rows.json` is regenerated by the Go test over the deterministic EQL guest: ADR 0001 leaves are wire version 2, so the committed ciphertexts no longer opened. --- languages/golang/encrypt/guest/Cargo.lock | 13 -- packages/eql/Cargo.lock | 55 ++--- packages/eql/Cargo.toml | 14 ++ packages/eql/crates/eql-bindings/CHANGELOG.md | 7 + packages/eql/crates/eql-bindings/Cargo.toml | 20 +- packages/eql/crates/eql-bindings/README.md | 2 +- .../eql/crates/eql-bindings/src/encryption.rs | 10 +- .../eql-bindings/src/encryption/plaintext.rs | 220 ++++++------------ .../eql-bindings/src/encryption/targets.rs | 2 +- .../eql/crates/eql-bindings/src/v4/targets.rs | 2 +- .../eql/crates/eql-codegen/src/targets.rs | 2 +- packages/eql/tests/encryption/Cargo.toml | 9 +- .../tests/encryption/fixtures/v4_rows.json | 28 +-- .../eql/tests/encryption/tests/targets.rs | 2 +- .../eql/tests/encryption/tests/text_eq.rs | 18 +- .../eql/tests/encryption/tests/v4_types.rs | 2 +- 16 files changed, 160 insertions(+), 246 deletions(-) diff --git a/languages/golang/encrypt/guest/Cargo.lock b/languages/golang/encrypt/guest/Cargo.lock index 48458d72d..ef9e3436e 100644 --- a/languages/golang/encrypt/guest/Cargo.lock +++ b/languages/golang/encrypt/guest/Cargo.lock @@ -710,9 +710,7 @@ dependencies = [ "thiserror 2.0.20", "ts-rs", "vitaminc-aead", - "vitaminc-aead-value", "vitaminc-prf", - "vitaminc-protected", "zeroize", ] @@ -2656,17 +2654,6 @@ dependencies = [ "vitaminc-aead", ] -[[package]] -name = "vitaminc-aead-value" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d78cdadbc6374c3bd7c19fb4d99abca693b96953b18fef8c608efbe092cf3ea" -dependencies = [ - "vitaminc-aead", - "vitaminc-protected", - "zeroize", -] - [[package]] name = "vitaminc-context" version = "0.5.1" diff --git a/packages/eql/Cargo.lock b/packages/eql/Cargo.lock index ee6e0d103..f7f798b99 100644 --- a/packages/eql/Cargo.lock +++ b/packages/eql/Cargo.lock @@ -1205,9 +1205,7 @@ dependencies = [ "thiserror 2.0.20", "ts-rs", "vitaminc-aead 0.5.1", - "vitaminc-aead-value", "vitaminc-prf", - "vitaminc-protected 0.5.1", "zeroize", ] @@ -1248,7 +1246,7 @@ dependencies = [ "stack-kms", "tokio", "uuid", - "vitaminc-aead-value", + "vitaminc-aead 0.5.1", ] [[package]] @@ -4242,6 +4240,7 @@ dependencies = [ "thiserror 1.0.69", "uuid", "vitaminc-aead 0.5.1", + "vitaminc-aead-frame", "vitaminc-encrypt 0.5.1", "vitaminc-hmac", "vitaminc-prf", @@ -4698,9 +4697,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.1.2+spec-1.1.0" +version = "1.1.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" dependencies = [ "winnow 1.0.3", ] @@ -5033,8 +5032,7 @@ dependencies = [ [[package]] name = "vitaminc" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35042fb7a81d72dc43c53fec835e61027f18b4d01cc0f7e12d6c6a6ffbf3bfb9" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "vitaminc-aead 0.5.1", "vitaminc-context", @@ -5060,11 +5058,12 @@ dependencies = [ [[package]] name = "vitaminc-aead" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62cacd4dae485e98cfe5407e919187a8468dd933ea4234be5115b7d13a30f3c6" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "bytes", + "futures-util", "serde", + "thiserror 2.0.20", "vitaminc-aead-derive", "vitaminc-context", "vitaminc-protected 0.5.1", @@ -5075,8 +5074,7 @@ dependencies = [ [[package]] name = "vitaminc-aead-derive" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ff34056b70a8417ad3bf9db73b2bd3be97f448794566c6de2080bd8a7cd9b6b" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "proc-macro2", "quote", @@ -5084,21 +5082,17 @@ dependencies = [ ] [[package]] -name = "vitaminc-aead-value" +name = "vitaminc-aead-frame" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d78cdadbc6374c3bd7c19fb4d99abca693b96953b18fef8c608efbe092cf3ea" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "vitaminc-aead 0.5.1", - "vitaminc-protected 0.5.1", - "zeroize", ] [[package]] name = "vitaminc-context" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7df52501fe33cdb3ee9e155b5242fac184c037d147e58c96e5143ed011252a8" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "mutants", "vitaminc-protected 0.5.1", @@ -5121,8 +5115,7 @@ dependencies = [ [[package]] name = "vitaminc-encrypt" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbd0e1748055e381e02033e08af385de879c4dcf32e0f6739ac0c0cf0c6f13d" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "aes-gcm", "aws-lc-rs", @@ -5135,8 +5128,7 @@ dependencies = [ [[package]] name = "vitaminc-hmac" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15fe558a5e92d4f2b3f811c5b67d863d9255da5f66d08a71d4f0dd3d0011c563" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "hmac 0.13.0", "sha2 0.11.0", @@ -5148,8 +5140,7 @@ dependencies = [ [[package]] name = "vitaminc-prf" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712d31d46d76b38c6b62deb35f516fc5636a35f90f39c0213d0ca9a052b4400b" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "mutants", "thiserror 2.0.20", @@ -5175,8 +5166,7 @@ dependencies = [ [[package]] name = "vitaminc-protected" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7b514f605a63f88874ba46ffea81775fad64994475ab239cb19da159e884b7b" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "bitvec", "digest 0.11.3", @@ -5203,9 +5193,9 @@ dependencies = [ [[package]] name = "vitaminc-protected-derive" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cb0a3d11afbe2c909f0cfc9996fe37fd89b1196eb0b8d89a66e28716360ee09" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ + "proc-macro-crate", "proc-macro2", "quote", "syn 3.0.3", @@ -5228,8 +5218,7 @@ dependencies = [ [[package]] name = "vitaminc-random" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7b1bfab35ee322de37269c6fd94d8cd94047a66f97cf6722e53e3321482167" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "chacha20", "getrandom 0.4.2", @@ -5254,8 +5243,7 @@ dependencies = [ [[package]] name = "vitaminc-random-derives" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0915e2bfb417ce11e23123fb7ec122af5d27d8cfd22d6ad6a2cf7a1c2dbb4712" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "proc-macro2", "quote", @@ -5281,8 +5269,7 @@ dependencies = [ [[package]] name = "vitaminc-traits" version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b31ec300e351a7a59e64899d0a2d7e7227cfa74ac0514f4e745acb2cc9335cee" +source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" dependencies = [ "anyhow", "bytes", diff --git a/packages/eql/Cargo.toml b/packages/eql/Cargo.toml index ab1492558..e12e2ec0c 100644 --- a/packages/eql/Cargo.toml +++ b/packages/eql/Cargo.toml @@ -35,3 +35,17 @@ default-members = ["tests/sqlx"] [workspace.lints.rust] dead_code = "deny" unused_imports = "deny" + +# Temporary, as in the repository root's Cargo.toml: eql-bindings' +# `stack-encrypt` feature and tests/encryption compile stack-encrypt by path, +# so the vitaminc crates it builds on come from the branch that implements +# vitaminc ADR 0001 (cipherstash/vitaminc PRs #439 to #445) until vitaminc +# releases it. +[patch.crates-io] +vitaminc = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } +vitaminc-aead = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } +vitaminc-aead-frame = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } +vitaminc-encrypt = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } +vitaminc-hmac = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } +vitaminc-prf = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } +vitaminc-protected = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } diff --git a/packages/eql/crates/eql-bindings/CHANGELOG.md b/packages/eql/crates/eql-bindings/CHANGELOG.md index 242167c93..c4ea4cf1e 100644 --- a/packages/eql/crates/eql-bindings/CHANGELOG.md +++ b/packages/eql/crates/eql-bindings/CHANGELOG.md @@ -35,6 +35,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **The `stack-encrypt` feature builds on vitaminc ADR 0001.** + `encryption::targets` takes `FfiValue` and `ValueKind` from + `stack_encrypt::dynamic` (the feature turns on stack-encrypt's `dynamic` + feature) in place of `vitaminc-aead-value`, which is gone, and the + `Ciphertext` target takes any `Plaintext` plaintext and opens into any + `Decrypt + Send` type, as stack-encrypt's own targets do. + - **Query-operand domain names switched to the `query_` prefix, homed in the `eql_v3` schema.** The SQL domain names carried by the query twins — in `DomainType::sql_domain`, the names `QueryPayload::parse` diff --git a/packages/eql/crates/eql-bindings/Cargo.toml b/packages/eql/crates/eql-bindings/Cargo.toml index 2d1ea219d..df16578d2 100644 --- a/packages/eql/crates/eql-bindings/Cargo.toml +++ b/packages/eql/crates/eql-bindings/Cargo.toml @@ -43,23 +43,19 @@ schemars = "1" # from the PACKAGED crate against the registry stack-encrypt instead: # `cargo publish --dry-run --all-features` in test-eql.yml at PR time, and # `publish_all_features` in ../../release-plz.toml at release time. -stack-encrypt = { path = "../../../stack-encrypt", version = "0.2.0", optional = true, default-features = false } +# `dynamic`: `FfiValue` / `ValueKind`, the runtime plaintext a plan field +# target takes and the kind names a plan's `"type"` key spells +# (`encryption::targets`). +stack-encrypt = { path = "../../../stack-encrypt", version = "0.2.0", optional = true, default-features = false, features = ["dynamic"] } # The vitaminc line stack-encrypt is built on. `PrfValue` is named from this # crate directly (stack-encrypt re-exports only `IntoPrfContext` and # `PrfContext`), and two vitaminc lines in one graph do not interoperate, so # the requirement is stack-encrypt's. vitaminc-prf = { version = "0.5.0", optional = true } -# `FfiValue` / `ValueKind`: the runtime plaintext a plan field target takes -# and the kind names a plan's `"type"` key spells (`encryption::targets`). -# Named from vitaminc directly, like `PrfValue` above, so the feature needs -# nothing from stack-encrypt's own `dynamic` feature; the same line -# stack-encrypt and the Go guest build against. -vitaminc-aead-value = { version = "0.5.1", optional = true } -# `DecipherVisitor` and `Protected`: the scalar plaintexts in -# `encryption::plaintext` seal and open vitaminc's tagged leaves themselves, -# because vitaminc 0.5 has no native `Encrypt` for the scalar types. +# `Physical`, `Int`, `Float` and `FromScalar`: the scalar plaintexts in +# `encryption::plaintext` describe themselves through vitaminc's `Plaintext` +# and read back only their own width through `FromScalar`. vitaminc-aead = { version = "0.5.1", optional = true } -vitaminc-protected = { version = "0.5.1", optional = true } # The plaintext a target reads out of its `Protected` runtime value is held # in `Zeroizing` until it is sealed, so the copy is wiped like the original. zeroize = { version = "1", optional = true } @@ -72,9 +68,7 @@ default = [] stack-encrypt = [ "dep:stack-encrypt", "dep:vitaminc-prf", - "dep:vitaminc-aead-value", "dep:vitaminc-aead", - "dep:vitaminc-protected", "dep:zeroize", "dep:thiserror", "dep:base64", diff --git a/packages/eql/crates/eql-bindings/README.md b/packages/eql/crates/eql-bindings/README.md index 5a16a25fb..f8cef452a 100644 --- a/packages/eql/crates/eql-bindings/README.md +++ b/packages/eql/crates/eql-bindings/README.md @@ -226,7 +226,7 @@ unknown one with "no such EQL type". ```rust,ignore use eql_bindings::encryption::targets; use stack_encrypt::Label; -use vitaminc_aead_value::FfiValue; +use stack_encrypt::dynamic::FfiValue; let column = Label::new(["users", "email"])?; // the field's context: table/column let stored: Vec = targets::encrypt("TextEq", &keyset, &column, FfiValue::String("alice@example.com".into()))?.await?; diff --git a/packages/eql/crates/eql-bindings/src/encryption.rs b/packages/eql/crates/eql-bindings/src/encryption.rs index af87ceb33..d072d0972 100644 --- a/packages/eql/crates/eql-bindings/src/encryption.rs +++ b/packages/eql/crates/eql-bindings/src/encryption.rs @@ -63,7 +63,7 @@ //! An encrypted EQL record is not a plaintext value: //! ```compile_fail //! use eql_bindings::v4::text::TextEq; -//! fn require() {} +//! fn require() {} //! require::(); //! ``` //! The text domain accepts text, and requires a concrete column identifier: @@ -89,8 +89,8 @@ use stack_encrypt::target::transcode::{Reader, Transcode, Visitor}; use stack_encrypt::target::{self, AeadContext, CallerContext}; use stack_encrypt::{ CipherText, ContextPiece, Decrypt, DecryptField, DecryptInto, Decryptable, Decryption, - Describe, Description, Encrypt, EncryptFrom, Encryption, Error, IntoContext, MaybeEmpty, - NonEmpty, SealedValue, StackCipherText, + Describe, Description, EncryptFrom, Encryption, Error, IntoContext, MaybeEmpty, NonEmpty, + Plaintext, SealedValue, StackCipherText, }; use vitaminc_prf::PrfValue; @@ -178,7 +178,7 @@ impl Reader for Ciphertext { } } -impl EncryptFrom for Ciphertext { +impl EncryptFrom for Ciphertext { type Context = AeadContext; fn encryption<'s, K: 'static>() -> Encryption<'s, S, Self, K, Self::Context> where @@ -199,7 +199,7 @@ impl Visitor for NativeCiphertextVisitor { Ok(CipherText::Single(leaf)) } } -impl + 'static> DecryptInto

for Ciphertext { +impl DecryptInto

for Ciphertext { type Context = AeadContext; fn decryption(self, context: Self::Context) -> Decryption { match self.read(NativeCiphertextVisitor) { diff --git a/packages/eql/crates/eql-bindings/src/encryption/plaintext.rs b/packages/eql/crates/eql-bindings/src/encryption/plaintext.rs index 610a7ad9b..3ce960c4a 100644 --- a/packages/eql/crates/eql-bindings/src/encryption/plaintext.rs +++ b/packages/eql/crates/eql-bindings/src/encryption/plaintext.rs @@ -1,17 +1,20 @@ //! Scalar plaintexts for the v4 number and boolean types. //! //! A Stack Encrypt target encrypts a plaintext through Vitamin C's -//! `Encrypt` and `Decrypt` traits, and vitaminc 0.5 implements -//! `Encrypt` for `String` but not for `i32`, `i64`, `bool`, `f32` or `f64`. -//! These newtypes fill that gap for the storage-only EQL v4 types: [`Int32`] -//! for `Integer`, [`Int64`] for `Bigint`, [`Bool`] for `Boolean`, and -//! [`Float32`] / [`Float64`] for `Real` and `Double`. +//! `Plaintext` and reads it back through `Decrypt`. These newtypes name the +//! plaintext of each storage-only EQL v4 type: [`Int32`] for `Integer`, +//! [`Int64`] for `Bigint`, [`Bool`] for `Boolean`, and [`Float32`] / +//! [`Float64`] for `Real` and `Double`. //! -//! Each seals as vitaminc's tagged leaf, `[tag] ++ payload` from the frozen -//! table in `vitaminc_aead_value::tags`, which is exactly what the value enum -//! (`FfiValue`) seals for the same value. So a value written through these -//! types and one written by the Go SDK as a plain `int32` field open each -//! other. +//! Each describes itself as the primitive it wraps, so it seals the leaf +//! vitaminc seals for that primitive, which is the leaf a data plan's field of +//! the same type seals. So a value written through these types and one +//! written by the Go SDK as a plain `int32` field open each other. +//! +//! Each reads back only its own width. vitaminc's primitive targets convert +//! an integer of any width that fits, and widen an `f32` to an `f64`; these +//! refuse a leaf of another width, so a column's type is the one it was +//! written with. //! //! None of them derives an equality term. `IntegerEq` and `BigintEq` are held //! back until vitaminc 0.6, whose integer equality term uses one PRF domain @@ -29,41 +32,12 @@ //! None of them implements `Debug`, so a plaintext does not reach a log by //! accident. -use stack_encrypt::{Cipher, Decipher, Decrypt, Encrypt, IntoAad, Unspecified}; -use vitaminc_aead::DecipherVisitor; -use vitaminc_aead_value::tagged::{ - TaggedBoolFalse, TaggedBoolTrue, TaggedFloat32, TaggedFloat64, TaggedInt32, TaggedInt64, -}; -use vitaminc_aead_value::tags; -use vitaminc_protected::{Controlled, Protected}; +use stack_encrypt::Plaintext; +use vitaminc_aead::{Float, FromScalar, Int, Logical, Physical, Sink}; use zeroize::Zeroize; -/// Reads one tagged scalar leaf back, refusing any other tag or width: a -/// value sealed as one kind never opens as another. -struct LeafVisitor { - tag: u8, - parse: fn(&[u8]) -> Option, -} - -impl<'c, T: Send> DecipherVisitor<'c> for LeafVisitor { - type Value = T; - - fn visit_bytes_vec(self, data: Protected>) -> Result { - match data.risky_ref().split_first() { - Some((&tag, payload)) if tag == self.tag => (self.parse)(payload).ok_or(Unspecified), - _ => Err(Unspecified), - } - } -} - -/// Bytes a [`LeafVisitor`] reads back: one tag, then exactly `N` payload -/// bytes. Anything else is not this kind. -fn leaf_payload(payload: &[u8]) -> Option<[u8; N]> { - payload.try_into().ok() -} - macro_rules! fixed_scalar { - ($(#[$doc:meta])* $name:ident($inner:ty), $tagged:ty, $tag:expr, $width:literal, |$bytes:ident| $decode:expr, |$a:ident, $b:ident| $eq:expr) => { + ($(#[$doc:meta])* $name:ident($inner:ty), $read:pat => $value:expr, |$a:ident, $b:ident| $eq:expr) => { $(#[$doc])* #[derive(Clone, Copy, Zeroize)] pub struct $name(pub $inner); @@ -92,35 +66,21 @@ macro_rules! fixed_scalar { } } - impl Encrypt for $name { - fn encrypt_with_aad<'a, C, A>(self, cipher: C, aad: A) -> Result - where - C: Cipher, - A: IntoAad<'a>, - { - <$tagged>::from(self.0).encrypt_with_aad(cipher, aad) + /// Describes itself as the primitive it wraps. + impl Plaintext for $name { + fn visit(self, sink: S) -> Result { + self.0.visit(sink) } } - impl<'c> Decrypt<'c> for $name { - fn decrypt_with_aad<'a, D, A>(decipher: D, aad: A) -> D::Ok - where - D: Decipher<'c>, - A: IntoAad<'a>, - { - decipher.decrypt_any(Self::visitor(), aad) - } - } - - impl $name { - /// The reader [`Decrypt`] opens this type's leaf with. - fn visitor() -> LeafVisitor { - LeafVisitor { - tag: $tag, - parse: |payload| { - let $bytes = leaf_payload::<$width>(payload)?; - Some($name($decode)) - }, + /// Reads back a leaf of exactly this kind and width, and nothing + /// else: a value sealed as one kind never opens as another. + impl FromScalar for $name { + const EXPECTED: &'static str = stringify!($inner); + fn from_scalar(ty: Option, value: Physical<'_>) -> Option { + match (ty, value) { + (None, $read) => Some($name($value)), + _ => None, } } } @@ -129,25 +89,23 @@ macro_rules! fixed_scalar { fixed_scalar!( /// The plaintext of `Integer`: a 32-bit signed integer. - Int32(i32), TaggedInt32, tags::INT32, 4, |b| i32::from_le_bytes(b), |a, b| a == b + Int32(i32), Physical::Int(Int::I32(v)) => v, |a, b| a == b ); fixed_scalar!( /// The plaintext of `Bigint`: a 64-bit signed integer. - Int64(i64), TaggedInt64, tags::INT64, 8, |b| i64::from_le_bytes(b), |a, b| a == b + Int64(i64), Physical::Int(Int::I64(v)) => v, |a, b| a == b ); fixed_scalar!( /// The plaintext of `Real`: a 32-bit float, sealed by its bit pattern, so /// `-0.0` and a NaN's payload survive a round trip. - Float32(f32), TaggedFloat32, tags::FLOAT32, 4, |b| f32::from_bits(u32::from_le_bytes(b)), - |a, b| a.to_bits() == b.to_bits() + Float32(f32), Physical::Float(Float::F32(v)) => v, |a, b| a.to_bits() == b.to_bits() ); fixed_scalar!( /// The plaintext of `Double`: a 64-bit float, sealed by its bit pattern. - Float64(f64), TaggedFloat64, tags::FLOAT64, 8, |b| f64::from_bits(u64::from_le_bytes(b)), - |a, b| a.to_bits() == b.to_bits() + Float64(f64), Physical::Float(Float::F64(v)) => v, |a, b| a.to_bits() == b.to_bits() ); -/// The plaintext of `Boolean`. A boolean is the tag itself: no payload. +/// The plaintext of `Boolean`. #[derive(Clone, Copy, PartialEq, Eq, Zeroize)] pub struct Bool(pub bool); @@ -163,118 +121,82 @@ impl From for bool { } } -impl Encrypt for Bool { - fn encrypt_with_aad<'a, C, A>(self, cipher: C, aad: A) -> Result - where - C: Cipher, - A: IntoAad<'a>, - { - if self.0 { - TaggedBoolTrue::tag_only().encrypt_with_aad(cipher, aad) - } else { - TaggedBoolFalse::tag_only().encrypt_with_aad(cipher, aad) - } +/// Describes itself as a `bool`. +impl Plaintext for Bool { + fn visit(self, sink: S) -> Result { + self.0.visit(sink) } } -/// Opens either boolean tag, and nothing else. -struct BoolVisitor; - -impl<'c> DecipherVisitor<'c> for BoolVisitor { - type Value = Bool; - - fn visit_bytes_vec(self, data: Protected>) -> Result { - match data.risky_ref().as_slice() { - [tags::BOOL_TRUE] => Ok(Bool(true)), - [tags::BOOL_FALSE] => Ok(Bool(false)), - _ => Err(Unspecified), +/// Reads back a boolean leaf, and nothing else. +impl FromScalar for Bool { + const EXPECTED: &'static str = "bool"; + fn from_scalar(ty: Option, value: Physical<'_>) -> Option { + match (ty, value) { + (None, Physical::Bool(b)) => Some(Bool(b)), + _ => None, } } } -impl<'c> Decrypt<'c> for Bool { - fn decrypt_with_aad<'a, D, A>(decipher: D, aad: A) -> D::Ok - where - D: Decipher<'c>, - A: IntoAad<'a>, - { - decipher.decrypt_any(BoolVisitor, aad) - } -} - #[cfg(test)] mod tests { - //! The leaf each type reads back, byte for byte. Sealing goes through - //! vitaminc's own `Tagged*` types; what is hand-written here is the - //! reading, so these pin its tag, width and byte order. The round trip - //! against the value enum (`FfiValue`) is in the encryption test crate + //! What each type reads back. Sealing is the primitive's own; what is + //! written here is the reading, so these pin that each type takes only + //! its own kind and width. The round trip against the value enum + //! (`FfiValue`) is in the encryption test crate //! (`tests/encryption/tests/v4_types.rs`). use super::*; - fn leaf(tag: u8, payload: &[u8]) -> Protected> { - let mut bytes = vec![tag]; - bytes.extend_from_slice(payload); - Protected::new(bytes) - } - - fn read<'c, V: DecipherVisitor<'c>>(visitor: V, bytes: Protected>) -> Option { - visitor.visit_bytes_vec(bytes).ok() + fn read(value: Physical<'_>) -> Option { + T::from_scalar(None, value) } #[test] - fn integers_read_their_own_little_endian_leaf() { - let (int32, int64) = (Int32::visitor, Int64::visitor); + fn integers_read_their_own_width() { for v in [0, 1, -1, 42, i32::MIN, i32::MAX] { - assert!(read(int32(), leaf(tags::INT32, &v.to_le_bytes())) == Some(Int32(v))); + assert!(read::(Physical::Int(Int::I32(v))) == Some(Int32(v))); } for v in [0, -1, 42, i64::MIN, i64::MAX] { - assert!(read(int64(), leaf(tags::INT64, &v.to_le_bytes())) == Some(Int64(v))); + assert!(read::(Physical::Int(Int::I64(v))) == Some(Int64(v))); } - // Little-endian, not big: 1 is 01 00 00 00. - assert!(read(int32(), leaf(tags::INT32, &[1, 0, 0, 0])) == Some(Int32(1))); } #[test] - fn a_leaf_of_another_tag_or_width_is_refused() { - let (int32, int64) = (Int32::visitor, Int64::visitor); + fn a_scalar_of_another_kind_or_width_is_refused() { // An Int64 leaf is not an Int32, even when its value would fit. - assert!(read(int32(), leaf(tags::INT64, &1i64.to_le_bytes())).is_none()); - assert!(read(int64(), leaf(tags::INT32, &1i32.to_le_bytes())).is_none()); - // The right tag with the wrong width. - assert!(read(int32(), leaf(tags::INT32, &1i64.to_le_bytes())).is_none()); - assert!(read(int32(), leaf(tags::INT32, &[1, 0, 0])).is_none()); - assert!(read(int64(), leaf(tags::INT64, &1i32.to_le_bytes())).is_none()); - // No tag at all. - assert!(read(int32(), Protected::new(Vec::new())).is_none()); - // A float's leaf is not an integer's. - assert!(read(int32(), leaf(tags::FLOAT32, &1f32.to_le_bytes())).is_none()); + assert!(read::(Physical::Int(Int::I64(1))).is_none()); + assert!(read::(Physical::Int(Int::I32(1))).is_none()); + assert!(read::(Physical::Int(Int::U32(1))).is_none()); + // A float's leaf is not an integer's, nor a boolean's. + assert!(read::(Physical::Float(Float::F32(1.0))).is_none()); + assert!(read::(Physical::Int(Int::I32(1))).is_none()); + // An f32 leaf is not an f64, though it would widen exactly. + assert!(read::(Physical::Float(Float::F32(1.0))).is_none()); + assert!(read::(Physical::Float(Float::F64(1.0))).is_none()); + // A logical type on the leaf is not the bare scalar. + assert!(Int32::from_scalar(Some(Logical::Date), Physical::Int(Int::I32(1))).is_none()); } #[test] fn floats_read_their_bit_pattern() { - let (f32s, f64s) = (Float32::visitor, Float64::visitor); let nan32 = f32::from_bits(0x7fc0_1234); for v in [0.0, -0.0, 1.5, f32::INFINITY, f32::NEG_INFINITY, nan32] { - let opened = read(f32s(), leaf(tags::FLOAT32, &v.to_bits().to_le_bytes())).unwrap(); + let opened = read::(Physical::Float(Float::F32(v))).unwrap(); assert_eq!(opened.0.to_bits(), v.to_bits()); } let nan64 = f64::from_bits(0x7ff8_0000_0000_beef); for v in [0.0, -0.0, 1.5, f64::INFINITY, nan64] { - let opened = read(f64s(), leaf(tags::FLOAT64, &v.to_bits().to_le_bytes())).unwrap(); + let opened = read::(Physical::Float(Float::F64(v))).unwrap(); assert_eq!(opened.0.to_bits(), v.to_bits()); } - assert!(read(f64s(), leaf(tags::FLOAT32, &1f32.to_le_bytes())).is_none()); - assert!(read(f32s(), leaf(tags::FLOAT32, &1f64.to_le_bytes())).is_none()); } #[test] - fn booleans_are_the_tag_alone() { - assert!(read(BoolVisitor, Protected::new(vec![tags::BOOL_TRUE])) == Some(Bool(true))); - assert!(read(BoolVisitor, Protected::new(vec![tags::BOOL_FALSE])) == Some(Bool(false))); - // A payload after the tag, or another tag, is not a boolean. - assert!(read(BoolVisitor, Protected::new(vec![tags::BOOL_TRUE, 0])).is_none()); - assert!(read(BoolVisitor, leaf(tags::INT32, &1i32.to_le_bytes())).is_none()); - assert!(read(BoolVisitor, Protected::new(Vec::new())).is_none()); + fn booleans_read_both_values() { + assert!(read::(Physical::Bool(true)) == Some(Bool(true))); + assert!(read::(Physical::Bool(false)) == Some(Bool(false))); + assert!(read::(Physical::Null).is_none()); } #[test] diff --git a/packages/eql/crates/eql-bindings/src/encryption/targets.rs b/packages/eql/crates/eql-bindings/src/encryption/targets.rs index 764acb045..703cf4a15 100644 --- a/packages/eql/crates/eql-bindings/src/encryption/targets.rs +++ b/packages/eql/crates/eql-bindings/src/encryption/targets.rs @@ -79,12 +79,12 @@ use std::fmt; use serde::{de::DeserializeOwned, Serialize}; +use stack_encrypt::dynamic::{FfiValue, ValueKind}; use stack_encrypt::kms::MaybeSend; use stack_encrypt::target::ExpectedContext; use stack_encrypt::{ DecryptInto, EncryptFrom, Error, KeysetCipher, Label, NonEmpty, Pending, StackCipher, }; -use vitaminc_aead_value::{FfiValue, ValueKind}; use super::plaintext; use crate::v4::targets::{decrypt_named, encrypt_named, query_named, TARGETS}; diff --git a/packages/eql/crates/eql-bindings/src/v4/targets.rs b/packages/eql/crates/eql-bindings/src/v4/targets.rs index 1fb65935f..9b7170749 100644 --- a/packages/eql/crates/eql-bindings/src/v4/targets.rs +++ b/packages/eql/crates/eql-bindings/src/v4/targets.rs @@ -9,8 +9,8 @@ use crate::encryption::targets::{ open_target, refuse, refuse_query, run_target, Opener, Target, TargetError, }; use crate::Identifier; +use stack_encrypt::dynamic::FfiValue; use stack_encrypt::{KeysetCipher, NonEmpty, Pending}; -use vitaminc_aead_value::FfiValue; /// Every EQL type a plan may name as a target, in /// `eql-domains::CATALOG` order: one per stored domain (every flat /// scalar domain plus the SteVec document). Query twins are not diff --git a/packages/eql/crates/eql-codegen/src/targets.rs b/packages/eql/crates/eql-codegen/src/targets.rs index d13ae65dc..4b8f3d7b2 100644 --- a/packages/eql/crates/eql-codegen/src/targets.rs +++ b/packages/eql/crates/eql-codegen/src/targets.rs @@ -277,7 +277,7 @@ fn render_targets_from(rows: &[Row]) -> String { let file = quote! { #![doc = #mod_doc] - use vitaminc_aead_value::FfiValue; + use stack_encrypt::dynamic::FfiValue; #imports use crate::encryption::targets::{#helpers refuse, refuse_query, Opener, Target, TargetError}; diff --git a/packages/eql/tests/encryption/Cargo.toml b/packages/eql/tests/encryption/Cargo.toml index 993b27f7e..41f04494b 100644 --- a/packages/eql/tests/encryption/Cargo.toml +++ b/packages/eql/tests/encryption/Cargo.toml @@ -17,10 +17,11 @@ eql-bindings = { path = "../../crates/eql-bindings", features = ["stack-encrypt" # The parity oracle for the generated target table (tests/targets.rs): the # table must name every stored catalog domain, in catalog order. eql-domains = { path = "../../crates/eql-domains" } -stack-encrypt = { path = "../../../stack-encrypt", default-features = false } -# `FfiValue`: the runtime plaintext the target dispatch takes. The same line -# eql-bindings names. -vitaminc-aead-value = "0.5.1" +# `dynamic`: `FfiValue`, the runtime plaintext the target dispatch takes, +# as eql-bindings names it. +stack-encrypt = { path = "../../../stack-encrypt", default-features = false, features = ["dynamic"] } +# `plaintext::Sink`, for a plaintext type with its own `Plaintext` impl. +vitaminc-aead = "0.5.1" stack-kms = { path = "../../../stack-kms", default-features = false, features = ["test-support"] } serde = "1" serde_json = "1" diff --git a/packages/eql/tests/encryption/fixtures/v4_rows.json b/packages/eql/tests/encryption/fixtures/v4_rows.json index 7f233b031..932bc7da6 100644 --- a/packages/eql/tests/encryption/fixtures/v4_rows.json +++ b/packages/eql/tests/encryption/fixtures/v4_rows.json @@ -21,7 +21,7 @@ "t": "profiles", "c": "active" }, - "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAAA7z71FqWkCCWSkyPiBK7IQIAD0ug/nDu5R6UBEM9vJjtvDMBTuommRH359HgY7qUe5cAGqLPvdkpm6yw99+ZyvZ/D0GwxwF7jiS0n79tWSkg==" + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAAA7z71FqWkCCWSkyPiBK7IQIAD0ug/nDu5R6UBEM9vJjtvDMBTuommRH359HgY7qUe5cAKqQ9Rw3pcUd7BfnKYrOhzckWm8HdRMHxyVzd9RFw==" }, "balance": { "v": 4, @@ -29,7 +29,7 @@ "t": "profiles", "c": "balance" }, - "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAAAZJDEBcfVqv/o1B2CASJrAIAA9a/qD4wfv0t6fGVxDhKQOlQDIuYj5si13wnlmGENABAF7hMm8/oFuh1IxWGO3alDB86saK1iUeiWeC+NH10iDJzaWG8JP" + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAAAZJDEBcfVqv/o1B2CASJrAIAA9a/qD4wfv0t6fGVxDhKQOlQDIuYj5si13wnlmGENABAL2IJzA54Z5ngSRGOJOvqJHMZMxYFECpqo2ctbI2/lFvH4dB2UG" }, "name": { "v": 4, @@ -37,7 +37,7 @@ "t": "profiles", "c": "name" }, - "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAAASxNA10KeHfMqSIglat8/GIADzC2+uswJkz9HL6DqUBBNDtGODBX5yNHLKryZ0Lgc24AHLdWoZqUFokOA4X/RHwY8HLX/T8ltWVZz/heIAOGxpuGY=", + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAAASxNA10KeHfMqSIglat8/GIADzC2+uswJkz9HL6DqUBBNDtGODBX5yNHLKryZ0Lgc24AI7p9YY7R27PBmPccknBNdQVQ5bN3ZoYucccbZKDKHtZ5Oi", "bf": [ 59, 96, @@ -65,7 +65,7 @@ "t": "profiles", "c": "note" }, - "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAACaWtPMkbUZL3WUSHDD8aD7IADB6CfVElii+gglc1gys6JmpNlLoIFn47AvStPnpJb63wEGdkbtSd+MIQURFj5j9R0iQpEA9zXGHSnKKTDGDBmveJjSwGn2r0NA2cM=" + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAACaWtPMkbUZL3WUSHDD8aD7IADB6CfVElii+gglc1gys6JmpNlLoIFn47AvStPnpJb63wKnKP6D8GlSml9xks+a/2m67Xf/hmkdJoIjSiHatSvocY0rmNYwOK7y8D/1" }, "ratio": { "v": 4, @@ -73,7 +73,7 @@ "t": "profiles", "c": "ratio" }, - "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAAAuHszuxtkGGxdJ+yeeEgrtIACJ8iRTZnq+2wD3LmxdAqmDDHzDzzH5th5lG8EQq4qujQEsYLgPsjMiNaL8ePzWTx0zmDmQKF7VtItuQqEFLJUguIE=" + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAAAuHszuxtkGGxdJ+yeeEgrtIACJ8iRTZnq+2wD3LmxdAqmDDHzDzzH5th5lG8EQq4qujQLHE5wCg6qAj8/uMYEVG5qF6L5gXLr5QaZstV9YZc45ZOQ=" }, "score": { "v": 4, @@ -81,7 +81,7 @@ "t": "profiles", "c": "score" }, - "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAAAbQulFdQ7itXP4biDOHDhxIAA4jkCscZEzyZymEuOdqFk34IZ2lMPnDiYFZgGjl+Qh0gEDR8u18kphPu/+f4858qnbijVEzqzfHPJGCXD5w1l5bXR99M8f" + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAAAbQulFdQ7itXP4biDOHDhxIAA4jkCscZEzyZymEuOdqFk34IZ2lMPnDiYFZgGjl+Qh0gLxDHYdX6NYqmC8h8famVOyZaf25m+nnYQN9DUOpfNIY1VytlWH" }, "visits": { "v": 4, @@ -89,7 +89,7 @@ "t": "profiles", "c": "visits" }, - "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAAAFDIc6s4iXDM+dzEA4ntTLIACXLoQBRqkNqM7yC8syykzCq7A4M32xA4/pWvh7C8atkgGTGrJ9dKvtm/a2fJWaHbrmXpEgNIWno718QRpijgZ/hYA=" + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAAAFDIc6s4iXDM+dzEA4ntTLIACXLoQBRqkNqM7yC8syykzCq7A4M32xA4/pWvh7C8atkgLCJ46LW8E8GmjsBQyX5huv9Uu+kXR6u0IAiilhVTd0WUE=" } } }, @@ -111,7 +111,7 @@ "t": "profiles", "c": "active" }, - "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAABGXg/rWzFqWgcCw0KVmlV7IADK5ojyhDsQsCNphHB+3xjIrvP/87lbuOrEITfr0CumXgHI4YSxjlcPwyOXHLayGQAGvwWuAFlpYrH5X+d9Vg==" + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAABGXg/rWzFqWgcCw0KVmlV7IADK5ojyhDsQsCNphHB+3xjIrvP/87lbuOrEITfr0CumXgLUiEl5E+cspiz1C60uZ+fJwoQTdc+Jp+a5wakFmw==" }, "balance": { "v": 4, @@ -119,7 +119,7 @@ "t": "profiles", "c": "balance" }, - "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAAACHOuVBG9X5O4D9ApUC1DpIADAVYvyCPRFFVrNaWA5vGxA5PF75NkyEagzyfXSd1UIKwHr8BV896CyFbYSClca7yamexnn0aL1nH9V0jpHrTkTj1DFD5MC" + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAAACHOuVBG9X5O4D9ApUC1DpIADAVYvyCPRFFVrNaWA5vGxA5PF75NkyEagzyfXSd1UIKwLmqZbAOf4jZccVcQQuUwb7UNTUV2ss2ui6ssAwQzw+z8g0PEF4" }, "name": { "v": 4, @@ -127,7 +127,7 @@ "t": "profiles", "c": "name" }, - "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAAAlsDmtaCz7PVYXLYbtkdYiIADMhjWHFWFQffW6FoF5Kd0/vSO5UX+T+vZyisdueEEtugFjeI9b2b8JAmPZVA1YwGnWAQvTk/NzycoZCSO93CEGlPqUfS99", + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAAAlsDmtaCz7PVYXLYbtkdYiIADMhjWHFWFQffW6FoF5Kd0/vSO5UX+T+vZyisdueEEtugJ8AJTL+FsTQv84umI+UgkWWr5miQkExs7tdXZQZb8WjeymUZN0aw==", "bf": [ 3, 15, @@ -179,7 +179,7 @@ "t": "profiles", "c": "note" }, - "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAABtkZKQt8AupdgAkSYhBWuLIABushFe05tB4gQmcP9j+7VPX95cAW65PgvtgFajKobc6gFRQvqrq/jE0/A6z/h1wkJaprHCR2rzks2dQO0T" + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAABtkZKQt8AupdgAkSYhBWuLIABushFe05tB4gQmcP9j+7VPX95cAW65PgvtgFajKobc6gK+nIJANyuGOdzfonrK9/ZuZwh0i7+r+umozYODDw==" }, "ratio": { "v": 4, @@ -187,7 +187,7 @@ "t": "profiles", "c": "ratio" }, - "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAADJpOTT8kFqPHwznhWnYNgQIAC3GPCNDOMGBnh6gPSa5KTDr+VBQDNZAZr7kqKF16sx0QGRhJu9MKyPnOHlPvMXoeAJaiwDz/4FFzW39/K6QYHakms=" + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAADJpOTT8kFqPHwznhWnYNgQIAC3GPCNDOMGBnh6gPSa5KTDr+VBQDNZAZr7kqKF16sx0QLztGogKoty9XImWxjaNUYSF8LDSdG148wg16xStACiBvw=" }, "score": { "v": 4, @@ -195,7 +195,7 @@ "t": "profiles", "c": "score" }, - "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAABAfBAgu3xTGBg6oVcxVdCnIADQvBhh5sMdxmdosyrWjFQngkmQ+312sbVAFpgs2UBpcQFVAT9+lg/ey6n6zeWVQydtF2UVswD18AhJnGMs3DGhMVnxU7Ow" + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAABAfBAgu3xTGBg6oVcxVdCnIADQvBhh5sMdxmdosyrWjFQngkmQ+312sbVAFpgs2UBpcQLccQgtBqswM85HQZDzsPtZO6xgsqyDMtKBRKqHCzmWAvYCoF9W" }, "visits": { "v": 4, @@ -203,7 +203,7 @@ "t": "profiles", "c": "visits" }, - "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAABi/myUaRsBCFbuVTRBJLPiIADSnhrChMGg0gQaL8/2ghirKHXXC41P2aq5DghRI2FtWgHdF0NP0QUsZgASmJF0gwN1v4RdsYU6LW96F5S3NrNBBe0=" + "c": "stack-encrypt:1:AQAAAAAAAAAAAAAAAAAAAABi/myUaRsBCFbuVTRBJLPiIADSnhrChMGg0gQaL8/2ghirKHXXC41P2aq5DghRI2FtWgI+v9AtiXTz9LFxGu3KrmHaGwRcO9zEFfxpsWitt/WZob8=" } } } diff --git a/packages/eql/tests/encryption/tests/targets.rs b/packages/eql/tests/encryption/tests/targets.rs index d4089d024..fbf147bd9 100644 --- a/packages/eql/tests/encryption/tests/targets.rs +++ b/packages/eql/tests/encryption/tests/targets.rs @@ -9,8 +9,8 @@ use eql_bindings::encryption::targets::{self as targets, Opener, Target, TargetE use eql_bindings::v4::text::{TextEq, TextEqQuery}; use eql_bindings::{v4, Identifier}; use eql_domains::{Shape, Term, CATALOG}; +use stack_encrypt::dynamic::{FfiValue, ValueKind}; use stack_encrypt::Label; -use vitaminc_aead_value::{FfiValue, ValueKind}; fn email() -> Label { Label::new(["users", "email"]).unwrap() diff --git a/packages/eql/tests/encryption/tests/text_eq.rs b/packages/eql/tests/encryption/tests/text_eq.rs index d1bffd753..e5dd45372 100644 --- a/packages/eql/tests/encryption/tests/text_eq.rs +++ b/packages/eql/tests/encryption/tests/text_eq.rs @@ -7,7 +7,8 @@ use eql_bindings::v4::text::{TextEq, TextEqQuery}; use eql_bindings::v4::SchemaVersion; use eql_bindings::Identifier; use stack_encrypt::target::transcode::{Reader, Transcode}; -use stack_encrypt::{Cipher, Encrypt, IntoAad, NonEmpty}; +use stack_encrypt::{NonEmpty, Plaintext}; +use vitaminc_aead::plaintext::Sink; fn column() -> NonEmpty { Identifier::for_column("users", "email").unwrap() @@ -290,13 +291,14 @@ async fn a_column_batches_generation_and_retrieval_under_the_same_descriptors() #[derive(Clone)] struct WithoutSerde(String); -impl Encrypt for WithoutSerde { - fn encrypt_with_aad<'a, C: Cipher, A: IntoAad<'a>>( - self, - cipher: C, - aad: A, - ) -> Result { - self.0.encrypt_with_aad(cipher, aad) +impl Plaintext for WithoutSerde { + fn visit(self, sink: S) -> Result { + self.0.visit(sink) + } +} +impl Plaintext for &WithoutSerde { + fn visit(self, sink: S) -> Result { + (&self.0).visit(sink) } } #[tokio::test] diff --git a/packages/eql/tests/encryption/tests/v4_types.rs b/packages/eql/tests/encryption/tests/v4_types.rs index de19d5ec8..70ad17053 100644 --- a/packages/eql/tests/encryption/tests/v4_types.rs +++ b/packages/eql/tests/encryption/tests/v4_types.rs @@ -20,9 +20,9 @@ use eql_bindings::v3::terms::Ciphertext; use eql_bindings::v4::{bigint, boolean, double, integer, real, text}; use eql_bindings::Identifier; use serde::{de::DeserializeOwned, Serialize}; +use stack_encrypt::dynamic::{FfiValue, ValueKind}; use stack_encrypt::target::transcode::{Reader, Visitor}; use stack_encrypt::{CipherText, Label, NonEmpty}; -use vitaminc_aead_value::{FfiValue, ValueKind}; fn column() -> NonEmpty { Identifier::for_column("users", "age").unwrap() From bbc407587ff8712b5f79da6e75c074107d286066 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Fri, 16 Oct 2026 23:48:33 +1100 Subject: [PATCH 06/15] build!: pin vitaminc to the commit ore.rs builds against, and take its Encrypt rename The vitaminc crates came from `feat/aead-remove-legacy-traits` by branch name. That stack has merged into vitaminc main and the branch is gone, so a lockfile refresh could no longer resolve it. Every workspace that compiles stack-encrypt (the root, both Go guests, packages/eql and the fuzz crate) now pins one commit of cipherstash/vitaminc#464, vitaminc main plus the order-term comparison, which is the commit ore.rs's vitaminc implementation (cipherstash/ore.rs#115) builds against. Pinned by rev, not branch, so a merge cannot pull it out from under the build. Between the two commits vitaminc renamed the source trait `Plaintext` to `Encrypt`, to pair with `Decrypt`. stack-encrypt re-exports it under the new name, and every bound, impl, import and doc link in the stack crates, the guests, stack-guest-abi and eql-bindings follows. `TargetError:: Plaintext`, `Supports`'s `In::Plaintext` and eql-bindings' own `targets::Plaintext` are different things and keep their names. The trybuild expectations follow the rename. The other breaking changes since (Timestamp as plaintext, Copy normalizers, vitaminc-ore's column handle and compare) reach no code at this layer. v4_rows.json is unchanged: the leaves are the same format. --- Cargo.lock | 26 +++++++++---------- Cargo.toml | 23 ++++++++-------- languages/golang/auth/guest/Cargo.lock | 22 ++++++++-------- languages/golang/auth/guest/Cargo.toml | 15 +++++------ languages/golang/encrypt/guest/Cargo.lock | 26 +++++++++---------- languages/golang/encrypt/guest/Cargo.toml | 18 ++++++------- languages/golang/encrypt/guest/src/ops.rs | 4 +-- packages/eql/Cargo.lock | 26 +++++++++---------- packages/eql/Cargo.toml | 19 +++++++------- packages/eql/crates/eql-bindings/CHANGELOG.md | 2 +- .../eql/crates/eql-bindings/src/encryption.rs | 8 +++--- .../eql-bindings/src/encryption/plaintext.rs | 8 +++--- .../eql/tests/encryption/tests/text_eq.rs | 6 ++--- packages/stack-encrypt-derive/src/lib.rs | 4 +-- packages/stack-encrypt/CHANGELOG.md | 15 ++++++----- packages/stack-encrypt/examples/mixed_user.rs | 4 +-- packages/stack-encrypt/fuzz/Cargo.lock | 26 +++++++++---------- packages/stack-encrypt/fuzz/Cargo.toml | 18 ++++++------- packages/stack-encrypt/src/cipher.rs | 10 +++---- .../stack-encrypt/src/dynamic/ffi_value.rs | 10 +++---- packages/stack-encrypt/src/dynamic/mod.rs | 4 +-- packages/stack-encrypt/src/dynamic/value.rs | 6 ++--- packages/stack-encrypt/src/lib.rs | 10 +++---- packages/stack-encrypt/src/plan/build.rs | 6 ++--- packages/stack-encrypt/src/plan/chain.rs | 8 +++--- packages/stack-encrypt/src/plan/value.rs | 4 +-- packages/stack-encrypt/src/target/core.rs | 6 ++--- packages/stack-encrypt/src/target/index.rs | 4 +-- .../stack-encrypt/src/target/operations.rs | 4 +-- packages/stack-encrypt/tests/common/mod.rs | 4 +-- packages/stack-encrypt/tests/index.rs | 2 +- packages/stack-encrypt/tests/plan_grammar.rs | 4 +-- packages/stack-encrypt/tests/roundtrip.rs | 6 ++--- packages/stack-encrypt/tests/source_mode.rs | 10 +++---- packages/stack-encrypt/tests/transcode.rs | 10 +++---- .../tests/ui/owned_fan_out_needs_clone.rs | 4 +-- .../tests/ui/owned_fan_out_needs_clone.stderr | 4 +-- .../tests/ui/pass/owned_without_clone.rs | 4 +-- .../tests/ui/plaintext_needs_encrypt.stderr | 6 ++--- .../tests/ui/struct_field_must_open.rs | 6 ++--- packages/stack-guest-abi/src/value.rs | 8 +++--- 41 files changed, 204 insertions(+), 206 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 123d5c85a..d5d5f8a74 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4109,7 +4109,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vitaminc" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "vitaminc-aead", "vitaminc-context", @@ -4122,7 +4122,7 @@ dependencies = [ [[package]] name = "vitaminc-aead" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "bytes", "futures-util", @@ -4138,7 +4138,7 @@ dependencies = [ [[package]] name = "vitaminc-aead-derive" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "proc-macro2", "quote", @@ -4148,7 +4148,7 @@ dependencies = [ [[package]] name = "vitaminc-aead-frame" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "vitaminc-aead", ] @@ -4156,7 +4156,7 @@ dependencies = [ [[package]] name = "vitaminc-context" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "mutants", "vitaminc-protected", @@ -4165,7 +4165,7 @@ dependencies = [ [[package]] name = "vitaminc-encrypt" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "aes-gcm", "aws-lc-rs", @@ -4178,7 +4178,7 @@ dependencies = [ [[package]] name = "vitaminc-hmac" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "hmac", "sha2 0.11.0", @@ -4190,7 +4190,7 @@ dependencies = [ [[package]] name = "vitaminc-prf" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "mutants", "thiserror 2.0.18", @@ -4201,7 +4201,7 @@ dependencies = [ [[package]] name = "vitaminc-protected" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "bitvec", "digest 0.11.3", @@ -4217,7 +4217,7 @@ dependencies = [ [[package]] name = "vitaminc-protected-derive" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -4228,7 +4228,7 @@ dependencies = [ [[package]] name = "vitaminc-random" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "chacha20", "getrandom 0.4.2", @@ -4242,7 +4242,7 @@ dependencies = [ [[package]] name = "vitaminc-random-derives" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "proc-macro2", "quote", @@ -4252,7 +4252,7 @@ dependencies = [ [[package]] name = "vitaminc-traits" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "anyhow", "bytes", diff --git a/Cargo.toml b/Cargo.toml index 30bb41e57..eb32b99a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -130,17 +130,18 @@ vitaminc-hmac = "0.5.1" vitaminc-prf = "0.5.1" vitaminc-protected = "0.5.1" -# Temporary: every vitaminc crate this workspace uses comes from the branch -# that implements vitaminc ADR 0001 (cipherstash/vitaminc PRs #439 to #445, -# top branch `feat/aead-remove-legacy-traits`) until vitaminc releases it. -# Replace this block with the released versions then. The Go guests are +# Temporary: every vitaminc crate this workspace uses comes from one commit +# of cipherstash/vitaminc#464, which is vitaminc main (ADR 0001 and +# vitaminc-ore) plus the order-term comparison ore.rs builds against, until +# vitaminc releases 0.6. Pinned by `rev`, never by branch: a branch is deleted +# when its stack merges. Replace this block with the released versions then. The Go guests are # detached workspaces and carry the same block, so every build of these # crates sees one vitaminc. [patch.crates-io] -vitaminc = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } -vitaminc-aead = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } -vitaminc-aead-frame = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } -vitaminc-encrypt = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } -vitaminc-hmac = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } -vitaminc-prf = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } -vitaminc-protected = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } +vitaminc = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } +vitaminc-aead = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } +vitaminc-aead-frame = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } +vitaminc-encrypt = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } +vitaminc-hmac = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } +vitaminc-prf = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } +vitaminc-protected = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } diff --git a/languages/golang/auth/guest/Cargo.lock b/languages/golang/auth/guest/Cargo.lock index 141489710..cff369660 100644 --- a/languages/golang/auth/guest/Cargo.lock +++ b/languages/golang/auth/guest/Cargo.lock @@ -2286,7 +2286,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vitaminc" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "vitaminc-aead", "vitaminc-context", @@ -2299,7 +2299,7 @@ dependencies = [ [[package]] name = "vitaminc-aead" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "bytes", "futures-util", @@ -2315,7 +2315,7 @@ dependencies = [ [[package]] name = "vitaminc-aead-derive" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "proc-macro2", "quote", @@ -2325,7 +2325,7 @@ dependencies = [ [[package]] name = "vitaminc-aead-frame" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "vitaminc-aead", ] @@ -2333,7 +2333,7 @@ dependencies = [ [[package]] name = "vitaminc-context" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "mutants", "vitaminc-protected", @@ -2342,7 +2342,7 @@ dependencies = [ [[package]] name = "vitaminc-encrypt" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "aes-gcm", "aws-lc-rs", @@ -2355,7 +2355,7 @@ dependencies = [ [[package]] name = "vitaminc-protected" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "bitvec", "digest 0.11.3", @@ -2371,7 +2371,7 @@ dependencies = [ [[package]] name = "vitaminc-protected-derive" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -2382,7 +2382,7 @@ dependencies = [ [[package]] name = "vitaminc-random" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "chacha20", "getrandom 0.4.3", @@ -2396,7 +2396,7 @@ dependencies = [ [[package]] name = "vitaminc-random-derives" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "proc-macro2", "quote", @@ -2406,7 +2406,7 @@ dependencies = [ [[package]] name = "vitaminc-traits" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "anyhow", "bytes", diff --git a/languages/golang/auth/guest/Cargo.toml b/languages/golang/auth/guest/Cargo.toml index 39973dab4..2d8a72380 100644 --- a/languages/golang/auth/guest/Cargo.toml +++ b/languages/golang/auth/guest/Cargo.toml @@ -19,15 +19,14 @@ publish = false [workspace] # Temporary, as in the root Cargo.toml: the vitaminc crates stack-guest-abi -# and the stack crates build on come from the branch that implements -# vitaminc ADR 0001 (cipherstash/vitaminc PRs #439 to #445) until vitaminc -# releases it. +# and the stack crates build on come from the same vitaminc commit +# (cipherstash/vitaminc#464) until vitaminc releases 0.6. [patch.crates-io] -vitaminc = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } -vitaminc-aead = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } -vitaminc-aead-frame = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } -vitaminc-encrypt = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } -vitaminc-protected = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } +vitaminc = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } +vitaminc-aead = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } +vitaminc-aead-frame = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } +vitaminc-encrypt = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } +vitaminc-protected = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } [lib] # cdylib: the .wasm guest module. rlib: lets the ops/status modules diff --git a/languages/golang/encrypt/guest/Cargo.lock b/languages/golang/encrypt/guest/Cargo.lock index ef9e3436e..067fd6f9c 100644 --- a/languages/golang/encrypt/guest/Cargo.lock +++ b/languages/golang/encrypt/guest/Cargo.lock @@ -2610,7 +2610,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vitaminc" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "vitaminc-aead", "vitaminc-context", @@ -2623,7 +2623,7 @@ dependencies = [ [[package]] name = "vitaminc-aead" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "bytes", "futures-util", @@ -2639,7 +2639,7 @@ dependencies = [ [[package]] name = "vitaminc-aead-derive" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "proc-macro2", "quote", @@ -2649,7 +2649,7 @@ dependencies = [ [[package]] name = "vitaminc-aead-frame" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "vitaminc-aead", ] @@ -2657,7 +2657,7 @@ dependencies = [ [[package]] name = "vitaminc-context" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "mutants", "vitaminc-protected", @@ -2666,7 +2666,7 @@ dependencies = [ [[package]] name = "vitaminc-encrypt" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "aes-gcm", "aws-lc-rs", @@ -2679,7 +2679,7 @@ dependencies = [ [[package]] name = "vitaminc-hmac" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "hmac", "sha2 0.11.0", @@ -2691,7 +2691,7 @@ dependencies = [ [[package]] name = "vitaminc-prf" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "mutants", "thiserror 2.0.20", @@ -2702,7 +2702,7 @@ dependencies = [ [[package]] name = "vitaminc-protected" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "bitvec", "digest 0.11.3", @@ -2718,7 +2718,7 @@ dependencies = [ [[package]] name = "vitaminc-protected-derive" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -2729,7 +2729,7 @@ dependencies = [ [[package]] name = "vitaminc-random" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "chacha20", "getrandom 0.4.3", @@ -2743,7 +2743,7 @@ dependencies = [ [[package]] name = "vitaminc-random-derives" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "proc-macro2", "quote", @@ -2753,7 +2753,7 @@ dependencies = [ [[package]] name = "vitaminc-traits" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "anyhow", "bytes", diff --git a/languages/golang/encrypt/guest/Cargo.toml b/languages/golang/encrypt/guest/Cargo.toml index 4cef89332..bbe677570 100644 --- a/languages/golang/encrypt/guest/Cargo.toml +++ b/languages/golang/encrypt/guest/Cargo.toml @@ -18,16 +18,16 @@ publish = false [workspace] # Temporary, as in the root Cargo.toml: the vitaminc crates this guest and -# stack-encrypt build on come from the branch that implements vitaminc ADR -# 0001 (cipherstash/vitaminc PRs #439 to #445) until vitaminc releases it. +# stack-encrypt build on come from the same vitaminc commit +# (cipherstash/vitaminc#464) until vitaminc releases 0.6. [patch.crates-io] -vitaminc = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } -vitaminc-aead = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } -vitaminc-aead-frame = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } -vitaminc-encrypt = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } -vitaminc-hmac = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } -vitaminc-prf = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } -vitaminc-protected = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } +vitaminc = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } +vitaminc-aead = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } +vitaminc-aead-frame = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } +vitaminc-encrypt = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } +vitaminc-hmac = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } +vitaminc-prf = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } +vitaminc-protected = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } [lib] # cdylib: the .wasm guest module. rlib: lets the ops/config/status modules diff --git a/languages/golang/encrypt/guest/src/ops.rs b/languages/golang/encrypt/guest/src/ops.rs index 30d5008a9..2d85fbb5b 100644 --- a/languages/golang/encrypt/guest/src/ops.rs +++ b/languages/golang/encrypt/guest/src/ops.rs @@ -40,9 +40,7 @@ use stack_encrypt::dynamic::{self, FfiValue, Scalar, Scope}; use stack_encrypt::sem::MatchOptions; use stack_encrypt::target::IndexSpec; use stack_encrypt::ErrorPayload; -use stack_encrypt::{ - CipherText, KeysetCipher, Label, Node, Plaintext, SealedValue, StackCipherText, -}; +use stack_encrypt::{CipherText, Encrypt, KeysetCipher, Label, Node, SealedValue, StackCipherText}; use stack_kms::DataKeySource; use vitaminc_aead_frame::{decode_ciphertext, encode_ciphertext, FrameWriter}; use vitaminc_protected::Controlled; diff --git a/packages/eql/Cargo.lock b/packages/eql/Cargo.lock index f7f798b99..072bd0f6e 100644 --- a/packages/eql/Cargo.lock +++ b/packages/eql/Cargo.lock @@ -5032,7 +5032,7 @@ dependencies = [ [[package]] name = "vitaminc" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "vitaminc-aead 0.5.1", "vitaminc-context", @@ -5058,7 +5058,7 @@ dependencies = [ [[package]] name = "vitaminc-aead" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "bytes", "futures-util", @@ -5074,7 +5074,7 @@ dependencies = [ [[package]] name = "vitaminc-aead-derive" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "proc-macro2", "quote", @@ -5084,7 +5084,7 @@ dependencies = [ [[package]] name = "vitaminc-aead-frame" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "vitaminc-aead 0.5.1", ] @@ -5092,7 +5092,7 @@ dependencies = [ [[package]] name = "vitaminc-context" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "mutants", "vitaminc-protected 0.5.1", @@ -5115,7 +5115,7 @@ dependencies = [ [[package]] name = "vitaminc-encrypt" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "aes-gcm", "aws-lc-rs", @@ -5128,7 +5128,7 @@ dependencies = [ [[package]] name = "vitaminc-hmac" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "hmac 0.13.0", "sha2 0.11.0", @@ -5140,7 +5140,7 @@ dependencies = [ [[package]] name = "vitaminc-prf" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "mutants", "thiserror 2.0.20", @@ -5166,7 +5166,7 @@ dependencies = [ [[package]] name = "vitaminc-protected" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "bitvec", "digest 0.11.3", @@ -5193,7 +5193,7 @@ dependencies = [ [[package]] name = "vitaminc-protected-derive" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -5218,7 +5218,7 @@ dependencies = [ [[package]] name = "vitaminc-random" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "chacha20", "getrandom 0.4.2", @@ -5243,7 +5243,7 @@ dependencies = [ [[package]] name = "vitaminc-random-derives" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "proc-macro2", "quote", @@ -5269,7 +5269,7 @@ dependencies = [ [[package]] name = "vitaminc-traits" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "anyhow", "bytes", diff --git a/packages/eql/Cargo.toml b/packages/eql/Cargo.toml index e12e2ec0c..2452354eb 100644 --- a/packages/eql/Cargo.toml +++ b/packages/eql/Cargo.toml @@ -38,14 +38,13 @@ unused_imports = "deny" # Temporary, as in the repository root's Cargo.toml: eql-bindings' # `stack-encrypt` feature and tests/encryption compile stack-encrypt by path, -# so the vitaminc crates it builds on come from the branch that implements -# vitaminc ADR 0001 (cipherstash/vitaminc PRs #439 to #445) until vitaminc -# releases it. +# so the vitaminc crates it builds on come from the same vitaminc commit +# (cipherstash/vitaminc#464) until vitaminc releases 0.6. [patch.crates-io] -vitaminc = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } -vitaminc-aead = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } -vitaminc-aead-frame = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } -vitaminc-encrypt = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } -vitaminc-hmac = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } -vitaminc-prf = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } -vitaminc-protected = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } +vitaminc = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } +vitaminc-aead = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } +vitaminc-aead-frame = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } +vitaminc-encrypt = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } +vitaminc-hmac = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } +vitaminc-prf = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } +vitaminc-protected = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } diff --git a/packages/eql/crates/eql-bindings/CHANGELOG.md b/packages/eql/crates/eql-bindings/CHANGELOG.md index c4ea4cf1e..61cbda704 100644 --- a/packages/eql/crates/eql-bindings/CHANGELOG.md +++ b/packages/eql/crates/eql-bindings/CHANGELOG.md @@ -39,7 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `encryption::targets` takes `FfiValue` and `ValueKind` from `stack_encrypt::dynamic` (the feature turns on stack-encrypt's `dynamic` feature) in place of `vitaminc-aead-value`, which is gone, and the - `Ciphertext` target takes any `Plaintext` plaintext and opens into any + `Ciphertext` target takes any vitaminc `Encrypt` plaintext and opens into any `Decrypt + Send` type, as stack-encrypt's own targets do. - **Query-operand domain names switched to the `query_` prefix, homed diff --git a/packages/eql/crates/eql-bindings/src/encryption.rs b/packages/eql/crates/eql-bindings/src/encryption.rs index d072d0972..2f4c64673 100644 --- a/packages/eql/crates/eql-bindings/src/encryption.rs +++ b/packages/eql/crates/eql-bindings/src/encryption.rs @@ -63,7 +63,7 @@ //! An encrypted EQL record is not a plaintext value: //! ```compile_fail //! use eql_bindings::v4::text::TextEq; -//! fn require() {} +//! fn require() {} //! require::(); //! ``` //! The text domain accepts text, and requires a concrete column identifier: @@ -89,8 +89,8 @@ use stack_encrypt::target::transcode::{Reader, Transcode, Visitor}; use stack_encrypt::target::{self, AeadContext, CallerContext}; use stack_encrypt::{ CipherText, ContextPiece, Decrypt, DecryptField, DecryptInto, Decryptable, Decryption, - Describe, Description, EncryptFrom, Encryption, Error, IntoContext, MaybeEmpty, NonEmpty, - Plaintext, SealedValue, StackCipherText, + Describe, Description, Encrypt, EncryptFrom, Encryption, Error, IntoContext, MaybeEmpty, + NonEmpty, SealedValue, StackCipherText, }; use vitaminc_prf::PrfValue; @@ -178,7 +178,7 @@ impl Reader for Ciphertext { } } -impl EncryptFrom for Ciphertext { +impl EncryptFrom for Ciphertext { type Context = AeadContext; fn encryption<'s, K: 'static>() -> Encryption<'s, S, Self, K, Self::Context> where diff --git a/packages/eql/crates/eql-bindings/src/encryption/plaintext.rs b/packages/eql/crates/eql-bindings/src/encryption/plaintext.rs index 3ce960c4a..4c990f5fc 100644 --- a/packages/eql/crates/eql-bindings/src/encryption/plaintext.rs +++ b/packages/eql/crates/eql-bindings/src/encryption/plaintext.rs @@ -1,7 +1,7 @@ //! Scalar plaintexts for the v4 number and boolean types. //! //! A Stack Encrypt target encrypts a plaintext through Vitamin C's -//! `Plaintext` and reads it back through `Decrypt`. These newtypes name the +//! `Encrypt` and reads it back through `Decrypt`. These newtypes name the //! plaintext of each storage-only EQL v4 type: [`Int32`] for `Integer`, //! [`Int64`] for `Bigint`, [`Bool`] for `Boolean`, and [`Float32`] / //! [`Float64`] for `Real` and `Double`. @@ -32,7 +32,7 @@ //! None of them implements `Debug`, so a plaintext does not reach a log by //! accident. -use stack_encrypt::Plaintext; +use stack_encrypt::Encrypt; use vitaminc_aead::{Float, FromScalar, Int, Logical, Physical, Sink}; use zeroize::Zeroize; @@ -67,7 +67,7 @@ macro_rules! fixed_scalar { } /// Describes itself as the primitive it wraps. - impl Plaintext for $name { + impl Encrypt for $name { fn visit(self, sink: S) -> Result { self.0.visit(sink) } @@ -122,7 +122,7 @@ impl From for bool { } /// Describes itself as a `bool`. -impl Plaintext for Bool { +impl Encrypt for Bool { fn visit(self, sink: S) -> Result { self.0.visit(sink) } diff --git a/packages/eql/tests/encryption/tests/text_eq.rs b/packages/eql/tests/encryption/tests/text_eq.rs index e5dd45372..2c275c93f 100644 --- a/packages/eql/tests/encryption/tests/text_eq.rs +++ b/packages/eql/tests/encryption/tests/text_eq.rs @@ -7,7 +7,7 @@ use eql_bindings::v4::text::{TextEq, TextEqQuery}; use eql_bindings::v4::SchemaVersion; use eql_bindings::Identifier; use stack_encrypt::target::transcode::{Reader, Transcode}; -use stack_encrypt::{NonEmpty, Plaintext}; +use stack_encrypt::{Encrypt, NonEmpty}; use vitaminc_aead::plaintext::Sink; fn column() -> NonEmpty { @@ -291,12 +291,12 @@ async fn a_column_batches_generation_and_retrieval_under_the_same_descriptors() #[derive(Clone)] struct WithoutSerde(String); -impl Plaintext for WithoutSerde { +impl Encrypt for WithoutSerde { fn visit(self, sink: S) -> Result { self.0.visit(sink) } } -impl Plaintext for &WithoutSerde { +impl Encrypt for &WithoutSerde { fn visit(self, sink: S) -> Result { (&self.0).visit(sink) } diff --git a/packages/stack-encrypt-derive/src/lib.rs b/packages/stack-encrypt-derive/src/lib.rs index 05170d5f1..7861385d4 100644 --- a/packages/stack-encrypt-derive/src/lib.rs +++ b/packages/stack-encrypt-derive/src/lib.rs @@ -108,7 +108,7 @@ //! use stack_encrypt::target::transcode::{Transcode, Visitor}; //! use stack_encrypt::target::{self, CallerContext}; //! use stack_encrypt::{ -//! CipherText, Decryptable, EncryptFrom, Encryption, Error, Plaintext, SealedValue, +//! CipherText, Decryptable, EncryptFrom, Encryption, Error, Encrypt, SealedValue, //! StackCipher, nonempty, //! }; //! use stack_encrypt::kms::FakeDataKeySource; @@ -138,7 +138,7 @@ //! } //! // The declaration: the canonical ciphertext operation, read into this type. //! // It seals under the AEAD half of the context the record threads to it. -//! impl EncryptFrom for LeafBytes { +//! impl EncryptFrom for LeafBytes { //! type Context = CallerContext; //! fn encryption<'s, K: 'static>() -> Encryption<'s, S, Self, K, Self::Context> //! where diff --git a/packages/stack-encrypt/CHANGELOG.md b/packages/stack-encrypt/CHANGELOG.md index 218c15b49..68c9daa3c 100644 --- a/packages/stack-encrypt/CHANGELOG.md +++ b/packages/stack-encrypt/CHANGELOG.md @@ -142,13 +142,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 same way. Storing the empty filter tells a reader of the database that the value is empty, separator-only or shorter than the n-gram length (ADR-0009). -- **Values describe themselves with vitaminc's `Plaintext` and read back +- **Values describe themselves with vitaminc's new `Encrypt` and read back with `Decrypt` (vitaminc ADR 0001).** `KeysetCipher::encrypt`, - `StackCipher::decrypt` and every target take `Plaintext` where they took - `Encrypt`, and `Decrypt` where they took `Decrypt<'static>`; derive both - with `vitaminc_aead::{Plaintext, Decrypt}`. `Encrypt`, `Decipher`, - `Element` and `Cipher` are no longer re-exported, and `StackDecipher` and - `BoxedPassthrough` are gone. A pending tree is planned rather than driven: + `StackCipher::decrypt` and every target take the new `Encrypt`, a source + that describes its value to a sink, where they took the old + visitor-driven `Encrypt`, and `Decrypt` where they took + `Decrypt<'static>`; derive both with `vitaminc_aead::{Encrypt, Decrypt}` + (`stack_encrypt::{Encrypt, Decrypt}` re-exports them). The name is the + same and the trait is not: a hand-written impl of the old one does not + compile against the new one. `Decipher`, `Element` and `Cipher` are no + longer re-exported, and `StackDecipher` and `BoxedPassthrough` are gone. A pending tree is planned rather than driven: `PendingStackCipherText::plan(value, aad)`, then `seal` or `into_pending` as before. The key fetch is unchanged: one batched `generate_keys` per assembly to seal, one `retrieve_keys` per keyset to diff --git a/packages/stack-encrypt/examples/mixed_user.rs b/packages/stack-encrypt/examples/mixed_user.rs index f7cd18004..b2257026d 100644 --- a/packages/stack-encrypt/examples/mixed_user.rs +++ b/packages/stack-encrypt/examples/mixed_user.rs @@ -38,7 +38,7 @@ //! `zerokms_auth` example for the lookup order). use stack_encrypt::{CipherText, Label, StackCipher, StackCipherText}; -use vitaminc_aead::{Decrypt, Plaintext}; +use vitaminc_aead::{Decrypt, Encrypt}; // --- The record type --------------------------------------------------------- @@ -46,7 +46,7 @@ use vitaminc_aead::{Decrypt, Plaintext}; // the clear; each *encrypted* value is sealed against an AAD derived from the // map's AAD + its key, so entries cannot be renamed or swapped. Passthrough // entries carry no such binding. -#[derive(Debug, PartialEq, Plaintext, Decrypt)] +#[derive(Debug, PartialEq, Encrypt, Decrypt)] struct User { #[aead(passthrough)] id: u32, // visible in the stored ciphertext diff --git a/packages/stack-encrypt/fuzz/Cargo.lock b/packages/stack-encrypt/fuzz/Cargo.lock index 635728183..f9e70a6aa 100644 --- a/packages/stack-encrypt/fuzz/Cargo.lock +++ b/packages/stack-encrypt/fuzz/Cargo.lock @@ -2513,7 +2513,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vitaminc" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "vitaminc-aead", "vitaminc-context", @@ -2526,7 +2526,7 @@ dependencies = [ [[package]] name = "vitaminc-aead" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "bytes", "futures-util", @@ -2542,7 +2542,7 @@ dependencies = [ [[package]] name = "vitaminc-aead-derive" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "proc-macro2", "quote", @@ -2552,7 +2552,7 @@ dependencies = [ [[package]] name = "vitaminc-aead-frame" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "vitaminc-aead", ] @@ -2560,7 +2560,7 @@ dependencies = [ [[package]] name = "vitaminc-context" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "mutants", "vitaminc-protected", @@ -2569,7 +2569,7 @@ dependencies = [ [[package]] name = "vitaminc-encrypt" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "aes-gcm", "aws-lc-rs", @@ -2582,7 +2582,7 @@ dependencies = [ [[package]] name = "vitaminc-hmac" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "hmac", "sha2 0.11.0", @@ -2594,7 +2594,7 @@ dependencies = [ [[package]] name = "vitaminc-prf" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "mutants", "thiserror 2.0.18", @@ -2605,7 +2605,7 @@ dependencies = [ [[package]] name = "vitaminc-protected" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "bitvec", "digest 0.11.3", @@ -2621,7 +2621,7 @@ dependencies = [ [[package]] name = "vitaminc-protected-derive" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -2632,7 +2632,7 @@ dependencies = [ [[package]] name = "vitaminc-random" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "chacha20", "getrandom 0.4.2", @@ -2646,7 +2646,7 @@ dependencies = [ [[package]] name = "vitaminc-random-derives" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "proc-macro2", "quote", @@ -2656,7 +2656,7 @@ dependencies = [ [[package]] name = "vitaminc-traits" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?branch=feat%2Faead-remove-legacy-traits#2e895cb9caedeb04d1adc7d98fc3b067d24fd35d" +source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" dependencies = [ "anyhow", "bytes", diff --git a/packages/stack-encrypt/fuzz/Cargo.toml b/packages/stack-encrypt/fuzz/Cargo.toml index 8db1c8589..56b105d5e 100644 --- a/packages/stack-encrypt/fuzz/Cargo.toml +++ b/packages/stack-encrypt/fuzz/Cargo.toml @@ -65,13 +65,13 @@ bench = false resolver = "2" # Temporary, as in the root Cargo.toml: the vitaminc crates stack-encrypt -# builds on come from the branch that implements vitaminc ADR 0001 -# (cipherstash/vitaminc PRs #439 to #445) until vitaminc releases it. +# builds on come from the same vitaminc commit (cipherstash/vitaminc#464) +# until vitaminc releases 0.6. [patch.crates-io] -vitaminc = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } -vitaminc-aead = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } -vitaminc-aead-frame = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } -vitaminc-encrypt = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } -vitaminc-hmac = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } -vitaminc-prf = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } -vitaminc-protected = { git = "https://github.com/cipherstash/vitaminc", branch = "feat/aead-remove-legacy-traits" } +vitaminc = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } +vitaminc-aead = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } +vitaminc-aead-frame = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } +vitaminc-encrypt = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } +vitaminc-hmac = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } +vitaminc-prf = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } +vitaminc-protected = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } diff --git a/packages/stack-encrypt/src/cipher.rs b/packages/stack-encrypt/src/cipher.rs index 7f94d9664..8d48b95d5 100644 --- a/packages/stack-encrypt/src/cipher.rs +++ b/packages/stack-encrypt/src/cipher.rs @@ -5,7 +5,7 @@ //! cipher bound to one of that client's keysets — seals values whose //! per-leaf keys are ZeroKMS data keys, minted under that keyset, rather //! than one fixed key. The value side is vitaminc's: a value describes -//! itself once ([`Plaintext`]), vitaminc plans it into a tree of pending +//! itself once ([`Encrypt`]), vitaminc plans it into a tree of pending //! leaves, each with its tagged plaintext and the AAD its place derives, //! and a bytes-only [`Cipher`] seals and opens one leaf at a time. The //! result is a ciphertext tree ([`StackCipherText`], the analog of @@ -105,8 +105,8 @@ use stack_kms::{FallbackKeyProvider, KeyProvider, KeyProviderError, ProfileStore use uuid::Uuid; use vitaminc_aead::seal::seal_all; use vitaminc_aead::{ - Cipher, CipherText, Context, Decrypt, IntoAad, IntoContext, LocalCipherText, Node, Pending, - Plaintext, Unspecified, + Cipher, CipherText, Context, Decrypt, Encrypt, IntoAad, IntoContext, LocalCipherText, Node, + Pending, Unspecified, }; use vitaminc_encrypt::{Aes256Cipher, Key as AesKey}; use vitaminc_protected::Protected; @@ -818,7 +818,7 @@ impl KeysetCipher<'_, K> { /// Every key is requested under the [`Descriptor`] of `aad`. pub async fn encrypt<'a, T, A>(&self, value: T, aad: A) -> Result where - T: Plaintext, + T: Encrypt, A: IntoContext<'a>, { let context = aad.into_context(); @@ -1272,7 +1272,7 @@ impl PendingStackCipherText { /// key, a container whose every child is a passthrough, a nesting deeper /// than the transport carries) is [`Error::Aead`], as it is when the /// value is sealed in one step. - pub fn plan<'a, P: Plaintext>(value: P, aad: impl IntoAad<'a>) -> Result { + pub fn plan<'a, P: Encrypt>(value: P, aad: impl IntoAad<'a>) -> Result { vitaminc_aead::plan::<_, Unspecified, _>(value, aad) .map(Self) .map_err(|_| Error::Aead) diff --git a/packages/stack-encrypt/src/dynamic/ffi_value.rs b/packages/stack-encrypt/src/dynamic/ffi_value.rs index 231d5db48..51073df99 100644 --- a/packages/stack-encrypt/src/dynamic/ffi_value.rs +++ b/packages/stack-encrypt/src/dynamic/ffi_value.rs @@ -3,7 +3,7 @@ //! //! Both were `vitaminc-aead-value`'s until vitaminc ADR 0001 reduced that //! crate to the transport (`vitaminc-aead-frame`). A value now describes -//! itself to vitaminc through [`Plaintext`] and is read back through a +//! itself to vitaminc through [`Encrypt`] and is read back through a //! [`Decrypt`] sink, so an [`FfiValue`] seals the same tagged leaves a //! typed Rust value of the same kind does, and crosses an FFI boundary as a //! transport frame ([`to_frame`](FfiValue::to_frame), @@ -19,7 +19,7 @@ use vitaminc_aead::plaintext::{ visit_map, visit_seq, Error as _, Float, Int, Logical, MapSink, Physical, SeqSink, Sink, SourceError, }; -use vitaminc_aead::{to_frame, DecodeError, Decrypt, Frame, Plaintext}; +use vitaminc_aead::{to_frame, DecodeError, Decrypt, Encrypt, Frame}; use vitaminc_protected::{Controlled, Protected}; use zeroize::Zeroize; @@ -163,7 +163,7 @@ pub enum FromFrameError { /// Walks the value, leaving it where it is. Text and bytes are handed to /// the sink as borrows of the `Protected` payloads. -impl Plaintext for &FfiValue { +impl Encrypt for &FfiValue { fn visit(self, sink: S) -> Result { match self { FfiValue::Null => sink.scalar(None, Physical::Null), @@ -193,7 +193,7 @@ impl Plaintext for &FfiValue { /// Walks the value by reference, then drops it, which wipes its text and /// bytes. -impl Plaintext for FfiValue { +impl Encrypt for FfiValue { fn visit(self, sink: S) -> Result { (&self).visit(sink) } @@ -304,7 +304,7 @@ impl Sink for ValueSink { }) } - fn passthrough(self, value: P) -> Result { + fn passthrough(self, value: P) -> Result { let inner = value.visit(ValueSink(Root))?; Ok(self.0.put(FfiValue::Passthrough(Box::new(inner)))) } diff --git a/packages/stack-encrypt/src/dynamic/mod.rs b/packages/stack-encrypt/src/dynamic/mod.rs index 225b41a7e..78de96b30 100644 --- a/packages/stack-encrypt/src/dynamic/mod.rs +++ b/packages/stack-encrypt/src/dynamic/mod.rs @@ -1,7 +1,7 @@ //! Encrypting values whose type is known only at runtime. //! //! Everything else in this crate is typed: a target names its source type, -//! and the operations it composes are reached through bounds — `Plaintext` for +//! and the operations it composes are reached through bounds — `Encrypt` for //! a ciphertext, [`PrfValue`](vitaminc_prf::PrfValue) for an equality term, //! `AsRef` for a match term. That is what makes a term a cross-language //! contract: `equality_term(34u32)` derives the same bytes wherever it is @@ -37,7 +37,7 @@ //! //! # What is not here //! -//! Encrypting a whole value is not: [`FfiValue`] implements `Plaintext` +//! Encrypting a whole value is not: [`FfiValue`] implements `Encrypt` //! already, so `keyset.encrypt(value, aad)` is the whole of it and needs //! nothing from this module. Nor is a second executor: nothing here calls //! the term functions or the seal path to produce a record. A capability a diff --git a/packages/stack-encrypt/src/dynamic/value.rs b/packages/stack-encrypt/src/dynamic/value.rs index e632d892e..4d92c52dc 100644 --- a/packages/stack-encrypt/src/dynamic/value.rs +++ b/packages/stack-encrypt/src/dynamic/value.rs @@ -3,7 +3,7 @@ use std::fmt; use vitaminc_aead::plaintext::Sink; -use vitaminc_aead::{Decrypt, Plaintext}; +use vitaminc_aead::{Decrypt, Encrypt}; use vitaminc_protected::{Controlled, Protected}; use super::{FfiValue, Put, ValueSink}; @@ -105,13 +105,13 @@ fn duplicate(value: &FfiValue) -> FfiValue { } } -impl Plaintext for Value { +impl Encrypt for Value { fn visit(self, sink: S) -> Result { self.0.visit(sink) } } -impl Plaintext for &Value { +impl Encrypt for &Value { fn visit(self, sink: S) -> Result { (&self.0).visit(sink) } diff --git a/packages/stack-encrypt/src/lib.rs b/packages/stack-encrypt/src/lib.rs index 264699881..8afe073e5 100644 --- a/packages/stack-encrypt/src/lib.rs +++ b/packages/stack-encrypt/src/lib.rs @@ -26,9 +26,9 @@ //! the cipher bound to one of that client's keysets, from //! [`default_keyset`](StackCipher::default_keyset) or //! [`keyset`](StackCipher::keyset) — encrypts any value that implements -//! vitaminc's [`Plaintext`] (`String`, `Vec`, `HashMap`, +//! vitaminc's [`Encrypt`] (`String`, `Vec`, `HashMap`, //! `Option`, `Protected`, your own types through -//! `#[derive(vitaminc_aead::Plaintext)]`, and any nesting of them). Either +//! `#[derive(vitaminc_aead::Encrypt)]`, and any nesting of them). Either //! cipher decrypts back into any [`Decrypt`] type. Every scalar inside the value is //! sealed under its **own** ZeroKMS data key, so each value access is an //! individually auditable key retrieval — there is no long-lived key in your @@ -306,13 +306,13 @@ pub use target::{ // Re-export the vitaminc AEAD surface callers need to drive the cipher, so they // don't have to depend on `vitaminc-aead` directly for the common path: a -// value describes itself once (`Plaintext`) and names the sink that reads it +// value describes itself once (`Encrypt`) and names the sink that reads it // back (`Decrypt`). A context type of your own implements `IntoContext` once; // `IntoAad` and `IntoPrfContext` are vitaminc's blankets over it, so both // derivations see the same bytes. pub use vitaminc_aead::{ - CipherText, Context, ContextPiece, ContextTag, Decrypt, IntoAad, IntoContext, Node, - PassthroughFrame, Plaintext, Unspecified, + CipherText, Context, ContextPiece, ContextTag, Decrypt, Encrypt, IntoAad, IntoContext, Node, + PassthroughFrame, Unspecified, }; // The vitaminc 0.4 names, deprecated there; re-exported for one transition so // a caller that spelled them keeps compiling with a warning. diff --git a/packages/stack-encrypt/src/plan/build.rs b/packages/stack-encrypt/src/plan/build.rs index b095d3ebf..3cbf85ca1 100644 --- a/packages/stack-encrypt/src/plan/build.rs +++ b/packages/stack-encrypt/src/plan/build.rs @@ -727,7 +727,7 @@ impl FieldsBuilder { /// `/`. Lowers to [`ciphertext`]`().under(label)`. pub fn encrypt(self, field: impl FieldRef) -> Self where - F: crate::Plaintext + crate::Decrypt + Clone + Send + 'static, + F: crate::Encrypt + crate::Decrypt + Clone + Send + 'static, { let (name, access) = resolve(field); let read = Arc::clone(&access.read); @@ -764,14 +764,14 @@ impl FieldsBuilder { indexes: impl Indexes + Clone + Send + Sync + 'static, ) -> Self where - F: crate::Plaintext + crate::Decrypt + Clone + Send + 'static, + F: crate::Encrypt + crate::Decrypt + Clone + Send + 'static, { self.encrypt_index_of(field, indexes) } fn encrypt_index_of(self, field: impl FieldRef, indexes: X) -> Self where - F: crate::Plaintext + crate::Decrypt + Clone + Send + 'static, + F: crate::Encrypt + crate::Decrypt + Clone + Send + 'static, X: Indexes + Clone + Send + Sync + 'static, X::Terms: Send, { diff --git a/packages/stack-encrypt/src/plan/chain.rs b/packages/stack-encrypt/src/plan/chain.rs index 8a197668e..a6842d175 100644 --- a/packages/stack-encrypt/src/plan/chain.rs +++ b/packages/stack-encrypt/src/plan/chain.rs @@ -406,7 +406,7 @@ impl<'a, S: ?Sized, K: 'static> EncryptWithContext<'a, S, K> { impl<'a, S, K: 'static> Operation<'a, K> for EncryptWithContext<'a, S, K> where - S: crate::Plaintext + Clone, + S: crate::Encrypt + Clone, { type Output = StackCipherText; fn cipher(&self) -> &'a StackCipher { @@ -445,7 +445,7 @@ pub struct EncryptIndexed<'a, S, X, K> { impl<'a, S, X, K: 'static> Operation<'a, K> for EncryptIndexed<'a, S, X, K> where - S: crate::Plaintext + Clone, + S: crate::Encrypt + Clone, X: Indexes + Clone, X::Terms: MaybeSend, { @@ -483,7 +483,7 @@ impl<'a, S: 'static, K: 'static> EncryptFields<'a, S, K> { /// [`FieldsBuilder::encrypt`]. pub fn encrypt(mut self, field: impl FieldRef) -> Self where - F: crate::Plaintext + crate::Decrypt + Clone + Send + 'static, + F: crate::Encrypt + crate::Decrypt + Clone + Send + 'static, { self.plan = self.plan.encrypt::(field); self @@ -495,7 +495,7 @@ impl<'a, S: 'static, K: 'static> EncryptFields<'a, S, K> { indexes: impl Indexes + Clone + Send + Sync + 'static, ) -> Self where - F: crate::Plaintext + crate::Decrypt + Clone + Send + 'static, + F: crate::Encrypt + crate::Decrypt + Clone + Send + 'static, { self.plan = self.plan.encrypt_index::(field, indexes); self diff --git a/packages/stack-encrypt/src/plan/value.rs b/packages/stack-encrypt/src/plan/value.rs index 03d6e024b..13b0f63fc 100644 --- a/packages/stack-encrypt/src/plan/value.rs +++ b/packages/stack-encrypt/src/plan/value.rs @@ -356,7 +356,7 @@ pub trait ValueShape: ValueLayout { impl ValueLayout for Indexed where - S: crate::Plaintext + Clone, + S: crate::Encrypt + Clone, X: Indexes + Clone, { type Output = Encrypted; @@ -367,7 +367,7 @@ where impl ValueShape for Indexed where - S: crate::Plaintext + Clone, + S: crate::Encrypt + Clone, X: Indexes + Clone, { fn lower<'s, K: 'static>(&self) -> Encryption<'s, S, Self::Output, K, CallerContext> diff --git a/packages/stack-encrypt/src/target/core.rs b/packages/stack-encrypt/src/target/core.rs index 63c3c3921..c06a84f03 100644 --- a/packages/stack-encrypt/src/target/core.rs +++ b/packages/stack-encrypt/src/target/core.rs @@ -2,7 +2,7 @@ use super::{CipherScope, Pending, Request, Responses}; use crate::cipher::{bind_keys, open_keyed, KeyedCipherText, PendingStackCipherText}; use crate::{Descriptor, Error, KeysetCipher, StackCipher, StackCipherText}; -use vitaminc_aead::{CipherText, Decrypt, IntoAad, IntoContext, Plaintext}; +use vitaminc_aead::{CipherText, Decrypt, Encrypt, IntoAad, IntoContext}; use vitaminc_protected::NonEmpty; /// Internal term operation. It is deliberately inaccessible to target authors. @@ -21,12 +21,12 @@ pub(crate) trait Term: Sized { } /// Seal `source` into the native tree. It is consumed, as Vitamin C's -/// by-value [`Plaintext`] walk consumes it: a caller holding only a borrow +/// by-value [`Encrypt`] walk consumes it: a caller holding only a borrow /// clones before it gets here (see [`ConsumeSource`](super::ConsumeSource)), /// and one holding the value hands it over without a copy. The walk happens /// here, before any request exists, so the pending holds only the planned /// leaves. -pub(crate) fn encrypt_native<'a, 'c, S: Plaintext, K, T: IntoContext<'c>>( +pub(crate) fn encrypt_native<'a, 'c, S: Encrypt, K, T: IntoContext<'c>>( source: S, cipher: &'a KeysetCipher<'_, K>, context: NonEmpty, diff --git a/packages/stack-encrypt/src/target/index.rs b/packages/stack-encrypt/src/target/index.rs index 63db8d400..d44d950c0 100644 --- a/packages/stack-encrypt/src/target/index.rs +++ b/packages/stack-encrypt/src/target/index.rs @@ -507,7 +507,7 @@ impl DecryptInto

for Encrypted EncryptFrom for Encrypted where - S: crate::Plaintext + Clone, + S: crate::Encrypt + Clone, Terms: TermSet, { type Context = CallerContext; @@ -621,7 +621,7 @@ pub fn indexed<'s, S, K, M, X>( indexes: X, ) -> Encryption<'s, S, Encrypted, K, CallerContext, M> where - S: crate::Plaintext + 's, + S: crate::Encrypt + 's, K: 'static, M: ConsumeSource<'s, S> + ShareSource<'s, S>, X: Indexes, diff --git a/packages/stack-encrypt/src/target/operations.rs b/packages/stack-encrypt/src/target/operations.rs index eae1d175c..688a67dbc 100644 --- a/packages/stack-encrypt/src/target/operations.rs +++ b/packages/stack-encrypt/src/target/operations.rs @@ -449,7 +449,7 @@ where /// `Encrypt` consumes the plaintext. In [`Owned`](super::Owned) mode it is /// handed over, so `S` need not be `Clone`; in the default [`Borrowed`] mode /// it is cloned once, which is what `M: ConsumeSource<'s, S>` asks. -pub fn ciphertext<'s, S: crate::Plaintext + 's, K: 'static, M: ConsumeSource<'s, S>>( +pub fn ciphertext<'s, S: crate::Encrypt + 's, K: 'static, M: ConsumeSource<'s, S>>( ) -> Encryption<'s, S, StackCipherText, K, AeadContext, M> { Encryption { build: Box::new( @@ -878,7 +878,7 @@ impl DecryptFrom for T {} // `Borrowed: ConsumeSource<'s, S>`, spelled out. The constructors themselves // (`ciphertext`, `equality`, `ore`, `ope`) ask only for the scheme's own // capability, and run a non-`Clone` plaintext in `Owned` mode. -impl EncryptFrom for StackCipherText { +impl EncryptFrom for StackCipherText { type Context = AeadContext; fn encryption<'s, K: 'static>() -> Encryption<'s, S, Self, K, Self::Context> where diff --git a/packages/stack-encrypt/tests/common/mod.rs b/packages/stack-encrypt/tests/common/mod.rs index b41b75ba9..472aa44ce 100644 --- a/packages/stack-encrypt/tests/common/mod.rs +++ b/packages/stack-encrypt/tests/common/mod.rs @@ -235,7 +235,7 @@ pub async fn deterministic_cipher(seed: [u8; 32]) -> StackCipher(value: P) -> stack_encrypt::PassthroughFrame { +pub fn passthrough(value: P) -> stack_encrypt::PassthroughFrame { use vitaminc_protected::Controlled; stack_encrypt::PassthroughFrame( vitaminc_aead::to_frame(value) @@ -248,5 +248,5 @@ pub fn passthrough(value: P) -> stack_encrypt::Pass pub fn passthrough_u32(payload: &stack_encrypt::PassthroughFrame) -> u32 { use vitaminc_aead::Decrypt; let frame = payload.parse().expect("one frame"); - vitaminc_aead::Plaintext::visit(frame, u32::sink()).expect("a u32") + vitaminc_aead::Encrypt::visit(frame, u32::sink()).expect("a u32") } diff --git a/packages/stack-encrypt/tests/index.rs b/packages/stack-encrypt/tests/index.rs index d5a89ec5b..34a079153 100644 --- a/packages/stack-encrypt/tests/index.rs +++ b/packages/stack-encrypt/tests/index.rs @@ -622,7 +622,7 @@ mod given_encrypted_terms_as_a_target { /// opens through the other's reader. async fn same_bytes(value: S, indexes: X) where - S: stack_encrypt::Plaintext + S: stack_encrypt::Encrypt + stack_encrypt::Decrypt + Send + Clone diff --git a/packages/stack-encrypt/tests/plan_grammar.rs b/packages/stack-encrypt/tests/plan_grammar.rs index 67d76cdd9..3e2a77fcc 100644 --- a/packages/stack-encrypt/tests/plan_grammar.rs +++ b/packages/stack-encrypt/tests/plan_grammar.rs @@ -22,7 +22,7 @@ use stack_encrypt::target::{ EncryptFrom, Encrypted, Encryption, IndexSpec, Indexes, TermSet, }; use stack_encrypt::{ - nonempty, Decrypt, Equality, Error, ErrorPayload, Label, Match, NonEmpty, Ope, Ore, Plaintext, + nonempty, Decrypt, Encrypt, Equality, Error, ErrorPayload, Label, Match, NonEmpty, Ope, Ore, Plan, StackCipherText, }; @@ -960,7 +960,7 @@ struct One { /// other plan. async fn same_field_bytes(value: F, indexes: X) where - F: Plaintext + Decrypt + Clone + Send + Sync + PartialEq + Debug + 'static, + F: Encrypt + Decrypt + Clone + Send + Sync + PartialEq + Debug + 'static, Terms: TermSet + PartialEq + Debug + Send, X: Indexes + Clone + Send + Sync + 'static, { diff --git a/packages/stack-encrypt/tests/roundtrip.rs b/packages/stack-encrypt/tests/roundtrip.rs index 96b115d01..def78a5a3 100644 --- a/packages/stack-encrypt/tests/roundtrip.rs +++ b/packages/stack-encrypt/tests/roundtrip.rs @@ -6,9 +6,7 @@ use std::collections::HashMap; -use stack_encrypt::{ - CipherText, ContextTag, PassthroughFrame, Plaintext, SealedValue, StackCipher, -}; +use stack_encrypt::{CipherText, ContextTag, Encrypt, PassthroughFrame, SealedValue, StackCipher}; use stack_kms::FakeDataKeySource; use vitaminc_aead::plaintext::{MapSink, Sink}; use vitaminc_aead::Decrypt; @@ -244,7 +242,7 @@ impl Row { } } -impl Plaintext for Row { +impl Encrypt for Row { fn visit(self, sink: S) -> Result { let mut map = sink.map(None)?; map.entry("id")?.passthrough(self.id)?; diff --git a/packages/stack-encrypt/tests/source_mode.rs b/packages/stack-encrypt/tests/source_mode.rs index 6780d5c67..023bfd6cf 100644 --- a/packages/stack-encrypt/tests/source_mode.rs +++ b/packages/stack-encrypt/tests/source_mode.rs @@ -21,7 +21,7 @@ use stack_encrypt::sem::{EqualityTerm, MatchTerms}; use stack_encrypt::target::{ ciphertext, equality, matching, AeadContext, Borrowed, CallerContext, Encryption, Owned, }; -use stack_encrypt::{nonempty, NonEmpty, Plaintext, StackCipherText}; +use stack_encrypt::{nonempty, Encrypt, NonEmpty, StackCipherText}; use stack_kms::FakeDataKeySource; use vitaminc_aead::plaintext::Sink; use vitaminc_prf::{IntoPrfContext, Prf, PrfValue, PrfVisitor}; @@ -52,7 +52,7 @@ impl Secret { Self(Protected::new(text.to_string())) } } -impl Plaintext for Secret { +impl Encrypt for Secret { fn visit(self, sink: S) -> Result { self.0.visit(sink) } @@ -99,7 +99,7 @@ impl Clone for Counted { } } } -impl Plaintext for Counted { +impl Encrypt for Counted { fn visit(self, sink: S) -> Result { self.text.visit(sink) } @@ -229,7 +229,7 @@ async fn one_owned_operation_makes_no_copy() { fn sealed_and_indexed<'s, S, M>( ) -> Encryption<'s, S, (StackCipherText, EqualityTerm), FakeDataKeySource, CallerContext, M> where - S: Plaintext + PrfValue + 's, + S: Encrypt + PrfValue + 's, M: stack_encrypt::target::ConsumeSource<'s, S> + stack_encrypt::target::ShareSource<'s, S>, { ciphertext().accepting().zip(equality()) @@ -342,7 +342,7 @@ impl Drop for Dropped { let _ = self.drops.fetch_add(1, Ordering::SeqCst); } } -impl Plaintext for Dropped { +impl Encrypt for Dropped { fn visit(mut self, sink: S) -> Result { std::mem::take(&mut self.text).visit(sink) } diff --git a/packages/stack-encrypt/tests/transcode.rs b/packages/stack-encrypt/tests/transcode.rs index 8f9033f90..bb69b6ec2 100644 --- a/packages/stack-encrypt/tests/transcode.rs +++ b/packages/stack-encrypt/tests/transcode.rs @@ -6,7 +6,7 @@ use stack_encrypt::target::transcode::{MapReader, Reader, SequenceReader, Transc use stack_encrypt::target::{self, CallerContext, ExpectedContext}; use stack_encrypt::{ nonempty, CipherText, ContextPiece, DecryptField, DecryptInto, Decryptable, Decryption, - EncryptFrom, Encryption, Error, IntoContext, MaybeEmpty, NonEmpty, PassthroughFrame, Plaintext, + Encrypt, EncryptFrom, Encryption, Error, IntoContext, MaybeEmpty, NonEmpty, PassthroughFrame, SealedValue, StackCipherText, }; use std::sync::atomic::Ordering; @@ -51,7 +51,7 @@ impl Transcode for StoredLeaf { LeafVisitor } } -impl EncryptFrom for StoredLeaf { +impl EncryptFrom for StoredLeaf { type Context = CallerContext; fn encryption<'s, K: 'static>() -> Encryption<'s, S, Self, K, Self::Context> where @@ -223,12 +223,12 @@ async fn empty_stored_identifier_is_refused_before_any_key_is_retrieved() { #[derive(Clone)] struct WithoutSerde(String); -impl Plaintext for WithoutSerde { +impl Encrypt for WithoutSerde { fn visit(self, sink: S) -> Result { self.0.visit(sink) } } -impl Plaintext for &WithoutSerde { +impl Encrypt for &WithoutSerde { fn visit(self, sink: S) -> Result { (&self.0).visit(sink) } @@ -285,7 +285,7 @@ async fn scalar_destination_refuses_a_sequence() { // A target whose declaration carries every context it needs asks its caller // for none: `Context = ()`. struct FixedLeaf(StoredLeaf); -impl EncryptFrom for FixedLeaf { +impl EncryptFrom for FixedLeaf { type Context = (); fn encryption<'s, K: 'static>() -> Encryption<'s, S, Self, K, Self::Context> where diff --git a/packages/stack-encrypt/tests/ui/owned_fan_out_needs_clone.rs b/packages/stack-encrypt/tests/ui/owned_fan_out_needs_clone.rs index 2573c44ca..e567cfd8d 100644 --- a/packages/stack-encrypt/tests/ui/owned_fan_out_needs_clone.rs +++ b/packages/stack-encrypt/tests/ui/owned_fan_out_needs_clone.rs @@ -3,10 +3,10 @@ //! one place a copy is unavoidable — the first side gets a clone, the last //! the value — and the refusal names `zip`, not the operations. use stack_encrypt::target::{ciphertext, equality, CallerContext, Owned}; -use stack_encrypt::{nonempty, KeysetCipher, Plaintext}; +use stack_encrypt::{nonempty, KeysetCipher, Encrypt}; use stack_kms::FakeDataKeySource; -fn seal_and_index( +fn seal_and_index( keyset: &KeysetCipher<'_, FakeDataKeySource>, value: S, ) { diff --git a/packages/stack-encrypt/tests/ui/owned_fan_out_needs_clone.stderr b/packages/stack-encrypt/tests/ui/owned_fan_out_needs_clone.stderr index ca42a4fa4..aca006b38 100644 --- a/packages/stack-encrypt/tests/ui/owned_fan_out_needs_clone.stderr +++ b/packages/stack-encrypt/tests/ui/owned_fan_out_needs_clone.stderr @@ -15,5 +15,5 @@ note: required by a bound in `Encryption::<'s, S, T, K, Ctx, M>::zip` | ^^^^^^^^^^^^^^^^^^ required by this bound in `Encryption::<'s, S, T, K, Ctx, M>::zip` help: consider further restricting type parameter `S` with trait `Clone` | - 9 | fn seal_and_index( - | +++++++++++++++++++ + 9 | fn seal_and_index( + | +++++++++++++++++++ diff --git a/packages/stack-encrypt/tests/ui/pass/owned_without_clone.rs b/packages/stack-encrypt/tests/ui/pass/owned_without_clone.rs index f15dcfedd..b871b992f 100644 --- a/packages/stack-encrypt/tests/ui/pass/owned_without_clone.rs +++ b/packages/stack-encrypt/tests/ui/pass/owned_without_clone.rs @@ -5,10 +5,10 @@ use stack_encrypt::sem::{CllwOpeEncrypt, CllwOreEncrypt, EqualityTerm, MatchTerm use stack_encrypt::target::{ ciphertext, equality, matching, ope, ore, CallerContext, Owned, Pending, }; -use stack_encrypt::{nonempty, KeysetCipher, Plaintext, StackCipherText}; +use stack_encrypt::{nonempty, KeysetCipher, Encrypt, StackCipherText}; use stack_kms::FakeDataKeySource; -fn seal<'a, S: Plaintext>( +fn seal<'a, S: Encrypt>( keyset: &'a KeysetCipher<'_, FakeDataKeySource>, value: S, ) -> Pending<'a, StackCipherText, FakeDataKeySource> { diff --git a/packages/stack-encrypt/tests/ui/plaintext_needs_encrypt.stderr b/packages/stack-encrypt/tests/ui/plaintext_needs_encrypt.stderr index 749218206..d8e66cd28 100644 --- a/packages/stack-encrypt/tests/ui/plaintext_needs_encrypt.stderr +++ b/packages/stack-encrypt/tests/ui/plaintext_needs_encrypt.stderr @@ -1,4 +1,4 @@ -error[E0277]: the trait bound `SerdeOnly: stack_encrypt::Plaintext` is not satisfied +error[E0277]: the trait bound `SerdeOnly: stack_encrypt::Encrypt` is not satisfied --> tests/ui/plaintext_needs_encrypt.rs:9:33 | 9 | let _=keyset.encrypt_as::<_,Target>(&SerdeOnly {value:"x".into()},nonempty!("column").into()); @@ -6,12 +6,12 @@ error[E0277]: the trait bound `SerdeOnly: stack_encrypt::Plaintext` is not satis | | | required by a bound introduced by this call | -help: the trait `stack_encrypt::Plaintext` is not implemented for `SerdeOnly` +help: the trait `stack_encrypt::Encrypt` is not implemented for `SerdeOnly` --> tests/ui/plaintext_needs_encrypt.rs:4:1 | 4 | struct SerdeOnly { value: String } | ^^^^^^^^^^^^^^^^ - = help: the following other types implement trait `stack_encrypt::Plaintext`: + = help: the following other types implement trait `stack_encrypt::Encrypt`: &&'b T &'a BTreeMap &'a HashMap diff --git a/packages/stack-encrypt/tests/ui/struct_field_must_open.rs b/packages/stack-encrypt/tests/ui/struct_field_must_open.rs index cf829679f..dfa8c9837 100644 --- a/packages/stack-encrypt/tests/ui/struct_field_must_open.rs +++ b/packages/stack-encrypt/tests/ui/struct_field_must_open.rs @@ -5,18 +5,18 @@ //! borrows its text, cannot be opened into, and a target with no //! `DecryptField` cannot open. use stack_encrypt::target::{Decryptable, Encryption, AeadContext}; -use stack_encrypt::{EncryptFrom, Plaintext, StackCipherText}; +use stack_encrypt::{EncryptFrom, Encrypt, StackCipherText}; use vitaminc_aead::plaintext::Sink; /// Text it borrows: it seals, and there is nothing to open it into. #[derive(Clone)] struct Name(&'static str); -impl Plaintext for Name { +impl Encrypt for Name { fn visit(self, sink: S) -> Result { self.0.visit(sink) } } -impl Plaintext for &Name { +impl Encrypt for &Name { fn visit(self, sink: S) -> Result { self.0.visit(sink) } diff --git a/packages/stack-guest-abi/src/value.rs b/packages/stack-guest-abi/src/value.rs index 013013566..750eed81a 100644 --- a/packages/stack-guest-abi/src/value.rs +++ b/packages/stack-guest-abi/src/value.rs @@ -5,12 +5,12 @@ //! fields of a token, the last error) crosses as one transport frame, the //! encoding vitaminc's `Frame` and `FrameWriter` define and the Go module //! reads with `vcffi`. A `HostValue` describes itself to vitaminc's -//! [`Plaintext`] model, so [`encode`](HostValue::encode) is vitaminc's own +//! [`Encrypt`] model, so [`encode`](HostValue::encode) is vitaminc's own //! frame writer, and [`decode`](HostValue::decode) reads a frame back //! through a [`Sink`], for the tests that check what a host is given. use vitaminc_aead::plaintext::{ - visit_map, visit_seq, Float, Logical, MapSink, Physical, Plaintext, SeqSink, Sink, SourceError, + visit_map, visit_seq, Encrypt, Float, Logical, MapSink, Physical, SeqSink, Sink, SourceError, }; use vitaminc_aead_frame::{to_frame, Frame, FrameError, FrameWriteError}; use vitaminc_protected::{Controlled, Protected}; @@ -140,7 +140,7 @@ impl vitaminc_aead::plaintext::Error for DecodeError { } /// Walks the value, leaving it where it is. -impl Plaintext for &HostValue { +impl Encrypt for &HostValue { fn visit(self, sink: S) -> Result { match self { HostValue::Null => sink.scalar(None, Physical::Null), @@ -238,7 +238,7 @@ impl Sink for ValueSink { } /// A guest never hands its host a passthrough. - fn passthrough(self, _: P) -> Result { + fn passthrough(self, _: P) -> Result { Err(DecodeError::Scalar) } } From 1bb42ab35b217b40fbf458c37a13bc94fc58742e Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Sat, 17 Oct 2026 00:24:40 +1100 Subject: [PATCH 07/15] fix(stack-guest-abi): wipe text a guest hands back, and pin the Go module to the same vitaminc Review follow-ups for the move onto vitaminc ADR 0001. `HostValue::Text` held a plain `String`. Before this PR the auth guest built its output from `FfiValue::String`, whose bytes live in a `Protected` buffer, so the access token it returns was wiped when dropped; after it, every `token` call left a bearer token in freed guest memory. `Text` now holds `Zeroizing` on every path that builds one (the constructor, JSON, a decoded frame), and a test pins it. The Go module still named vcffi and vcvalue at 2e895cb9caed, the head of the deleted `feat/aead-remove-legacy-traits` branch, which builds only while the Go proxy has it cached. It now names the commit the five Rust workspaces pin, 2413bd0e4bf7 (cipherstash/vitaminc#464); the Go code under bindings/go is the same at both. Docs that still called vitaminc's source trait `Plaintext` (CONTEXT.md, the mixed_user example, two test headers and two manifests) say `Encrypt`. The CHANGELOG no longer says a reader of any other type is refused: an integer reader takes any integer width the value fits, and an `f64` reader an `f32`, which change no value. --- languages/golang/auth/guest/src/ops.rs | 2 +- languages/golang/auth/guest/tests/leak.rs | 2 +- languages/golang/go.mod | 4 +- languages/golang/go.sum | 12 ++---- packages/eql/crates/eql-bindings/Cargo.toml | 2 +- packages/eql/tests/encryption/Cargo.toml | 2 +- packages/stack-encrypt/CHANGELOG.md | 10 +++-- packages/stack-encrypt/CONTEXT.md | 2 +- packages/stack-encrypt/examples/mixed_user.rs | 2 +- packages/stack-encrypt/tests/source_mode.rs | 2 +- packages/stack-guest-abi/src/value.rs | 38 +++++++++++++++---- 11 files changed, 51 insertions(+), 27 deletions(-) diff --git a/languages/golang/auth/guest/src/ops.rs b/languages/golang/auth/guest/src/ops.rs index ab8bb36ed..19e80580a 100644 --- a/languages/golang/auth/guest/src/ops.rs +++ b/languages/golang/auth/guest/src/ops.rs @@ -254,7 +254,7 @@ mod tests { let HostValue::Text(s) = value else { panic!("not a string"); }; - s.clone() + s.to_string() } fn as_bytes(value: &HostValue) -> Vec { diff --git a/languages/golang/auth/guest/tests/leak.rs b/languages/golang/auth/guest/tests/leak.rs index 2b9f23082..ae0083d80 100644 --- a/languages/golang/auth/guest/tests/leak.rs +++ b/languages/golang/auth/guest/tests/leak.rs @@ -102,7 +102,7 @@ fn get<'a>(value: &'a HostValue, key: &str) -> Option<&'a HostValue> { fn text(value: Option<&HostValue>) -> Option { match value? { - HostValue::Text(s) => Some(s.clone()), + HostValue::Text(s) => Some(s.to_string()), _ => None, } } diff --git a/languages/golang/go.mod b/languages/golang/go.mod index 52569fa25..c865c7b02 100644 --- a/languages/golang/go.mod +++ b/languages/golang/go.mod @@ -3,8 +3,8 @@ module github.com/cipherstash/stack/languages/golang go 1.26.0 require ( - github.com/cipherstash/vitaminc/bindings/go/vcffi v0.0.0-20261015123909-2e895cb9caed - github.com/cipherstash/vitaminc/bindings/go/vcvalue v0.0.0-20261015123909-2e895cb9caed + github.com/cipherstash/vitaminc/bindings/go/vcffi v0.0.0-20261016121549-2413bd0e4bf7 + github.com/cipherstash/vitaminc/bindings/go/vcvalue v0.0.0-20261016121549-2413bd0e4bf7 github.com/tetratelabs/wazero v1.12.0 golang.org/x/oauth2 v0.37.0 ) diff --git a/languages/golang/go.sum b/languages/golang/go.sum index e8211d9d1..ca8fa712d 100644 --- a/languages/golang/go.sum +++ b/languages/golang/go.sum @@ -1,11 +1,7 @@ -github.com/cipherstash/vitaminc/bindings/go/vcffi v0.0.0-20260902024806-f2c7f7d17fbc h1:iGbTgv9Kx5SaTI2tPxaxwDvS5BXZUO565N00cxRVwC8= -github.com/cipherstash/vitaminc/bindings/go/vcffi v0.0.0-20260902024806-f2c7f7d17fbc/go.mod h1:6jpaqAo6f7rjWuxoti0Ymoi9DQZyxpnieWVudhXv38Y= -github.com/cipherstash/vitaminc/bindings/go/vcffi v0.0.0-20261015123909-2e895cb9caed h1:supKop+UADso55MvBLebaBjP2vyilWmbAxOtFZJOnQ8= -github.com/cipherstash/vitaminc/bindings/go/vcffi v0.0.0-20261015123909-2e895cb9caed/go.mod h1:6jpaqAo6f7rjWuxoti0Ymoi9DQZyxpnieWVudhXv38Y= -github.com/cipherstash/vitaminc/bindings/go/vcvalue v0.0.0-20260902024806-f2c7f7d17fbc h1:vlrjoILAURGfpBWucVZEywK22k4lfky1xGDIKV6kqNg= -github.com/cipherstash/vitaminc/bindings/go/vcvalue v0.0.0-20260902024806-f2c7f7d17fbc/go.mod h1:RJODA1DCSm4H+1pbmCzDdT2pAIkxhinwIu6ewjh5sPs= -github.com/cipherstash/vitaminc/bindings/go/vcvalue v0.0.0-20261015123909-2e895cb9caed h1:do5JX9ovxiHzLc9G4wcybbr4f3xaVAYeIKFnOJk9NKw= -github.com/cipherstash/vitaminc/bindings/go/vcvalue v0.0.0-20261015123909-2e895cb9caed/go.mod h1:RJODA1DCSm4H+1pbmCzDdT2pAIkxhinwIu6ewjh5sPs= +github.com/cipherstash/vitaminc/bindings/go/vcffi v0.0.0-20261016121549-2413bd0e4bf7 h1:yw1oKlra5txMgE1u2LPiFBfa68IaEeSgLkLyDm6POqY= +github.com/cipherstash/vitaminc/bindings/go/vcffi v0.0.0-20261016121549-2413bd0e4bf7/go.mod h1:6jpaqAo6f7rjWuxoti0Ymoi9DQZyxpnieWVudhXv38Y= +github.com/cipherstash/vitaminc/bindings/go/vcvalue v0.0.0-20261016121549-2413bd0e4bf7 h1:e1YWsXn1jPEt+mfELQo9loKvM7yeSicPw1ZmBuQc7JQ= +github.com/cipherstash/vitaminc/bindings/go/vcvalue v0.0.0-20261016121549-2413bd0e4bf7/go.mod h1:RJODA1DCSm4H+1pbmCzDdT2pAIkxhinwIu6ewjh5sPs= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU= diff --git a/packages/eql/crates/eql-bindings/Cargo.toml b/packages/eql/crates/eql-bindings/Cargo.toml index df16578d2..62b400896 100644 --- a/packages/eql/crates/eql-bindings/Cargo.toml +++ b/packages/eql/crates/eql-bindings/Cargo.toml @@ -53,7 +53,7 @@ stack-encrypt = { path = "../../../stack-encrypt", version = "0.2.0", optional = # the requirement is stack-encrypt's. vitaminc-prf = { version = "0.5.0", optional = true } # `Physical`, `Int`, `Float` and `FromScalar`: the scalar plaintexts in -# `encryption::plaintext` describe themselves through vitaminc's `Plaintext` +# `encryption::plaintext` describe themselves through vitaminc's `Encrypt` # and read back only their own width through `FromScalar`. vitaminc-aead = { version = "0.5.1", optional = true } # The plaintext a target reads out of its `Protected` runtime value is held diff --git a/packages/eql/tests/encryption/Cargo.toml b/packages/eql/tests/encryption/Cargo.toml index 41f04494b..2ce463745 100644 --- a/packages/eql/tests/encryption/Cargo.toml +++ b/packages/eql/tests/encryption/Cargo.toml @@ -20,7 +20,7 @@ eql-domains = { path = "../../crates/eql-domains" } # `dynamic`: `FfiValue`, the runtime plaintext the target dispatch takes, # as eql-bindings names it. stack-encrypt = { path = "../../../stack-encrypt", default-features = false, features = ["dynamic"] } -# `plaintext::Sink`, for a plaintext type with its own `Plaintext` impl. +# `plaintext::Sink`, for a plaintext type with its own `Encrypt` impl. vitaminc-aead = "0.5.1" stack-kms = { path = "../../../stack-kms", default-features = false, features = ["test-support"] } serde = "1" diff --git a/packages/stack-encrypt/CHANGELOG.md b/packages/stack-encrypt/CHANGELOG.md index 68c9daa3c..69ab15c14 100644 --- a/packages/stack-encrypt/CHANGELOG.md +++ b/packages/stack-encrypt/CHANGELOG.md @@ -151,7 +151,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 (`stack_encrypt::{Encrypt, Decrypt}` re-exports them). The name is the same and the trait is not: a hand-written impl of the old one does not compile against the new one. `Decipher`, `Element` and `Cipher` are no - longer re-exported, and `StackDecipher` and `BoxedPassthrough` are gone. A pending tree is planned rather than driven: + longer re-exported, and `StackDecipher` and `BoxedPassthrough` are gone. + A pending tree is planned rather than driven: `PendingStackCipherText::plan(value, aad)`, then `seal` or `into_pending` as before. The key fetch is unchanged: one batched `generate_keys` per assembly to seal, one `retrieve_keys` per keyset to @@ -159,8 +160,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Every leaf records its type, so a Rust field and a data-plan field share leaves.** A Rust `u32` and a data plan's `uint32` field seal the same leaf under the same context, and each opens the other's; a reader of - another type is refused rather than handed changed plaintext, as a bare - `String` reader used to read a binding's leaf as `"\nalice"` (#1118). + another kind (text for an integer, an integer for bytes) is refused rather + than handed changed plaintext, as a bare `String` reader used to read a + binding's leaf as `"\nalice"` (#1118). An integer reader accepts an integer + leaf of any width whose value fits, and an `f64` reader an `f32` leaf, + which change no value. Leaves are vitaminc's wire version 2, so a ciphertext sealed by an earlier build does not open. - **An absent value is a sealed null leaf**, a `Single` like any other diff --git a/packages/stack-encrypt/CONTEXT.md b/packages/stack-encrypt/CONTEXT.md index c59e3e5b2..ee3130de6 100644 --- a/packages/stack-encrypt/CONTEXT.md +++ b/packages/stack-encrypt/CONTEXT.md @@ -9,7 +9,7 @@ derivation of searchable index terms from the same values. Covers **Cipher-directed**: Encryption driven by the value's shape: the value describes itself once -(vitaminc's `Plaintext`), vitaminc walks it into sealed leaves, and the +(vitaminc's `Encrypt`), vitaminc walks it into sealed leaves, and the caller decides the context, which may be absent. Nothing declares an index and no output type is involved. One of the two preferred entry points for a native Rust caller. A language SDK may leave it diff --git a/packages/stack-encrypt/examples/mixed_user.rs b/packages/stack-encrypt/examples/mixed_user.rs index b2257026d..48e3e43ba 100644 --- a/packages/stack-encrypt/examples/mixed_user.rs +++ b/packages/stack-encrypt/examples/mixed_user.rs @@ -17,7 +17,7 @@ //! Passthrough values travel in the clear and are **not authenticated** — //! use them for non-sensitive routing/display data only. //! -//! This is the *cipher-directed* layer — vitaminc's `Plaintext` / `Decrypt` +//! This is the *cipher-directed* layer — vitaminc's `Encrypt` / `Decrypt` //! derives, sealed through the keyset directly — one level below //! `#[derive(EncryptFrom)]`, which the //! `encrypted_record` example uses. Reach for this layer when a value needs diff --git a/packages/stack-encrypt/tests/source_mode.rs b/packages/stack-encrypt/tests/source_mode.rs index 023bfd6cf..2988dcfc7 100644 --- a/packages/stack-encrypt/tests/source_mode.rs +++ b/packages/stack-encrypt/tests/source_mode.rs @@ -1,6 +1,6 @@ //! How a description receives its plaintext (`target::{Borrowed, Owned}`). //! -//! Vitamin C's by-value `Plaintext` walk and `PrfValue` consume their input. A description +//! Vitamin C's by-value `Encrypt` walk and `PrfValue` consume their input. A description //! handed a borrow (the default, and what every `EncryptFrom` declaration //! runs in) clones before an operation consumes; one handed the value //! (`Owned`) gives it to the operation. These tests hold the two claims that diff --git a/packages/stack-guest-abi/src/value.rs b/packages/stack-guest-abi/src/value.rs index 750eed81a..9b7557ed2 100644 --- a/packages/stack-guest-abi/src/value.rs +++ b/packages/stack-guest-abi/src/value.rs @@ -14,6 +14,7 @@ use vitaminc_aead::plaintext::{ }; use vitaminc_aead_frame::{to_frame, Frame, FrameError, FrameWriteError}; use vitaminc_protected::{Controlled, Protected}; +use zeroize::Zeroizing; /// A value a guest hands its host: a scalar, a list or a map with string /// keys. @@ -32,8 +33,9 @@ pub enum HostValue { Int(i64), /// A float. Float(f64), - /// Text. - Text(String), + /// Text, wiped when dropped: a guest hands secrets back as text (an + /// access token), and the guest heap is not cleared when it frees. + Text(Zeroizing), /// Bytes. Bytes(Protected>), /// A list. @@ -45,7 +47,7 @@ pub enum HostValue { impl HostValue { /// Text, from anything that converts to a `String`. pub fn text(value: impl Into) -> Self { - Self::Text(value.into()) + Self::Text(Zeroizing::new(value.into())) } /// A structured field as a host value: a JSON number becomes an @@ -63,7 +65,7 @@ impl HostValue { Self::Float(number.as_f64().unwrap_or(f64::NAN)) } } - serde_json::Value::String(value) => Self::Text(value), + serde_json::Value::String(value) => Self::Text(Zeroizing::new(value)), serde_json::Value::Array(items) => { Self::List(items.into_iter().map(Self::from_json).collect()) } @@ -113,7 +115,7 @@ impl HostValue { /// The text, if this is text. pub fn as_text(&self) -> Option<&str> { match self { - Self::Text(text) => Some(text), + Self::Text(text) => Some(text.as_str()), _ => None, } } @@ -148,7 +150,7 @@ impl Encrypt for &HostValue { HostValue::UInt(u) => sink.scalar(None, Physical::Int((*u).into())), HostValue::Int(i) => sink.scalar(None, Physical::Int((*i).into())), HostValue::Float(f) => sink.scalar(None, Physical::Float(Float::F64(*f))), - HostValue::Text(text) => sink.scalar(None, Physical::Text(text)), + HostValue::Text(text) => sink.scalar(None, Physical::Text(text.as_str())), HostValue::Bytes(bytes) => sink.scalar(None, Physical::Bytes(bytes.risky_ref())), HostValue::List(items) => visit_seq(sink, Some(items.len()), items), HostValue::Map(entries) => visit_map( @@ -216,7 +218,7 @@ impl Sink for ValueSink { } } (None, Physical::Float(Float::F64(f))) => HostValue::Float(f), - (None, Physical::Text(text)) => HostValue::Text(text.to_owned()), + (None, Physical::Text(text)) => HostValue::Text(Zeroizing::new(text.to_owned())), (None, Physical::Bytes(bytes)) => HostValue::Bytes(Protected::new(bytes.to_vec())), _ => return Err(DecodeError::Scalar), }; @@ -291,6 +293,28 @@ impl MapSink for ValueMap { mod tests { use super::*; + /// Text is held as `Zeroizing`, so a secret a guest hands back + /// as text (the auth guest's access token) is wiped when the value is + /// dropped, by every path that builds one: the constructor, JSON and a + /// decoded frame. A plain `String` here would leave it in freed guest + /// memory. + #[test] + fn text_is_wiped_on_drop() { + fn held(value: HostValue) -> Zeroizing { + match value { + HostValue::Text(text) => text, + _ => panic!("not text"), + } + } + assert_eq!(held(HostValue::text("token")).as_str(), "token"); + assert_eq!( + held(HostValue::from_json(serde_json::json!("token"))).as_str(), + "token" + ); + let frame = HostValue::text("token").encode().unwrap(); + assert_eq!(held(HostValue::decode(&frame).unwrap()).as_str(), "token"); + } + #[test] fn a_value_reads_back_as_it_was_written() { let value = HostValue::Map(vec![ From beb3074ec65c4188b0fbbf16e8c5f4921cfc9abd Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Sat, 17 Oct 2026 00:44:39 +1100 Subject: [PATCH 08/15] fix(guests): hand the host output buffers with no spare capacity, so registering one copies nothing unwiped Copilot review. The registry wipes the buffer it is handed, but `register` turns a `Vec` into a boxed slice, and a `Vec` with more capacity than length is reallocated on the way, leaving the old allocation unwiped. The encrypt guest reserved the exact encoded length before writing a frame, but vitaminc's frame writer reserves an upper bound per leaf (14 bytes for a six-byte string's 11), so a decrypt's plaintext output could still grow past it and leave a copy behind; the auth guest's `HostValue::encode` returned the writer's buffer as it was. Both now write into a buffer that is wiped when dropped and copy the frame into one whose capacity is its length. A test in each pins it over every value shape the guest writes. --- languages/golang/encrypt/guest/src/ops.rs | 37 ++++++++++++++++------- packages/stack-guest-abi/src/value.rs | 27 ++++++++++++++--- 2 files changed, 49 insertions(+), 15 deletions(-) diff --git a/languages/golang/encrypt/guest/src/ops.rs b/languages/golang/encrypt/guest/src/ops.rs index 2d85fbb5b..a20f36286 100644 --- a/languages/golang/encrypt/guest/src/ops.rs +++ b/languages/golang/encrypt/guest/src/ops.rs @@ -44,6 +44,7 @@ use stack_encrypt::{CipherText, Encrypt, KeysetCipher, Label, Node, SealedValue, use stack_kms::DataKeySource; use vitaminc_aead_frame::{decode_ciphertext, encode_ciphertext, FrameWriter}; use vitaminc_protected::Controlled; +use zeroize::Zeroizing; use stack_guest_abi::last_error; @@ -377,21 +378,23 @@ fn decode_value(bytes: &[u8]) -> Result { .map_err(|_| last_error::malformed("an input is not a value in the transport frame")) } -/// Encode a value tree into a buffer sized **before** the first byte is -/// written. +/// Encode a value tree into a buffer whose capacity is exactly its length. /// -/// This buffer is plaintext on the decrypt path, and a `Vec` grown by the -/// frame writer's pushes would leave partial plaintext in every abandoned -/// allocation a reallocation could not extend in place — memory nothing -/// wipes, undercutting the guarantee the registry makes about the buffer it -/// eventually hands the host. Reserving the exact encoded length up front -/// means the writer never reallocates, and `register`'s `into_boxed_slice` -/// (capacity == length) does not copy either. +/// This buffer is plaintext on the decrypt path, and the registry wipes only +/// the buffer it holds: `register`'s `into_boxed_slice` reallocates a `Vec` +/// whose capacity exceeds its length, leaving the old allocation unwiped. +/// The frame writer reserves an upper bound per leaf (a six-byte string +/// reserves 14 for 11), so writing straight into the exactly sized buffer +/// can still grow it. It writes into a `Zeroizing` staging buffer instead +/// (the writer wipes any allocation it replaces), which is copied into an +/// exactly sized output and then wiped. fn encode_value(value: FfiValue) -> Result, u32> { - let mut out = exact_buffer(value_encoded_len(&value))?; + let mut staging = Zeroizing::new(exact_buffer(value_encoded_len(&value))?); (&value) - .visit(FrameWriter::new(&mut out)) + .visit(FrameWriter::new(&mut staging)) .map_err(|_| STATUS_ENCODING)?; + let mut out = exact_buffer(Some(staging.len()))?; + out.extend_from_slice(&staging); Ok(out) } @@ -545,6 +548,18 @@ mod tests { ] } + /// The registry wipes the buffer it is handed, and `into_boxed_slice` + /// reallocates one whose capacity exceeds its length, leaving the old + /// allocation unwiped. So every encoded output's capacity is its length, + /// not only its length the frame's. + #[test] + fn an_encoded_value_has_no_spare_capacity() { + for (i, value) in every_value_shape().into_iter().enumerate() { + let out = encode_value(value).expect("encodes"); + assert_eq!(out.capacity(), out.len(), "shape {i}"); + } + } + #[test] fn value_encoded_len_matches_the_codec_exactly() { for (i, value) in every_value_shape().into_iter().enumerate() { diff --git a/packages/stack-guest-abi/src/value.rs b/packages/stack-guest-abi/src/value.rs index 9b7557ed2..984475589 100644 --- a/packages/stack-guest-abi/src/value.rs +++ b/packages/stack-guest-abi/src/value.rs @@ -80,9 +80,12 @@ impl HostValue { /// This value as a transport frame. /// - /// The frame is returned bare, as every guest output is: it is copied - /// into a registered buffer, which the registry wipes when the host - /// releases it. + /// The frame is returned bare, as every guest output is: it becomes a + /// registered buffer, which the registry wipes when the host releases + /// it. Its capacity is its length: registering a `Vec` with spare + /// capacity reallocates it (`into_boxed_slice`), and the allocation left + /// behind is not wiped. So the frame is copied into an exactly sized + /// buffer, and the writer's own buffer is wiped when it drops. /// /// # Errors /// @@ -90,7 +93,10 @@ impl HostValue { /// deeper than a frame carries. A guest builds its outputs itself, so /// either is a bug in the guest. pub fn encode(&self) -> Result, FrameWriteError> { - to_frame(self).map(Protected::risky_unwrap) + let frame = to_frame(self)?; + let mut out = Vec::with_capacity(frame.risky_ref().len()); + out.extend_from_slice(frame.risky_ref()); + Ok(out) } /// Read one value from a transport frame. @@ -298,6 +304,19 @@ mod tests { /// dropped, by every path that builds one: the constructor, JSON and a /// decoded frame. A plain `String` here would leave it in freed guest /// memory. + /// An encoded frame has no spare capacity, so registering it does not + /// reallocate and leave an unwiped copy behind. + #[test] + fn an_encoded_frame_has_no_spare_capacity() { + let out = HostValue::Map(vec![( + "access_token".to_string(), + HostValue::text("a-bearer-token"), + )]) + .encode() + .unwrap(); + assert_eq!(out.capacity(), out.len()); + } + #[test] fn text_is_wiped_on_drop() { fn held(value: HostValue) -> Zeroizing { From 59e32bb2044ccc4e935acd88ab680ef67cbe9b0a Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Sat, 17 Oct 2026 01:01:51 +1100 Subject: [PATCH 09/15] test(stack-encrypt): cover every FfiValue Debug branch The CRAP gate failed on `FfiValue::fmt` (complexity 15, 35% covered), which this PR moves into stack-encrypt: the one test printed three of its fourteen variants. A test now prints every variant and checks that text and bytes are redacted inside an array, an object and a passthrough as well as at the top. --- .../stack-encrypt/src/dynamic/ffi_value.rs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/packages/stack-encrypt/src/dynamic/ffi_value.rs b/packages/stack-encrypt/src/dynamic/ffi_value.rs index 51073df99..a39de4b1e 100644 --- a/packages/stack-encrypt/src/dynamic/ffi_value.rs +++ b/packages/stack-encrypt/src/dynamic/ffi_value.rs @@ -660,6 +660,38 @@ mod tests { assert_eq!(format!("{:?}", FfiValue::UInt32(34)), "UInt32(34)"); } + /// Every variant's `Debug` form: numbers and booleans print, text and + /// bytes are redacted at any depth, so a container or a passthrough + /// holding a secret prints none of it. + #[test] + fn debug_prints_every_variant_and_redacts_at_any_depth() { + let cases = [ + (FfiValue::Null, "Null"), + (FfiValue::Undefined, "Undefined"), + (FfiValue::Bool(true), "Bool(true)"), + (FfiValue::Int32(-1), "Int32(-1)"), + (FfiValue::Int64(-2), "Int64(-2)"), + (FfiValue::UInt64(3), "UInt64(3)"), + (FfiValue::Float32(1.5), "Float32(1.5)"), + (FfiValue::Float64(2.5), "Float64(2.5)"), + ( + FfiValue::Array(vec![s("secret"), FfiValue::UInt32(1)]), + "Array([String(), UInt32(1)])", + ), + ( + FfiValue::Object(vec![("k".into(), s("secret"))]), + "Object([(\"k\", String())])", + ), + ( + FfiValue::Passthrough(Box::new(FfiValue::Bytes(Protected::new(vec![1])))), + "Passthrough(Bytes())", + ), + ]; + for (value, printed) in cases { + assert_eq!(format!("{value:?}"), printed); + } + } + #[test] fn kind_names_parse_back_exactly() { for kind in ValueKind::ALL { From c45b71284a40f5b377e6833ad8b75fe692e46712 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Sat, 17 Oct 2026 02:04:31 +1100 Subject: [PATCH 10/15] build(stack-encrypt): drop the unused vitaminc-aead-frame dependency `cargo udeps` failed on this PR: the `dynamic` feature still enabled `vitaminc-aead-frame`, but nothing in stack-encrypt uses it, since an `FfiValue` frames itself through vitaminc-aead's own `to_frame` and `Frame`. The guests and stack-guest-abi, which do carry ciphertext trees in the frame codec, keep their own dependency. `dynamic` is now a plain feature. --- Cargo.lock | 1 - languages/golang/encrypt/guest/Cargo.lock | 1 - packages/eql/Cargo.lock | 9 --------- packages/eql/Cargo.toml | 1 - packages/stack-encrypt/Cargo.toml | 6 ++---- packages/stack-encrypt/fuzz/Cargo.lock | 9 --------- packages/stack-encrypt/fuzz/Cargo.toml | 1 - 7 files changed, 2 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d5d5f8a74..c0509ea08 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3299,7 +3299,6 @@ dependencies = [ "trybuild", "uuid", "vitaminc-aead", - "vitaminc-aead-frame", "vitaminc-encrypt", "vitaminc-hmac", "vitaminc-prf", diff --git a/languages/golang/encrypt/guest/Cargo.lock b/languages/golang/encrypt/guest/Cargo.lock index 067fd6f9c..63295b7c4 100644 --- a/languages/golang/encrypt/guest/Cargo.lock +++ b/languages/golang/encrypt/guest/Cargo.lock @@ -2085,7 +2085,6 @@ dependencies = [ "thiserror 1.0.69", "uuid", "vitaminc-aead", - "vitaminc-aead-frame", "vitaminc-encrypt", "vitaminc-hmac", "vitaminc-prf", diff --git a/packages/eql/Cargo.lock b/packages/eql/Cargo.lock index 072bd0f6e..173b22612 100644 --- a/packages/eql/Cargo.lock +++ b/packages/eql/Cargo.lock @@ -4240,7 +4240,6 @@ dependencies = [ "thiserror 1.0.69", "uuid", "vitaminc-aead 0.5.1", - "vitaminc-aead-frame", "vitaminc-encrypt 0.5.1", "vitaminc-hmac", "vitaminc-prf", @@ -5081,14 +5080,6 @@ dependencies = [ "syn 3.0.3", ] -[[package]] -name = "vitaminc-aead-frame" -version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" -dependencies = [ - "vitaminc-aead 0.5.1", -] - [[package]] name = "vitaminc-context" version = "0.5.1" diff --git a/packages/eql/Cargo.toml b/packages/eql/Cargo.toml index 2452354eb..a6e266d4f 100644 --- a/packages/eql/Cargo.toml +++ b/packages/eql/Cargo.toml @@ -43,7 +43,6 @@ unused_imports = "deny" [patch.crates-io] vitaminc = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } vitaminc-aead = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } -vitaminc-aead-frame = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } vitaminc-encrypt = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } vitaminc-hmac = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } vitaminc-prf = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } diff --git a/packages/stack-encrypt/Cargo.toml b/packages/stack-encrypt/Cargo.toml index 0be8a4526..a046919a4 100644 --- a/packages/stack-encrypt/Cargo.toml +++ b/packages/stack-encrypt/Cargo.toml @@ -30,8 +30,6 @@ stack-encrypt-derive = { path = "../stack-encrypt-derive", version = "0.2.0" } # (#318), all of which this crate relies on. All five must share one # version or the aead crate is duplicated. vitaminc-aead = { workspace = true } -# `dynamic`: the runtime value model every language binding funnels through. -vitaminc-aead-frame = { workspace = true, optional = true } vitaminc-encrypt = { workspace = true } vitaminc-hmac = { workspace = true } vitaminc-prf = { workspace = true } @@ -60,8 +58,8 @@ default = ["http"] http = ["dep:stack-auth", "stack-auth/http", "stack-kms/http"] # `stack_encrypt::dynamic`: encrypt and decrypt values whose types are known # only at runtime, as an FFI binding's are. Off by default — it is only of -# use to a binding author, and it pulls `vitaminc-aead-frame` in. -dynamic = ["dep:vitaminc-aead-frame"] +# use to a binding author. Its transport frames are vitaminc-aead's own. +dynamic = [] # `stack_encrypt::kms::FakeDataKeySource`: the in-memory key source for tests # with no ZeroKMS, so a caller's dev-dependency on stack-encrypt is enough and # no direct stack-kms dependency has to agree with this crate's. diff --git a/packages/stack-encrypt/fuzz/Cargo.lock b/packages/stack-encrypt/fuzz/Cargo.lock index f9e70a6aa..d0c479f1e 100644 --- a/packages/stack-encrypt/fuzz/Cargo.lock +++ b/packages/stack-encrypt/fuzz/Cargo.lock @@ -2042,7 +2042,6 @@ dependencies = [ "thiserror 1.0.69", "uuid", "vitaminc-aead", - "vitaminc-aead-frame", "vitaminc-encrypt", "vitaminc-hmac", "vitaminc-prf", @@ -2549,14 +2548,6 @@ dependencies = [ "syn 3.0.3", ] -[[package]] -name = "vitaminc-aead-frame" -version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" -dependencies = [ - "vitaminc-aead", -] - [[package]] name = "vitaminc-context" version = "0.5.1" diff --git a/packages/stack-encrypt/fuzz/Cargo.toml b/packages/stack-encrypt/fuzz/Cargo.toml index 56b105d5e..48683a13f 100644 --- a/packages/stack-encrypt/fuzz/Cargo.toml +++ b/packages/stack-encrypt/fuzz/Cargo.toml @@ -70,7 +70,6 @@ resolver = "2" [patch.crates-io] vitaminc = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } vitaminc-aead = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } -vitaminc-aead-frame = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } vitaminc-encrypt = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } vitaminc-hmac = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } vitaminc-prf = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } From 1a7fff5111027f4b145c1d37c5e75b4dfada0b9b Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Sat, 17 Oct 2026 12:19:20 +1100 Subject: [PATCH 11/15] fix(ci): package without a registry build while a crate is pinned to git The two "package cleanly for crates.io" steps ran `cargo publish --dry-run` over the stack-* crates and eql-bindings. `cargo package` drops `[patch.crates-io]`, so once this branch pinned vitaminc to a git commit the verify compiled against vitaminc 0.5.1 from crates.io, which lacks the API the tree now uses, and failed with unresolved imports. That build tests a dependency the tree does not use, so it cannot pass until vitaminc is released, and it kept tests-crates and test-eql (and so ci-required) red. Both steps now read the crates' normal and build graph with `cargo tree`. When any crate in it resolves from git, they package with `--no-verify`, which still checks the metadata and that every path dependency names a version, and emit a warning naming the pins. With no git crate in the graph, as on main, they run the full verify as before. The release is not weakened: `cargo publish` verifies the same way and refuses these crates until the pins are registry versions. --- .github/workflows/test-eql.yml | 15 +++++++++++++++ .github/workflows/tests-crates.yml | 24 +++++++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-eql.yml b/.github/workflows/test-eql.yml index ead9b325c..e503d511e 100644 --- a/.github/workflows/test-eql.yml +++ b/.github/workflows/test-eql.yml @@ -649,9 +649,24 @@ jobs: # here it is a warning, not a failure, and the registry build is left to # the release. A version that RESOLVES and then does not compile is the # silent case, the one a path build can never see, and that fails. + # + # And one case the verify cannot judge at all: a crate in eql-bindings' + # graph pinned to GIT (`[patch.crates-io]`, as vitaminc is while `next` + # waits on its release). `cargo package` drops `[patch]`, so the verify + # would compile against the registry's version of that crate, which + # the tree does not use. While such a pin is in the graph the step only + # packages (`--no-verify`) and warns, as the stack-* crates' step in + # tests-crates.yml does; the publish refuses it the same way. - name: Verify eql-bindings packages cleanly for crates.io run: | set -euo pipefail + tree="$(cargo tree --locked -e normal,build --prefix none --all-features -p eql-bindings)" + pins="$(grep -E '\((https?|ssh)://' <<<"$tree" | sed -E 's/ \(\*\)$//' | sort -u || true)" + if [ -n "$pins" ]; then + printf 'Built against git, so not verifiable against the registry:\n%s\n' "$pins" + echo "::warning title=eql-bindings not verified against the registry::It builds against $(wc -l <<<"$pins" | tr -d ' ') crate(s) pinned to git ([patch.crates-io]), which cargo package ignores, so the registry build was skipped and only the packaging was checked. The publish refuses eql-bindings until the pins are registry versions." + exec cargo package --locked --no-verify --allow-dirty --all-features -p eql-bindings + fi # cargo's own status is kept in `status`: `|| status=$?` reads it # before anything else runs. (`status=$?` on the line after an `if` # reads the if's status, which is 0 when its condition failed, so a diff --git a/.github/workflows/tests-crates.yml b/.github/workflows/tests-crates.yml index 40cc572cc..2ac664c5a 100644 --- a/.github/workflows/tests-crates.yml +++ b/.github/workflows/tests-crates.yml @@ -153,8 +153,30 @@ jobs: # `--allow-dirty` tolerates files earlier steps leave in the tree. Once a # version is on crates.io, cargo warns rather than fails on a dry run of # it. + # + # A crate these build against from GIT (a `[patch.crates-io]` pin on an + # unreleased dependency, as vitaminc is while `next` waits on its + # release) cannot be verified here: `cargo package` drops `[patch]`, so + # the verify compiles against the registry's version of that crate, a + # build that tests a dependency the tree does not use and fails on API + # the release has not shipped. While such a pin is in the graph the step + # packages without verifying, which still checks the metadata and that + # every path dependency names a version, and warns, naming the pins. It + # is not a hole in the release: `cargo publish` in release-crates + # verifies the same way and refuses these crates until the pin is a + # registry version. - name: Verify the stack-* crates package cleanly for crates.io - run: cargo publish --locked --dry-run --allow-dirty -p stack-profile -p stack-auth -p stack-kms -p stack-encrypt-derive -p stack-encrypt + run: | + set -euo pipefail + crates=(-p stack-profile -p stack-auth -p stack-kms -p stack-encrypt-derive -p stack-encrypt) + tree="$(cargo tree --locked -e normal,build --prefix none "${crates[@]}")" + pins="$(grep -E '\((https?|ssh)://' <<<"$tree" | sed -E 's/ \(\*\)$//' | sort -u || true)" + if [ -z "$pins" ]; then + exec cargo publish --locked --dry-run --allow-dirty "${crates[@]}" + fi + printf 'Built against git, so not verifiable against the registry:\n%s\n' "$pins" + echo "::warning title=stack-* crates not verified against the registry::They build against $(wc -l <<<"$pins" | tr -d ' ') crate(s) pinned to git ([patch.crates-io]), which cargo package ignores, so the registry build was skipped and only the packaging was checked. release-crates refuses to publish them until the pins are registry versions." + cargo package --locked --no-verify --allow-dirty "${crates[@]}" node-bindings: name: node bindings, napi typings, stack-auth-wasm From 064c10406e2bd81f4f2acb823b48306696b1a3f3 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Sat, 17 Oct 2026 12:33:30 +1100 Subject: [PATCH 12/15] fix(stack-encrypt): address the second review of the vitaminc ADR 0001 port MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - A dynamic passthrough value is framed when the source is read, not after the key round trip. One the frame cannot carry was framed only in the output adapter, inside `try_map`, so it cost a ZeroKMS key-generation call and then failed the whole record as `Error::Aead` with no field named. `check_field` now trial-frames a passthrough value and refuses it as `Error::Source` naming the field: a repeated key as `Reason::RepeatedKey`, and a value too deep or too long for a frame as the new `Reason::Unframeable`. A test pins that no key is generated. - Reading an object from a frame checks for a repeated key with a lookup. `ValueMap::entry` scanned every entry read so far, so an object of `n` keys in host-supplied bytes cost `n²` comparisons. The keys seen are now kept in a `BTreeSet`, ordered rather than hashed so no key can be chosen to collide. - In `stack-guest-abi`, the doc comment for `text_is_wiped_on_drop` sat on the test above it; it is back on its own test. --- packages/stack-encrypt/CHANGELOG.md | 5 ++ .../stack-encrypt/src/dynamic/ffi_value.rs | 30 +++++++- packages/stack-encrypt/src/dynamic/mod.rs | 3 + packages/stack-encrypt/src/dynamic/record.rs | 73 ++++++++++++++++++- packages/stack-guest-abi/src/value.rs | 10 +-- 5 files changed, 111 insertions(+), 10 deletions(-) diff --git a/packages/stack-encrypt/CHANGELOG.md b/packages/stack-encrypt/CHANGELOG.md index 69ab15c14..5a5b5c6ca 100644 --- a/packages/stack-encrypt/CHANGELOG.md +++ b/packages/stack-encrypt/CHANGELOG.md @@ -174,6 +174,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `Node`: a passthrough subtree is a `PassthroughFrame`, the transport frame of its value, in place of a boxed value, and `transcode::Visitor::passthrough` takes one. + A dynamic passthrough value the frame cannot carry is refused when the + source is read, before any key is requested, as `Error::Source` naming + the field: a repeated key as `Reason::RepeatedKey`, and a value nested + deeper, or holding text, bytes or a key longer, than a frame carries as + the new `Reason::Unframeable` (`"unframeable"`). - **`dynamic::FfiValue` and `dynamic::ValueKind` are this crate's own**, with the same variants and kind names, since vitaminc no longer has a value tree. A value crosses a binding's boundary as a transport frame diff --git a/packages/stack-encrypt/src/dynamic/ffi_value.rs b/packages/stack-encrypt/src/dynamic/ffi_value.rs index a39de4b1e..34c7ebc95 100644 --- a/packages/stack-encrypt/src/dynamic/ffi_value.rs +++ b/packages/stack-encrypt/src/dynamic/ffi_value.rs @@ -11,6 +11,7 @@ //! in shape, so the engine and the bindings built on it keep their //! behaviour while they move onto vitaminc's model. +use std::collections::BTreeSet; use std::fmt; use std::str::FromStr; @@ -301,6 +302,7 @@ impl Sink for ValueSink { Ok(ValueMap { out: self.0, entries: Vec::new(), + keys: BTreeSet::new(), }) } @@ -338,6 +340,11 @@ impl SeqSink for ValueSeq { pub struct ValueMap { out: O, entries: Vec<(String, FfiValue)>, + /// The keys seen so far, so the repeated-key check is a lookup rather + /// than a scan of `entries`: a frame is host input, and a scan made an + /// object of `n` keys cost `n²` comparisons. Ordered rather than hashed, + /// so no key can be chosen to collide. + keys: BTreeSet, } impl MapSink for ValueMap { @@ -349,7 +356,7 @@ impl MapSink for ValueMap { Self: 's; fn entry(&mut self, key: &str) -> Result>, DecodeError> { - if self.entries.iter().any(|(k, _)| k == key) { + if !self.keys.insert(key.to_owned()) { return Err(DecodeError::duplicate_key(key)); } Ok(ValueSink(Entry(&mut self.entries, key.to_owned()))) @@ -571,6 +578,27 @@ mod tests { ]) } + /// Reading an object refuses a key it has already read, wherever the + /// repeat falls. + #[test] + fn reading_a_repeated_key_is_refused() { + let keys: Vec = (0..1000).map(|i| format!("k{i}")).collect(); + let entries = |repeat: &str| { + let mut entries: Vec<(String, FfiValue)> = + keys.iter().map(|k| (k.clone(), FfiValue::Null)).collect(); + entries.push((repeat.to_string(), FfiValue::Null)); + FfiValue::Object(entries) + }; + for repeat in ["k0", "k500", "k999"] { + let refused = entries(repeat).visit(FfiValue::sink()); + assert!(refused.is_err(), "{repeat} read twice is refused"); + } + let read = entries("k1000") + .visit(FfiValue::sink()) + .expect("all distinct"); + assert!(matches!(read, FfiValue::Object(e) if e.len() == 1001)); + } + #[test] fn every_variant_survives_a_frame_round_trip() { let frame = every_variant().to_frame().expect("frames"); diff --git a/packages/stack-encrypt/src/dynamic/mod.rs b/packages/stack-encrypt/src/dynamic/mod.rs index 78de96b30..1b4f8dd56 100644 --- a/packages/stack-encrypt/src/dynamic/mod.rs +++ b/packages/stack-encrypt/src/dynamic/mod.rs @@ -529,6 +529,9 @@ reasons! { FieldType => "field_type", "a value is not of the type its field declares"; /// A passthrough sits where a sealed value, or a ciphertext, must be. Passthrough => "passthrough", "a passthrough where a sealed value must be"; + /// A passthrough value cannot be stored as a frame: it nests deeper, + /// or holds text, bytes or a key longer, than a frame carries. + Unframeable => "unframeable", "a passthrough value nests deeper, or holds a longer value, than a frame carries"; /// A stored field is not a map of outputs. OutputsNotMap => "outputs_not_map", "a stored field is not a map of outputs"; /// A stored sealed field has no `"c"` node. diff --git a/packages/stack-encrypt/src/dynamic/record.rs b/packages/stack-encrypt/src/dynamic/record.rs index 43b4965a0..84a64e75e 100644 --- a/packages/stack-encrypt/src/dynamic/record.rs +++ b/packages/stack-encrypt/src/dynamic/record.rs @@ -138,6 +138,7 @@ //! that a round-trip invariant rather than data loss. use super::{FfiValue, ValueKind}; +use vitaminc_aead::frame::FrameWriteError; use vitaminc_protected::Controlled; use super::{ @@ -1832,12 +1833,26 @@ fn check_field(value: &FfiValue, field: &FieldPlan) -> Result<(), Error> { }); } } - Output::Passthrough => {} + Output::Passthrough => check_frame(value, &field.name)?, } } Ok(()) } +/// A passthrough value is stored as its frame ([`passthrough`]), so one the +/// frame cannot carry is refused here, naming its field, before any key is +/// requested: a repeated key as [`Reason::RepeatedKey`], anything else (too +/// deep, too long) as [`Reason::Unframeable`]. Left to the output adapter it +/// failed the whole record as a bare [`Aead`](crate::Error::Aead), after the +/// key round trip. The trial frame is wiped as it drops. +fn check_frame(value: &FfiValue, field: &str) -> Result<(), Error> { + match value.to_frame() { + Ok(_) => Ok(()), + Err(FrameWriteError::DuplicateKey) => Err(FfiValue::misfit(field, Reason::RepeatedKey)), + Err(_) => Err(FfiValue::misfit(field, Reason::Unframeable)), + } +} + // ============================================================================= // Output adapters: the engine's record as the stored shape, and back // ============================================================================= @@ -1867,9 +1882,10 @@ fn term_node(term: TermBytes) -> Result { ))) } -/// A value as a passthrough node: the value's frame, in the clear. A value -/// the frame cannot carry (nested deeper than it allows) is refused as a -/// value vitaminc refuses to encrypt is, with [`Aead`](crate::Error::Aead). +/// A value as a passthrough node: the value's frame, in the clear. A source +/// value the frame cannot carry is refused before this runs +/// ([`check_frame`]); one that reaches it anyway is refused as a value +/// vitaminc refuses to encrypt is, with [`Aead`](crate::Error::Aead). fn passthrough(value: &FfiValue) -> Result { let frame = value.to_frame().map_err(|_| crate::Error::Aead)?; Ok(CipherText::Passthrough(PassthroughFrame( @@ -2075,6 +2091,7 @@ mod tests { IdentifiedBy, IndexKey, IndexKeySource, RetrieveKeyPayload, UnverifiedContext, }; use uuid::Uuid; + use vitaminc_aead::frame::MAX_DEPTH; use vitaminc_protected::{Controlled, Protected}; use crate::dynamic::{context, term}; @@ -2146,6 +2163,11 @@ mod tests { // ---- values, as a binding would decode them ---------------------------- + /// A null inside `depth` nested arrays. + fn nested(depth: usize) -> FfiValue { + (0..depth).fold(FfiValue::Null, |inner, _| FfiValue::Array(vec![inner])) + } + fn s(value: &str) -> FfiValue { FfiValue::String(value.into()) } @@ -6501,6 +6523,27 @@ mod tests { } } + /// A passthrough the frame cannot carry is refused when the source is + /// read, naming its field, before any key is generated. + #[tokio::test] + async fn an_unframeable_passthrough_is_refused_before_keys_are_generated() { + let cipher = cipher().await; + let keyset = cipher.default_keyset(); + let mut source = object(row(34)); + let _ = take(&mut source, "id"); + source.push(("id".to_string(), nested(MAX_DEPTH + 1))); + let refused = encrypt(&keyset, FfiValue::Object(source), &the_plan()) + .err() + .expect("refused before the pending exists"); + expect(&refused, "Source", Some("id"), Reason::Unframeable); + assert_eq!(generates(&cipher), 0, "no key was generated"); + + let mut source = object(row(34)); + let _ = take(&mut source, "id"); + source.push(("id".to_string(), nested(MAX_DEPTH - 1))); + let _ = seal(&keyset, FfiValue::Object(source), &the_plan()).await; + } + #[test] fn a_source_that_does_not_fit() { use Reason::*; @@ -6521,8 +6564,30 @@ mod tests { "email".to_string(), FfiValue::Passthrough(Box::new(s("a@x"))), )); + let mut deep_id = without("id"); + deep_id.push(("id".to_string(), nested(MAX_DEPTH + 1))); + let mut repeated_id = without("id"); + repeated_id.push(( + "id".to_string(), + FfiValue::Object(vec![ + ("k".to_string(), FfiValue::Null), + ("k".to_string(), FfiValue::Null), + ]), + )); let cases: Vec<(&str, FfiValue, Option<&str>, Reason)> = vec![ ("not an object", s("x"), None, NotAnObject), + ( + "a passthrough deeper than a frame carries", + FfiValue::Object(deep_id), + Some("id"), + Unframeable, + ), + ( + "a passthrough with a repeated key", + FfiValue::Object(repeated_id), + Some("id"), + RepeatedKey, + ), ( "a batch row that is not an object", FfiValue::Array(vec![s("x")]), diff --git a/packages/stack-guest-abi/src/value.rs b/packages/stack-guest-abi/src/value.rs index 984475589..a350c3cde 100644 --- a/packages/stack-guest-abi/src/value.rs +++ b/packages/stack-guest-abi/src/value.rs @@ -299,11 +299,6 @@ impl MapSink for ValueMap { mod tests { use super::*; - /// Text is held as `Zeroizing`, so a secret a guest hands back - /// as text (the auth guest's access token) is wiped when the value is - /// dropped, by every path that builds one: the constructor, JSON and a - /// decoded frame. A plain `String` here would leave it in freed guest - /// memory. /// An encoded frame has no spare capacity, so registering it does not /// reallocate and leave an unwiped copy behind. #[test] @@ -317,6 +312,11 @@ mod tests { assert_eq!(out.capacity(), out.len()); } + /// Text is held as `Zeroizing`, so a secret a guest hands back + /// as text (the auth guest's access token) is wiped when the value is + /// dropped, by every path that builds one: the constructor, JSON and a + /// decoded frame. A plain `String` here would leave it in freed guest + /// memory. #[test] fn text_is_wiped_on_drop() { fn held(value: HostValue) -> Zeroizing { From 989f99ad96754144365d535834b05a564374c842 Mon Sep 17 00:00:00 2001 From: Dan Draper Date: Sat, 17 Oct 2026 13:39:08 +1100 Subject: [PATCH 13/15] fix(ci): leave out a crate whose git-pinned dependency has no crates.io release Packaging without the registry build still resolves every dependency against crates.io. vitaminc-normalize, which stack-encrypt takes from #1214 on, has no release there at all, so `cargo package --no-verify` failed on stack-encrypt with "no matching package named `vitaminc-normalize`" and kept tests-crates red. Both packaging steps now call scripts/package-git-pinned-crates.sh while a git pin is in the graph. It packages the crates with `--no-verify`, and when one needs a pinned crate that crates.io does not have, it leaves that crate out with a warning naming the dependency and packages the rest. Any other packaging failure still fails the step, and a graph with no git pin still gets the full verify. --- .github/workflows/test-eql.yml | 9 ++-- .github/workflows/tests-crates.yml | 12 +++--- scripts/package-git-pinned-crates.sh | 62 ++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 10 deletions(-) create mode 100755 scripts/package-git-pinned-crates.sh diff --git a/.github/workflows/test-eql.yml b/.github/workflows/test-eql.yml index e503d511e..3a101c81c 100644 --- a/.github/workflows/test-eql.yml +++ b/.github/workflows/test-eql.yml @@ -655,17 +655,16 @@ jobs: # waits on its release). `cargo package` drops `[patch]`, so the verify # would compile against the registry's version of that crate, which # the tree does not use. While such a pin is in the graph the step only - # packages (`--no-verify`) and warns, as the stack-* crates' step in - # tests-crates.yml does; the publish refuses it the same way. + # packages (`--no-verify`) and warns, through the script the stack-* + # crates' step in tests-crates.yml uses; the publish refuses it the + # same way. - name: Verify eql-bindings packages cleanly for crates.io run: | set -euo pipefail tree="$(cargo tree --locked -e normal,build --prefix none --all-features -p eql-bindings)" pins="$(grep -E '\((https?|ssh)://' <<<"$tree" | sed -E 's/ \(\*\)$//' | sort -u || true)" if [ -n "$pins" ]; then - printf 'Built against git, so not verifiable against the registry:\n%s\n' "$pins" - echo "::warning title=eql-bindings not verified against the registry::It builds against $(wc -l <<<"$pins" | tr -d ' ') crate(s) pinned to git ([patch.crates-io]), which cargo package ignores, so the registry build was skipped and only the packaging was checked. The publish refuses eql-bindings until the pins are registry versions." - exec cargo package --locked --no-verify --allow-dirty --all-features -p eql-bindings + PINS="$pins" exec "$GITHUB_WORKSPACE/scripts/package-git-pinned-crates.sh" eql-bindings --all-features -- eql-bindings fi # cargo's own status is kept in `status`: `|| status=$?` reads it # before anything else runs. (`status=$?` on the line after an `if` diff --git a/.github/workflows/tests-crates.yml b/.github/workflows/tests-crates.yml index 2ac664c5a..1a69a552a 100644 --- a/.github/workflows/tests-crates.yml +++ b/.github/workflows/tests-crates.yml @@ -161,8 +161,11 @@ jobs: # build that tests a dependency the tree does not use and fails on API # the release has not shipped. While such a pin is in the graph the step # packages without verifying, which still checks the metadata and that - # every path dependency names a version, and warns, naming the pins. It - # is not a hole in the release: `cargo publish` in release-crates + # every path dependency names a version, and warns, naming the pins; a + # crate that needs a pinned crate with no crates.io release at all + # cannot be packaged and is left out with a warning + # (scripts/package-git-pinned-crates.sh). It is not a hole in the + # release: `cargo publish` in release-crates # verifies the same way and refuses these crates until the pin is a # registry version. - name: Verify the stack-* crates package cleanly for crates.io @@ -174,9 +177,8 @@ jobs: if [ -z "$pins" ]; then exec cargo publish --locked --dry-run --allow-dirty "${crates[@]}" fi - printf 'Built against git, so not verifiable against the registry:\n%s\n' "$pins" - echo "::warning title=stack-* crates not verified against the registry::They build against $(wc -l <<<"$pins" | tr -d ' ') crate(s) pinned to git ([patch.crates-io]), which cargo package ignores, so the registry build was skipped and only the packaging was checked. release-crates refuses to publish them until the pins are registry versions." - cargo package --locked --no-verify --allow-dirty "${crates[@]}" + PINS="$pins" exec scripts/package-git-pinned-crates.sh "stack-* crates" -- \ + stack-profile stack-auth stack-kms stack-encrypt-derive stack-encrypt node-bindings: name: node bindings, napi typings, stack-auth-wasm diff --git a/scripts/package-git-pinned-crates.sh b/scripts/package-git-pinned-crates.sh new file mode 100755 index 000000000..ab48882ae --- /dev/null +++ b/scripts/package-git-pinned-crates.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# Package crates for crates.io WITHOUT the registry build, for a graph that +# pins a dependency to git through `[patch.crates-io]`. +# +# PINS="" \ +# scripts/package-git-pinned-crates.sh [cargo package flags...] -- <crate>... +# +# `cargo package` drops `[patch]`, so a verify would compile against the +# registry's version of the pinned crate, a build that tests a dependency the +# tree does not use. This packages with `--no-verify`, which still checks each +# crate's metadata and that every path dependency names a version. +# +# Packaging still resolves every dependency against crates.io. A crate whose +# graph needs a pinned crate that has NO release there yet (vitaminc-normalize +# before vitaminc 0.6) cannot be packaged at all; that crate is left out with a +# warning and the rest are packaged without it. Any other failure fails the +# step. None of this weakens the release: `cargo publish` verifies against the +# registry and refuses these crates until the pins are registry versions. +set -euo pipefail + +title="$1" +shift +flags=() +while [ "$#" -gt 0 ] && [ "$1" != "--" ]; do + flags+=("$1") + shift +done +[ "$#" -gt 0 ] || { echo "usage: $0 <title> [flags...] -- <crate>..." >&2; exit 2; } +shift +remaining=("$@") + +pinned_names="$(awk '{print $1}' <<<"${PINS:?PINS must list the git-pinned crates}" | sort -u)" +printf 'Built against git, so not verifiable against the registry:\n%s\n' "$PINS" +echo "::warning title=${title} not verified against the registry::They build against $(wc -l <<<"$PINS" | tr -d ' ') crate(s) pinned to git ([patch.crates-io]), which cargo package ignores, so the registry build was skipped and only the packaging was checked. The publish refuses them until the pins are registry versions." + +while [ "${#remaining[@]}" -gt 0 ]; do + args=() + for crate in "${remaining[@]}"; do + args+=(-p "$crate") + done + status=0 + out="$(cargo package --locked --no-verify --allow-dirty "${flags[@]}" "${args[@]}" 2>&1)" || status=$? + printf '%s\n' "$out" + [ "$status" -eq 0 ] && exit 0 + + missing="$(sed -nE 's/.*no matching package named `([^`]+)` found.*/\1/p' <<<"$out" | head -n 1)" + needer="$(sed -nE 's/.*required by package `([^ ]+) v.*/\1/p' <<<"$out" | head -n 1)" + if [ -z "$missing" ] || [ -z "$needer" ] || ! grep -qxF "$missing" <<<"$pinned_names"; then + echo "::error title=${title} failed to package::cargo package failed for a reason other than an unreleased git-pinned dependency; see the log above." + exit "$status" + fi + kept=() + for crate in "${remaining[@]}"; do + [ "$crate" = "$needer" ] || kept+=("$crate") + done + if [ "${#kept[@]}" -eq "${#remaining[@]}" ]; then + echo "::error title=${title} failed to package::${needer} needs ${missing}, which has no crates.io release, but ${needer} is not one of the crates being packaged." + exit "$status" + fi + echo "::warning title=${needer} not packaged::${needer} needs ${missing}, pinned to git and not yet released on crates.io, so it cannot be packaged until that release." + remaining=("${kept[@]+"${kept[@]}"}") +done From 17edce072960f475dd968f4b5268eff0d076a8ab Mon Sep 17 00:00:00 2001 From: Dan Draper <dan@cipherstash.com> Date: Sat, 17 Oct 2026 16:07:42 +1100 Subject: [PATCH 14/15] build!: require vitaminc 0.6.0 from crates.io, and drop the git pin and the CI packaging workaround vitaminc 0.6.0 is published, the first release of ADR 0001, so every workspace that compiles stack-encrypt (the root, the encrypt and credential guests, the stack-encrypt fuzz crate and packages/eql) now requires the 0.6.0 crates and carries no `[patch.crates-io]` block. Each lockfile moves from the git rev to the registry. The Go module moves vcffi and vcvalue to the release commit (ca2298d3e7e8), which the Rust crates were cut from. cts-common 0.43 (from crates.io) still requires vitaminc 0.5, which the patch had unified with ours, so the graph now holds both; nothing passes a vitaminc type between them. One trybuild snapshot gains rustc's note that two `vitaminc_aead` versions are present. With no git crate in the graph, the two "package cleanly for crates.io" steps are back to the full `cargo publish --dry-run` they ran before the pin, so the workaround for a pinned graph goes: scripts/package-git-pinned-crates.sh, and the branches in tests-crates.yml and test-eql.yml that called it. --- .github/workflows/test-eql.yml | 14 - .github/workflows/tests-crates.yml | 26 +- Cargo.lock | 247 +++++++++++++---- Cargo.toml | 30 +-- languages/golang/auth/guest/Cargo.lock | 221 +++++++++++++--- languages/golang/auth/guest/Cargo.toml | 12 +- languages/golang/encrypt/guest/Cargo.lock | 249 ++++++++++++++---- languages/golang/encrypt/guest/Cargo.toml | 16 +- languages/golang/go.mod | 4 +- languages/golang/go.sum | 8 +- packages/eql/Cargo.lock | 208 ++++++++++++--- packages/eql/Cargo.toml | 12 - packages/eql/crates/eql-bindings/Cargo.toml | 4 +- packages/eql/tests/encryption/Cargo.toml | 2 +- packages/stack-encrypt/CHANGELOG.md | 4 + packages/stack-encrypt/Cargo.toml | 2 +- packages/stack-encrypt/fuzz/Cargo.lock | 236 +++++++++++++---- packages/stack-encrypt/fuzz/Cargo.toml | 13 +- .../tests/ui/plaintext_needs_encrypt.stderr | 11 + scripts/package-git-pinned-crates.sh | 62 ----- 20 files changed, 983 insertions(+), 398 deletions(-) delete mode 100755 scripts/package-git-pinned-crates.sh diff --git a/.github/workflows/test-eql.yml b/.github/workflows/test-eql.yml index 3a101c81c..ead9b325c 100644 --- a/.github/workflows/test-eql.yml +++ b/.github/workflows/test-eql.yml @@ -649,23 +649,9 @@ jobs: # here it is a warning, not a failure, and the registry build is left to # the release. A version that RESOLVES and then does not compile is the # silent case, the one a path build can never see, and that fails. - # - # And one case the verify cannot judge at all: a crate in eql-bindings' - # graph pinned to GIT (`[patch.crates-io]`, as vitaminc is while `next` - # waits on its release). `cargo package` drops `[patch]`, so the verify - # would compile against the registry's version of that crate, which - # the tree does not use. While such a pin is in the graph the step only - # packages (`--no-verify`) and warns, through the script the stack-* - # crates' step in tests-crates.yml uses; the publish refuses it the - # same way. - name: Verify eql-bindings packages cleanly for crates.io run: | set -euo pipefail - tree="$(cargo tree --locked -e normal,build --prefix none --all-features -p eql-bindings)" - pins="$(grep -E '\((https?|ssh)://' <<<"$tree" | sed -E 's/ \(\*\)$//' | sort -u || true)" - if [ -n "$pins" ]; then - PINS="$pins" exec "$GITHUB_WORKSPACE/scripts/package-git-pinned-crates.sh" eql-bindings --all-features -- eql-bindings - fi # cargo's own status is kept in `status`: `|| status=$?` reads it # before anything else runs. (`status=$?` on the line after an `if` # reads the if's status, which is 0 when its condition failed, so a diff --git a/.github/workflows/tests-crates.yml b/.github/workflows/tests-crates.yml index 1a69a552a..40cc572cc 100644 --- a/.github/workflows/tests-crates.yml +++ b/.github/workflows/tests-crates.yml @@ -153,32 +153,8 @@ jobs: # `--allow-dirty` tolerates files earlier steps leave in the tree. Once a # version is on crates.io, cargo warns rather than fails on a dry run of # it. - # - # A crate these build against from GIT (a `[patch.crates-io]` pin on an - # unreleased dependency, as vitaminc is while `next` waits on its - # release) cannot be verified here: `cargo package` drops `[patch]`, so - # the verify compiles against the registry's version of that crate, a - # build that tests a dependency the tree does not use and fails on API - # the release has not shipped. While such a pin is in the graph the step - # packages without verifying, which still checks the metadata and that - # every path dependency names a version, and warns, naming the pins; a - # crate that needs a pinned crate with no crates.io release at all - # cannot be packaged and is left out with a warning - # (scripts/package-git-pinned-crates.sh). It is not a hole in the - # release: `cargo publish` in release-crates - # verifies the same way and refuses these crates until the pin is a - # registry version. - name: Verify the stack-* crates package cleanly for crates.io - run: | - set -euo pipefail - crates=(-p stack-profile -p stack-auth -p stack-kms -p stack-encrypt-derive -p stack-encrypt) - tree="$(cargo tree --locked -e normal,build --prefix none "${crates[@]}")" - pins="$(grep -E '\((https?|ssh)://' <<<"$tree" | sed -E 's/ \(\*\)$//' | sort -u || true)" - if [ -z "$pins" ]; then - exec cargo publish --locked --dry-run --allow-dirty "${crates[@]}" - fi - PINS="$pins" exec scripts/package-git-pinned-crates.sh "stack-* crates" -- \ - stack-profile stack-auth stack-kms stack-encrypt-derive stack-encrypt + run: cargo publish --locked --dry-run --allow-dirty -p stack-profile -p stack-auth -p stack-kms -p stack-encrypt-derive -p stack-encrypt node-bindings: name: node bindings, napi typings, stack-auth-wasm diff --git a/Cargo.lock b/Cargo.lock index c0509ea08..5f523afe0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -772,7 +772,7 @@ dependencies = [ "url", "utoipa", "uuid", - "vitaminc", + "vitaminc 0.5.1", ] [[package]] @@ -3236,8 +3236,8 @@ dependencies = [ "tracing-subscriber", "url", "uuid", - "vitaminc", - "vitaminc-protected", + "vitaminc 0.6.0", + "vitaminc-protected 0.6.0", "web-time", "zeroize", "zerokms-protocol", @@ -3259,7 +3259,7 @@ dependencies = [ "tempfile", "tokio", "url", - "vitaminc-protected", + "vitaminc-protected 0.6.0", "zeroize", ] @@ -3298,11 +3298,11 @@ dependencies = [ "tokio", "trybuild", "uuid", - "vitaminc-aead", - "vitaminc-encrypt", + "vitaminc-aead 0.6.0", + "vitaminc-encrypt 0.6.0", "vitaminc-hmac", "vitaminc-prf", - "vitaminc-protected", + "vitaminc-protected 0.6.0", "zeroize", "zerokms-protocol", ] @@ -3327,9 +3327,9 @@ dependencies = [ "proptest", "serde_json", "thiserror 1.0.69", - "vitaminc-aead", + "vitaminc-aead 0.6.0", "vitaminc-aead-frame", - "vitaminc-protected", + "vitaminc-protected 0.6.0", "zeroize", ] @@ -3361,8 +3361,8 @@ dependencies = [ "tracing", "url", "uuid", - "vitaminc", - "vitaminc-protected", + "vitaminc 0.6.0", + "vitaminc-protected 0.6.0", "zeroize", "zerokms-protocol", ] @@ -4108,36 +4108,79 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vitaminc" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35042fb7a81d72dc43c53fec835e61027f18b4d01cc0f7e12d6c6a6ffbf3bfb9" +dependencies = [ + "vitaminc-aead 0.5.1", + "vitaminc-context 0.5.1", + "vitaminc-encrypt 0.5.1", + "vitaminc-protected 0.5.1", + "vitaminc-random 0.5.1", + "vitaminc-traits 0.5.1", +] + +[[package]] +name = "vitaminc" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0c6b8dff9bfc94e076fcd5496395958e6c8dddb27605b6504c54123512c7c8e" dependencies = [ - "vitaminc-aead", - "vitaminc-context", - "vitaminc-encrypt", - "vitaminc-protected", - "vitaminc-random", - "vitaminc-traits", + "vitaminc-aead 0.6.0", + "vitaminc-context 0.6.0", + "vitaminc-encrypt 0.6.0", + "vitaminc-protected 0.6.0", + "vitaminc-random 0.6.0", + "vitaminc-traits 0.6.0", ] [[package]] name = "vitaminc-aead" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62cacd4dae485e98cfe5407e919187a8468dd933ea4234be5115b7d13a30f3c6" +dependencies = [ + "bytes", + "serde", + "vitaminc-aead-derive 0.5.1", + "vitaminc-context 0.5.1", + "vitaminc-protected 0.5.1", + "vitaminc-random 0.5.1", + "zeroize", +] + +[[package]] +name = "vitaminc-aead" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcea687f745050d80df32c93fbe7f8d18ed2cc5fb5e029814477dfe31449ec2e" dependencies = [ "bytes", "futures-util", "serde", "thiserror 2.0.18", - "vitaminc-aead-derive", - "vitaminc-context", - "vitaminc-protected", - "vitaminc-random", + "vitaminc-aead-derive 0.6.0", + "vitaminc-context 0.6.0", + "vitaminc-protected 0.6.0", + "vitaminc-random 0.6.0", "zeroize", ] [[package]] name = "vitaminc-aead-derive" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ff34056b70a8417ad3bf9db73b2bd3be97f448794566c6de2080bd8a7cd9b6b" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "vitaminc-aead-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "085afc8c77ffadb4c8a93c9c5fa2d897196a36ab38e17c08fe96f8b9c309e05f" dependencies = [ "proc-macro2", "quote", @@ -4146,61 +4189,108 @@ dependencies = [ [[package]] name = "vitaminc-aead-frame" -version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c253055230469c4af4791e902c41ce0ce55069c512249b33cf041dd5c58c517" dependencies = [ - "vitaminc-aead", + "vitaminc-aead 0.6.0", ] [[package]] name = "vitaminc-context" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7df52501fe33cdb3ee9e155b5242fac184c037d147e58c96e5143ed011252a8" +dependencies = [ + "mutants", + "vitaminc-protected 0.5.1", +] + +[[package]] +name = "vitaminc-context" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4d5679e511871da942ba2b29fcb1df198ee2359218bf7f11cfec4fd3dcfeee0" dependencies = [ "mutants", - "vitaminc-protected", + "vitaminc-protected 0.6.0", ] [[package]] name = "vitaminc-encrypt" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbd0e1748055e381e02033e08af385de879c4dcf32e0f6739ac0c0cf0c6f13d" dependencies = [ "aes-gcm", "aws-lc-rs", - "vitaminc-aead", - "vitaminc-protected", - "vitaminc-random", + "vitaminc-aead 0.5.1", + "vitaminc-protected 0.5.1", + "vitaminc-random 0.5.1", + "zeroize", +] + +[[package]] +name = "vitaminc-encrypt" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "061d55dc4084fcc2eaa7334dd28300cd8d321038339a41085b7bf2dc6ccf5d4c" +dependencies = [ + "aes-gcm", + "aws-lc-rs", + "vitaminc-aead 0.6.0", + "vitaminc-protected 0.6.0", + "vitaminc-random 0.6.0", "zeroize", ] [[package]] name = "vitaminc-hmac" -version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "393354b2348eabce0420626fdc7cf70ac779870f82abb566fb491ea19eeea066" dependencies = [ "hmac", "sha2 0.11.0", "vitaminc-prf", - "vitaminc-protected", + "vitaminc-protected 0.6.0", "zeroize", ] [[package]] name = "vitaminc-prf" -version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11de28e524ab1deecedbb023bd328916b6c023e2f52156521ac1df75a689d911" dependencies = [ "mutants", "thiserror 2.0.18", - "vitaminc-context", - "vitaminc-protected", + "vitaminc-context 0.6.0", + "vitaminc-protected 0.6.0", ] [[package]] name = "vitaminc-protected" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7b514f605a63f88874ba46ffea81775fad64994475ab239cb19da159e884b7b" +dependencies = [ + "bitvec", + "digest 0.11.3", + "libc", + "serde", + "serde_bytes", + "subtle", + "thiserror 2.0.18", + "vitaminc-protected-derive 0.5.1", + "zeroize", +] + +[[package]] +name = "vitaminc-protected" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efe97c67b61fd749b20d84248416a6cc1dc3f024a4695e3f69eee63aee7cb7c2" dependencies = [ "bitvec", "digest 0.11.3", @@ -4209,14 +4299,26 @@ dependencies = [ "serde_bytes", "subtle", "thiserror 2.0.18", - "vitaminc-protected-derive", + "vitaminc-protected-derive 0.6.0", "zeroize", ] [[package]] name = "vitaminc-protected-derive" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cb0a3d11afbe2c909f0cfc9996fe37fd89b1196eb0b8d89a66e28716360ee09" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "vitaminc-protected-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0483ecfcb3e874d45c9b2dc04d8e5ed55ff453023a9de98fddc4a24b315d9f6" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -4227,21 +4329,49 @@ dependencies = [ [[package]] name = "vitaminc-random" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7b1bfab35ee322de37269c6fd94d8cd94047a66f97cf6722e53e3321482167" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand 0.10.1", + "thiserror 2.0.18", + "vitaminc-protected 0.5.1", + "vitaminc-random-derives 0.5.1", + "zeroize", +] + +[[package]] +name = "vitaminc-random" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693ae19994e6a79f9839e510220f532daab6f2ab0b4167fdd0d8379a027c653f" dependencies = [ "chacha20", "getrandom 0.4.2", "rand 0.10.1", "thiserror 2.0.18", - "vitaminc-protected", - "vitaminc-random-derives", + "vitaminc-protected 0.6.0", + "vitaminc-random-derives 0.6.0", "zeroize", ] [[package]] name = "vitaminc-random-derives" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0915e2bfb417ce11e23123fb7ec122af5d27d8cfd22d6ad6a2cf7a1c2dbb4712" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "vitaminc-random-derives" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9ad6bb972f7e1b0889eba0a3ed534afe86dd8e52f197a2de20ebe25ee9e56" dependencies = [ "proc-macro2", "quote", @@ -4251,15 +4381,32 @@ dependencies = [ [[package]] name = "vitaminc-traits" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b31ec300e351a7a59e64899d0a2d7e7227cfa74ac0514f4e745acb2cc9335cee" +dependencies = [ + "anyhow", + "bytes", + "rmp-serde", + "serde", + "thiserror 2.0.18", + "vitaminc-protected 0.5.1", + "vitaminc-random 0.5.1", + "zeroize", +] + +[[package]] +name = "vitaminc-traits" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c9f86c930cb4f98eee9288f463a133f01774be039ba05a092f12148b45c430c" dependencies = [ "anyhow", "bytes", "rmp-serde", "serde", "thiserror 2.0.18", - "vitaminc-protected", - "vitaminc-random", + "vitaminc-protected 0.6.0", + "vitaminc-random 0.6.0", "zeroize", ] diff --git a/Cargo.toml b/Cargo.toml index eb32b99a6..325540829 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -120,28 +120,12 @@ web-time = "1.1" zeroize = { version = "1.8.1", features = ["derive"] } # This is needed because lock_api 0.4.6 was breaking things and the branch forces the use of 0.4.12 temp-env = { git = "https://github.com/cipherstash/temp-env", branch = "main" } -vitaminc = { version = "0.5.1", features = ["random", "protected", "encrypt"] } -vitaminc-aead = "0.5.1" +vitaminc = { version = "0.6.0", features = ["random", "protected", "encrypt"] } +vitaminc-aead = "0.6.0" # The transport frame and ciphertext codec every language binding carries # across its FFI boundary. Optional in stack-encrypt (the `dynamic` feature). -vitaminc-aead-frame = "0.5.1" -vitaminc-encrypt = "0.5.1" -vitaminc-hmac = "0.5.1" -vitaminc-prf = "0.5.1" -vitaminc-protected = "0.5.1" - -# Temporary: every vitaminc crate this workspace uses comes from one commit -# of cipherstash/vitaminc#464, which is vitaminc main (ADR 0001 and -# vitaminc-ore) plus the order-term comparison ore.rs builds against, until -# vitaminc releases 0.6. Pinned by `rev`, never by branch: a branch is deleted -# when its stack merges. Replace this block with the released versions then. The Go guests are -# detached workspaces and carry the same block, so every build of these -# crates sees one vitaminc. -[patch.crates-io] -vitaminc = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } -vitaminc-aead = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } -vitaminc-aead-frame = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } -vitaminc-encrypt = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } -vitaminc-hmac = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } -vitaminc-prf = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } -vitaminc-protected = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } +vitaminc-aead-frame = "0.6.0" +vitaminc-encrypt = "0.6.0" +vitaminc-hmac = "0.6.0" +vitaminc-prf = "0.6.0" +vitaminc-protected = "0.6.0" diff --git a/languages/golang/auth/guest/Cargo.lock b/languages/golang/auth/guest/Cargo.lock index cff369660..58ae17c2a 100644 --- a/languages/golang/auth/guest/Cargo.lock +++ b/languages/golang/auth/guest/Cargo.lock @@ -419,7 +419,7 @@ dependencies = [ "url", "utoipa", "uuid", - "vitaminc", + "vitaminc 0.5.1", ] [[package]] @@ -1847,8 +1847,8 @@ dependencies = [ "tracing", "url", "uuid", - "vitaminc", - "vitaminc-protected", + "vitaminc 0.6.0", + "vitaminc-protected 0.6.0", "web-time", "zeroize", "zerokms-protocol", @@ -1867,7 +1867,7 @@ dependencies = [ "stack-profile", "tempfile", "url", - "vitaminc-protected", + "vitaminc-protected 0.6.0", "zeroize", ] @@ -1878,9 +1878,9 @@ dependencies = [ "miette", "serde_json", "thiserror 1.0.69", - "vitaminc-aead", + "vitaminc-aead 0.6.0", "vitaminc-aead-frame", - "vitaminc-protected", + "vitaminc-protected 0.6.0", "zeroize", ] @@ -2286,36 +2286,79 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vitaminc" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35042fb7a81d72dc43c53fec835e61027f18b4d01cc0f7e12d6c6a6ffbf3bfb9" +dependencies = [ + "vitaminc-aead 0.5.1", + "vitaminc-context 0.5.1", + "vitaminc-encrypt 0.5.1", + "vitaminc-protected 0.5.1", + "vitaminc-random 0.5.1", + "vitaminc-traits 0.5.1", +] + +[[package]] +name = "vitaminc" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0c6b8dff9bfc94e076fcd5496395958e6c8dddb27605b6504c54123512c7c8e" dependencies = [ - "vitaminc-aead", - "vitaminc-context", - "vitaminc-encrypt", - "vitaminc-protected", - "vitaminc-random", - "vitaminc-traits", + "vitaminc-aead 0.6.0", + "vitaminc-context 0.6.0", + "vitaminc-encrypt 0.6.0", + "vitaminc-protected 0.6.0", + "vitaminc-random 0.6.0", + "vitaminc-traits 0.6.0", ] [[package]] name = "vitaminc-aead" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62cacd4dae485e98cfe5407e919187a8468dd933ea4234be5115b7d13a30f3c6" +dependencies = [ + "bytes", + "serde", + "vitaminc-aead-derive 0.5.1", + "vitaminc-context 0.5.1", + "vitaminc-protected 0.5.1", + "vitaminc-random 0.5.1", + "zeroize", +] + +[[package]] +name = "vitaminc-aead" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcea687f745050d80df32c93fbe7f8d18ed2cc5fb5e029814477dfe31449ec2e" dependencies = [ "bytes", "futures-util", "serde", "thiserror 2.0.20", - "vitaminc-aead-derive", - "vitaminc-context", - "vitaminc-protected", - "vitaminc-random", + "vitaminc-aead-derive 0.6.0", + "vitaminc-context 0.6.0", + "vitaminc-protected 0.6.0", + "vitaminc-random 0.6.0", "zeroize", ] [[package]] name = "vitaminc-aead-derive" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ff34056b70a8417ad3bf9db73b2bd3be97f448794566c6de2080bd8a7cd9b6b" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "vitaminc-aead-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "085afc8c77ffadb4c8a93c9c5fa2d897196a36ab38e17c08fe96f8b9c309e05f" dependencies = [ "proc-macro2", "quote", @@ -2324,38 +2367,83 @@ dependencies = [ [[package]] name = "vitaminc-aead-frame" -version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c253055230469c4af4791e902c41ce0ce55069c512249b33cf041dd5c58c517" dependencies = [ - "vitaminc-aead", + "vitaminc-aead 0.6.0", ] [[package]] name = "vitaminc-context" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7df52501fe33cdb3ee9e155b5242fac184c037d147e58c96e5143ed011252a8" dependencies = [ "mutants", - "vitaminc-protected", + "vitaminc-protected 0.5.1", +] + +[[package]] +name = "vitaminc-context" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4d5679e511871da942ba2b29fcb1df198ee2359218bf7f11cfec4fd3dcfeee0" +dependencies = [ + "mutants", + "vitaminc-protected 0.6.0", ] [[package]] name = "vitaminc-encrypt" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbd0e1748055e381e02033e08af385de879c4dcf32e0f6739ac0c0cf0c6f13d" dependencies = [ "aes-gcm", "aws-lc-rs", - "vitaminc-aead", - "vitaminc-protected", - "vitaminc-random", + "vitaminc-aead 0.5.1", + "vitaminc-protected 0.5.1", + "vitaminc-random 0.5.1", + "zeroize", +] + +[[package]] +name = "vitaminc-encrypt" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "061d55dc4084fcc2eaa7334dd28300cd8d321038339a41085b7bf2dc6ccf5d4c" +dependencies = [ + "aes-gcm", + "aws-lc-rs", + "vitaminc-aead 0.6.0", + "vitaminc-protected 0.6.0", + "vitaminc-random 0.6.0", "zeroize", ] [[package]] name = "vitaminc-protected" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7b514f605a63f88874ba46ffea81775fad64994475ab239cb19da159e884b7b" +dependencies = [ + "bitvec", + "digest 0.11.3", + "libc", + "serde", + "serde_bytes", + "subtle", + "thiserror 2.0.20", + "vitaminc-protected-derive 0.5.1", + "zeroize", +] + +[[package]] +name = "vitaminc-protected" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efe97c67b61fd749b20d84248416a6cc1dc3f024a4695e3f69eee63aee7cb7c2" dependencies = [ "bitvec", "digest 0.11.3", @@ -2364,14 +2452,26 @@ dependencies = [ "serde_bytes", "subtle", "thiserror 2.0.20", - "vitaminc-protected-derive", + "vitaminc-protected-derive 0.6.0", "zeroize", ] [[package]] name = "vitaminc-protected-derive" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cb0a3d11afbe2c909f0cfc9996fe37fd89b1196eb0b8d89a66e28716360ee09" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "vitaminc-protected-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0483ecfcb3e874d45c9b2dc04d8e5ed55ff453023a9de98fddc4a24b315d9f6" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -2382,21 +2482,49 @@ dependencies = [ [[package]] name = "vitaminc-random" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7b1bfab35ee322de37269c6fd94d8cd94047a66f97cf6722e53e3321482167" dependencies = [ "chacha20", "getrandom 0.4.3", "rand 0.10.3", "thiserror 2.0.20", - "vitaminc-protected", - "vitaminc-random-derives", + "vitaminc-protected 0.5.1", + "vitaminc-random-derives 0.5.1", + "zeroize", +] + +[[package]] +name = "vitaminc-random" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693ae19994e6a79f9839e510220f532daab6f2ab0b4167fdd0d8379a027c653f" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand 0.10.3", + "thiserror 2.0.20", + "vitaminc-protected 0.6.0", + "vitaminc-random-derives 0.6.0", "zeroize", ] [[package]] name = "vitaminc-random-derives" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0915e2bfb417ce11e23123fb7ec122af5d27d8cfd22d6ad6a2cf7a1c2dbb4712" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "vitaminc-random-derives" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9ad6bb972f7e1b0889eba0a3ed534afe86dd8e52f197a2de20ebe25ee9e56" dependencies = [ "proc-macro2", "quote", @@ -2406,15 +2534,32 @@ dependencies = [ [[package]] name = "vitaminc-traits" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b31ec300e351a7a59e64899d0a2d7e7227cfa74ac0514f4e745acb2cc9335cee" +dependencies = [ + "anyhow", + "bytes", + "rmp-serde", + "serde", + "thiserror 2.0.20", + "vitaminc-protected 0.5.1", + "vitaminc-random 0.5.1", + "zeroize", +] + +[[package]] +name = "vitaminc-traits" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c9f86c930cb4f98eee9288f463a133f01774be039ba05a092f12148b45c430c" dependencies = [ "anyhow", "bytes", "rmp-serde", "serde", "thiserror 2.0.20", - "vitaminc-protected", - "vitaminc-random", + "vitaminc-protected 0.6.0", + "vitaminc-random 0.6.0", "zeroize", ] diff --git a/languages/golang/auth/guest/Cargo.toml b/languages/golang/auth/guest/Cargo.toml index 2d8a72380..df3ce1563 100644 --- a/languages/golang/auth/guest/Cargo.toml +++ b/languages/golang/auth/guest/Cargo.toml @@ -18,16 +18,6 @@ publish = false [workspace] -# Temporary, as in the root Cargo.toml: the vitaminc crates stack-guest-abi -# and the stack crates build on come from the same vitaminc commit -# (cipherstash/vitaminc#464) until vitaminc releases 0.6. -[patch.crates-io] -vitaminc = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } -vitaminc-aead = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } -vitaminc-aead-frame = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } -vitaminc-encrypt = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } -vitaminc-protected = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } - [lib] # cdylib: the .wasm guest module. rlib: lets the ops/status modules # unit-test natively (`cargo test` here, no wasm toolchain needed). @@ -58,7 +48,7 @@ zeroize = { version = "1", features = ["derive"] } serde_json = "1" tempfile = "3" # `Controlled::risky_ref`, to read a codec value back in the tests. -vitaminc-protected = "0.5.0" +vitaminc-protected = "0.6.0" [profile.release] # Smaller .wasm; the guest is IO-bound on the FFI copy and the file reads, diff --git a/languages/golang/encrypt/guest/Cargo.lock b/languages/golang/encrypt/guest/Cargo.lock index 63295b7c4..ebc293901 100644 --- a/languages/golang/encrypt/guest/Cargo.lock +++ b/languages/golang/encrypt/guest/Cargo.lock @@ -484,7 +484,7 @@ dependencies = [ "url", "utoipa", "uuid", - "vitaminc", + "vitaminc 0.5.1", ] [[package]] @@ -709,7 +709,7 @@ dependencies = [ "stack-encrypt", "thiserror 2.0.20", "ts-rs", - "vitaminc-aead", + "vitaminc-aead 0.6.0", "vitaminc-prf", "zeroize", ] @@ -2064,8 +2064,8 @@ dependencies = [ "tracing", "url", "uuid", - "vitaminc", - "vitaminc-protected", + "vitaminc 0.6.0", + "vitaminc-protected 0.6.0", "web-time", "zeroize", "zerokms-protocol", @@ -2084,11 +2084,11 @@ dependencies = [ "stack-kms", "thiserror 1.0.69", "uuid", - "vitaminc-aead", - "vitaminc-encrypt", + "vitaminc-aead 0.6.0", + "vitaminc-encrypt 0.6.0", "vitaminc-hmac", "vitaminc-prf", - "vitaminc-protected", + "vitaminc-protected 0.6.0", "zeroize", ] @@ -2118,7 +2118,7 @@ dependencies = [ "stack-kms", "uuid", "vitaminc-aead-frame", - "vitaminc-protected", + "vitaminc-protected 0.6.0", "zeroize", "zerokms-protocol", ] @@ -2130,9 +2130,9 @@ dependencies = [ "miette", "serde_json", "thiserror 1.0.69", - "vitaminc-aead", + "vitaminc-aead 0.6.0", "vitaminc-aead-frame", - "vitaminc-protected", + "vitaminc-protected 0.6.0", "zeroize", ] @@ -2158,8 +2158,8 @@ dependencies = [ "tracing", "url", "uuid", - "vitaminc", - "vitaminc-protected", + "vitaminc 0.6.0", + "vitaminc-protected 0.6.0", "zeroize", "zerokms-protocol", ] @@ -2609,36 +2609,79 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vitaminc" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35042fb7a81d72dc43c53fec835e61027f18b4d01cc0f7e12d6c6a6ffbf3bfb9" +dependencies = [ + "vitaminc-aead 0.5.1", + "vitaminc-context 0.5.1", + "vitaminc-encrypt 0.5.1", + "vitaminc-protected 0.5.1", + "vitaminc-random 0.5.1", + "vitaminc-traits 0.5.1", +] + +[[package]] +name = "vitaminc" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0c6b8dff9bfc94e076fcd5496395958e6c8dddb27605b6504c54123512c7c8e" dependencies = [ - "vitaminc-aead", - "vitaminc-context", - "vitaminc-encrypt", - "vitaminc-protected", - "vitaminc-random", - "vitaminc-traits", + "vitaminc-aead 0.6.0", + "vitaminc-context 0.6.0", + "vitaminc-encrypt 0.6.0", + "vitaminc-protected 0.6.0", + "vitaminc-random 0.6.0", + "vitaminc-traits 0.6.0", ] [[package]] name = "vitaminc-aead" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62cacd4dae485e98cfe5407e919187a8468dd933ea4234be5115b7d13a30f3c6" +dependencies = [ + "bytes", + "serde", + "vitaminc-aead-derive 0.5.1", + "vitaminc-context 0.5.1", + "vitaminc-protected 0.5.1", + "vitaminc-random 0.5.1", + "zeroize", +] + +[[package]] +name = "vitaminc-aead" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcea687f745050d80df32c93fbe7f8d18ed2cc5fb5e029814477dfe31449ec2e" dependencies = [ "bytes", "futures-util", "serde", "thiserror 2.0.20", - "vitaminc-aead-derive", - "vitaminc-context", - "vitaminc-protected", - "vitaminc-random", + "vitaminc-aead-derive 0.6.0", + "vitaminc-context 0.6.0", + "vitaminc-protected 0.6.0", + "vitaminc-random 0.6.0", "zeroize", ] [[package]] name = "vitaminc-aead-derive" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ff34056b70a8417ad3bf9db73b2bd3be97f448794566c6de2080bd8a7cd9b6b" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "vitaminc-aead-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "085afc8c77ffadb4c8a93c9c5fa2d897196a36ab38e17c08fe96f8b9c309e05f" dependencies = [ "proc-macro2", "quote", @@ -2647,61 +2690,108 @@ dependencies = [ [[package]] name = "vitaminc-aead-frame" -version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c253055230469c4af4791e902c41ce0ce55069c512249b33cf041dd5c58c517" dependencies = [ - "vitaminc-aead", + "vitaminc-aead 0.6.0", ] [[package]] name = "vitaminc-context" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7df52501fe33cdb3ee9e155b5242fac184c037d147e58c96e5143ed011252a8" +dependencies = [ + "mutants", + "vitaminc-protected 0.5.1", +] + +[[package]] +name = "vitaminc-context" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4d5679e511871da942ba2b29fcb1df198ee2359218bf7f11cfec4fd3dcfeee0" dependencies = [ "mutants", - "vitaminc-protected", + "vitaminc-protected 0.6.0", ] [[package]] name = "vitaminc-encrypt" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbd0e1748055e381e02033e08af385de879c4dcf32e0f6739ac0c0cf0c6f13d" dependencies = [ "aes-gcm", "aws-lc-rs", - "vitaminc-aead", - "vitaminc-protected", - "vitaminc-random", + "vitaminc-aead 0.5.1", + "vitaminc-protected 0.5.1", + "vitaminc-random 0.5.1", + "zeroize", +] + +[[package]] +name = "vitaminc-encrypt" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "061d55dc4084fcc2eaa7334dd28300cd8d321038339a41085b7bf2dc6ccf5d4c" +dependencies = [ + "aes-gcm", + "aws-lc-rs", + "vitaminc-aead 0.6.0", + "vitaminc-protected 0.6.0", + "vitaminc-random 0.6.0", "zeroize", ] [[package]] name = "vitaminc-hmac" -version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "393354b2348eabce0420626fdc7cf70ac779870f82abb566fb491ea19eeea066" dependencies = [ "hmac", "sha2 0.11.0", "vitaminc-prf", - "vitaminc-protected", + "vitaminc-protected 0.6.0", "zeroize", ] [[package]] name = "vitaminc-prf" -version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11de28e524ab1deecedbb023bd328916b6c023e2f52156521ac1df75a689d911" dependencies = [ "mutants", "thiserror 2.0.20", - "vitaminc-context", - "vitaminc-protected", + "vitaminc-context 0.6.0", + "vitaminc-protected 0.6.0", ] [[package]] name = "vitaminc-protected" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7b514f605a63f88874ba46ffea81775fad64994475ab239cb19da159e884b7b" +dependencies = [ + "bitvec", + "digest 0.11.3", + "libc", + "serde", + "serde_bytes", + "subtle", + "thiserror 2.0.20", + "vitaminc-protected-derive 0.5.1", + "zeroize", +] + +[[package]] +name = "vitaminc-protected" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efe97c67b61fd749b20d84248416a6cc1dc3f024a4695e3f69eee63aee7cb7c2" dependencies = [ "bitvec", "digest 0.11.3", @@ -2710,14 +2800,26 @@ dependencies = [ "serde_bytes", "subtle", "thiserror 2.0.20", - "vitaminc-protected-derive", + "vitaminc-protected-derive 0.6.0", "zeroize", ] [[package]] name = "vitaminc-protected-derive" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cb0a3d11afbe2c909f0cfc9996fe37fd89b1196eb0b8d89a66e28716360ee09" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "vitaminc-protected-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0483ecfcb3e874d45c9b2dc04d8e5ed55ff453023a9de98fddc4a24b315d9f6" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -2728,21 +2830,49 @@ dependencies = [ [[package]] name = "vitaminc-random" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7b1bfab35ee322de37269c6fd94d8cd94047a66f97cf6722e53e3321482167" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand 0.10.2", + "thiserror 2.0.20", + "vitaminc-protected 0.5.1", + "vitaminc-random-derives 0.5.1", + "zeroize", +] + +[[package]] +name = "vitaminc-random" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693ae19994e6a79f9839e510220f532daab6f2ab0b4167fdd0d8379a027c653f" dependencies = [ "chacha20", "getrandom 0.4.3", "rand 0.10.2", "thiserror 2.0.20", - "vitaminc-protected", - "vitaminc-random-derives", + "vitaminc-protected 0.6.0", + "vitaminc-random-derives 0.6.0", "zeroize", ] [[package]] name = "vitaminc-random-derives" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0915e2bfb417ce11e23123fb7ec122af5d27d8cfd22d6ad6a2cf7a1c2dbb4712" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "vitaminc-random-derives" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9ad6bb972f7e1b0889eba0a3ed534afe86dd8e52f197a2de20ebe25ee9e56" dependencies = [ "proc-macro2", "quote", @@ -2752,15 +2882,32 @@ dependencies = [ [[package]] name = "vitaminc-traits" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b31ec300e351a7a59e64899d0a2d7e7227cfa74ac0514f4e745acb2cc9335cee" +dependencies = [ + "anyhow", + "bytes", + "rmp-serde", + "serde", + "thiserror 2.0.20", + "vitaminc-protected 0.5.1", + "vitaminc-random 0.5.1", + "zeroize", +] + +[[package]] +name = "vitaminc-traits" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c9f86c930cb4f98eee9288f463a133f01774be039ba05a092f12148b45c430c" dependencies = [ "anyhow", "bytes", "rmp-serde", "serde", "thiserror 2.0.20", - "vitaminc-protected", - "vitaminc-random", + "vitaminc-protected 0.6.0", + "vitaminc-random 0.6.0", "zeroize", ] diff --git a/languages/golang/encrypt/guest/Cargo.toml b/languages/golang/encrypt/guest/Cargo.toml index bbe677570..69f72c4fd 100644 --- a/languages/golang/encrypt/guest/Cargo.toml +++ b/languages/golang/encrypt/guest/Cargo.toml @@ -17,18 +17,6 @@ publish = false [workspace] -# Temporary, as in the root Cargo.toml: the vitaminc crates this guest and -# stack-encrypt build on come from the same vitaminc commit -# (cipherstash/vitaminc#464) until vitaminc releases 0.6. -[patch.crates-io] -vitaminc = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } -vitaminc-aead = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } -vitaminc-aead-frame = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } -vitaminc-encrypt = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } -vitaminc-hmac = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } -vitaminc-prf = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } -vitaminc-protected = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } - [lib] # cdylib: the .wasm guest module. rlib: lets the ops/config/status modules # unit-test natively (`cargo test` here, no wasm toolchain needed). @@ -78,8 +66,8 @@ zerokms-protocol = "=0.12.31" # against (see the comment in packages/stack-encrypt/Cargo.toml). A # different version here fails at the `FfiValue: Decrypt` bound, since the # aead crate would be duplicated. -vitaminc-aead-frame = "0.5.1" -vitaminc-protected = "0.5.1" +vitaminc-aead-frame = "0.6.0" +vitaminc-protected = "0.6.0" futures = { version = "0.3", default-features = false, features = ["executor"] } serde = "1" diff --git a/languages/golang/go.mod b/languages/golang/go.mod index c865c7b02..0d4e06ecd 100644 --- a/languages/golang/go.mod +++ b/languages/golang/go.mod @@ -3,8 +3,8 @@ module github.com/cipherstash/stack/languages/golang go 1.26.0 require ( - github.com/cipherstash/vitaminc/bindings/go/vcffi v0.0.0-20261016121549-2413bd0e4bf7 - github.com/cipherstash/vitaminc/bindings/go/vcvalue v0.0.0-20261016121549-2413bd0e4bf7 + github.com/cipherstash/vitaminc/bindings/go/vcffi v0.0.0-20261017042657-ca2298d3e7e8 + github.com/cipherstash/vitaminc/bindings/go/vcvalue v0.0.0-20261017042657-ca2298d3e7e8 github.com/tetratelabs/wazero v1.12.0 golang.org/x/oauth2 v0.37.0 ) diff --git a/languages/golang/go.sum b/languages/golang/go.sum index ca8fa712d..c2e15302b 100644 --- a/languages/golang/go.sum +++ b/languages/golang/go.sum @@ -1,7 +1,7 @@ -github.com/cipherstash/vitaminc/bindings/go/vcffi v0.0.0-20261016121549-2413bd0e4bf7 h1:yw1oKlra5txMgE1u2LPiFBfa68IaEeSgLkLyDm6POqY= -github.com/cipherstash/vitaminc/bindings/go/vcffi v0.0.0-20261016121549-2413bd0e4bf7/go.mod h1:6jpaqAo6f7rjWuxoti0Ymoi9DQZyxpnieWVudhXv38Y= -github.com/cipherstash/vitaminc/bindings/go/vcvalue v0.0.0-20261016121549-2413bd0e4bf7 h1:e1YWsXn1jPEt+mfELQo9loKvM7yeSicPw1ZmBuQc7JQ= -github.com/cipherstash/vitaminc/bindings/go/vcvalue v0.0.0-20261016121549-2413bd0e4bf7/go.mod h1:RJODA1DCSm4H+1pbmCzDdT2pAIkxhinwIu6ewjh5sPs= +github.com/cipherstash/vitaminc/bindings/go/vcffi v0.0.0-20261017042657-ca2298d3e7e8 h1:kXpXvBsE5P35aw8yN8l2k3ARuoPdjQkXvxocg4D2WUc= +github.com/cipherstash/vitaminc/bindings/go/vcffi v0.0.0-20261017042657-ca2298d3e7e8/go.mod h1:6jpaqAo6f7rjWuxoti0Ymoi9DQZyxpnieWVudhXv38Y= +github.com/cipherstash/vitaminc/bindings/go/vcvalue v0.0.0-20261017042657-ca2298d3e7e8 h1:72kmUdUUZENerHctDhFMD31/mWtE80cRbpjLPZOeLCs= +github.com/cipherstash/vitaminc/bindings/go/vcvalue v0.0.0-20261017042657-ca2298d3e7e8/go.mod h1:RJODA1DCSm4H+1pbmCzDdT2pAIkxhinwIu6ewjh5sPs= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU= diff --git a/packages/eql/Cargo.lock b/packages/eql/Cargo.lock index 173b22612..381490a3a 100644 --- a/packages/eql/Cargo.lock +++ b/packages/eql/Cargo.lock @@ -1204,7 +1204,7 @@ dependencies = [ "stack-encrypt", "thiserror 2.0.20", "ts-rs", - "vitaminc-aead 0.5.1", + "vitaminc-aead 0.6.0", "vitaminc-prf", "zeroize", ] @@ -1246,7 +1246,7 @@ dependencies = [ "stack-kms", "tokio", "uuid", - "vitaminc-aead 0.5.1", + "vitaminc-aead 0.6.0", ] [[package]] @@ -4219,8 +4219,8 @@ dependencies = [ "tracing", "url", "uuid", - "vitaminc 0.5.1", - "vitaminc-protected 0.5.1", + "vitaminc 0.6.0", + "vitaminc-protected 0.6.0", "web-time", "zeroize", "zerokms-protocol", @@ -4239,11 +4239,11 @@ dependencies = [ "stack-kms", "thiserror 1.0.69", "uuid", - "vitaminc-aead 0.5.1", - "vitaminc-encrypt 0.5.1", + "vitaminc-aead 0.6.0", + "vitaminc-encrypt 0.6.0", "vitaminc-hmac", "vitaminc-prf", - "vitaminc-protected 0.5.1", + "vitaminc-protected 0.6.0", "zeroize", ] @@ -4278,8 +4278,8 @@ dependencies = [ "tracing", "url", "uuid", - "vitaminc 0.5.1", - "vitaminc-protected 0.5.1", + "vitaminc 0.6.0", + "vitaminc-protected 0.6.0", "zeroize", "zerokms-protocol", ] @@ -5031,16 +5031,31 @@ dependencies = [ [[package]] name = "vitaminc" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35042fb7a81d72dc43c53fec835e61027f18b4d01cc0f7e12d6c6a6ffbf3bfb9" dependencies = [ "vitaminc-aead 0.5.1", - "vitaminc-context", + "vitaminc-context 0.5.1", "vitaminc-encrypt 0.5.1", "vitaminc-protected 0.5.1", "vitaminc-random 0.5.1", "vitaminc-traits 0.5.1", ] +[[package]] +name = "vitaminc" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0c6b8dff9bfc94e076fcd5496395958e6c8dddb27605b6504c54123512c7c8e" +dependencies = [ + "vitaminc-aead 0.6.0", + "vitaminc-context 0.6.0", + "vitaminc-encrypt 0.6.0", + "vitaminc-protected 0.6.0", + "vitaminc-random 0.6.0", + "vitaminc-traits 0.6.0", +] + [[package]] name = "vitaminc-aead" version = "0.2.0-pre" @@ -5057,23 +5072,51 @@ dependencies = [ [[package]] name = "vitaminc-aead" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62cacd4dae485e98cfe5407e919187a8468dd933ea4234be5115b7d13a30f3c6" dependencies = [ "bytes", - "futures-util", "serde", - "thiserror 2.0.20", - "vitaminc-aead-derive", - "vitaminc-context", + "vitaminc-aead-derive 0.5.1", + "vitaminc-context 0.5.1", "vitaminc-protected 0.5.1", "vitaminc-random 0.5.1", "zeroize", ] +[[package]] +name = "vitaminc-aead" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcea687f745050d80df32c93fbe7f8d18ed2cc5fb5e029814477dfe31449ec2e" +dependencies = [ + "bytes", + "futures-util", + "serde", + "thiserror 2.0.20", + "vitaminc-aead-derive 0.6.0", + "vitaminc-context 0.6.0", + "vitaminc-protected 0.6.0", + "vitaminc-random 0.6.0", + "zeroize", +] + [[package]] name = "vitaminc-aead-derive" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ff34056b70a8417ad3bf9db73b2bd3be97f448794566c6de2080bd8a7cd9b6b" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "vitaminc-aead-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "085afc8c77ffadb4c8a93c9c5fa2d897196a36ab38e17c08fe96f8b9c309e05f" dependencies = [ "proc-macro2", "quote", @@ -5083,12 +5126,23 @@ dependencies = [ [[package]] name = "vitaminc-context" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7df52501fe33cdb3ee9e155b5242fac184c037d147e58c96e5143ed011252a8" dependencies = [ "mutants", "vitaminc-protected 0.5.1", ] +[[package]] +name = "vitaminc-context" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4d5679e511871da942ba2b29fcb1df198ee2359218bf7f11cfec4fd3dcfeee0" +dependencies = [ + "mutants", + "vitaminc-protected 0.6.0", +] + [[package]] name = "vitaminc-encrypt" version = "0.2.0-pre" @@ -5106,7 +5160,8 @@ dependencies = [ [[package]] name = "vitaminc-encrypt" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbd0e1748055e381e02033e08af385de879c4dcf32e0f6739ac0c0cf0c6f13d" dependencies = [ "aes-gcm", "aws-lc-rs", @@ -5116,27 +5171,43 @@ dependencies = [ "zeroize", ] +[[package]] +name = "vitaminc-encrypt" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "061d55dc4084fcc2eaa7334dd28300cd8d321038339a41085b7bf2dc6ccf5d4c" +dependencies = [ + "aes-gcm", + "aws-lc-rs", + "vitaminc-aead 0.6.0", + "vitaminc-protected 0.6.0", + "vitaminc-random 0.6.0", + "zeroize", +] + [[package]] name = "vitaminc-hmac" -version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "393354b2348eabce0420626fdc7cf70ac779870f82abb566fb491ea19eeea066" dependencies = [ "hmac 0.13.0", "sha2 0.11.0", "vitaminc-prf", - "vitaminc-protected 0.5.1", + "vitaminc-protected 0.6.0", "zeroize", ] [[package]] name = "vitaminc-prf" -version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11de28e524ab1deecedbb023bd328916b6c023e2f52156521ac1df75a689d911" dependencies = [ "mutants", "thiserror 2.0.20", - "vitaminc-context", - "vitaminc-protected 0.5.1", + "vitaminc-context 0.6.0", + "vitaminc-protected 0.6.0", ] [[package]] @@ -5157,7 +5228,8 @@ dependencies = [ [[package]] name = "vitaminc-protected" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7b514f605a63f88874ba46ffea81775fad64994475ab239cb19da159e884b7b" dependencies = [ "bitvec", "digest 0.11.3", @@ -5170,6 +5242,23 @@ dependencies = [ "zeroize", ] +[[package]] +name = "vitaminc-protected" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efe97c67b61fd749b20d84248416a6cc1dc3f024a4695e3f69eee63aee7cb7c2" +dependencies = [ + "bitvec", + "digest 0.11.3", + "libc", + "serde", + "serde_bytes", + "subtle", + "thiserror 2.0.20", + "vitaminc-protected-derive 0.6.0", + "zeroize", +] + [[package]] name = "vitaminc-protected-derive" version = "0.2.0-pre" @@ -5184,7 +5273,19 @@ dependencies = [ [[package]] name = "vitaminc-protected-derive" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cb0a3d11afbe2c909f0cfc9996fe37fd89b1196eb0b8d89a66e28716360ee09" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "vitaminc-protected-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0483ecfcb3e874d45c9b2dc04d8e5ed55ff453023a9de98fddc4a24b315d9f6" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -5209,7 +5310,8 @@ dependencies = [ [[package]] name = "vitaminc-random" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7b1bfab35ee322de37269c6fd94d8cd94047a66f97cf6722e53e3321482167" dependencies = [ "chacha20", "getrandom 0.4.2", @@ -5220,6 +5322,21 @@ dependencies = [ "zeroize", ] +[[package]] +name = "vitaminc-random" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693ae19994e6a79f9839e510220f532daab6f2ab0b4167fdd0d8379a027c653f" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand 0.10.1", + "thiserror 2.0.20", + "vitaminc-protected 0.6.0", + "vitaminc-random-derives 0.6.0", + "zeroize", +] + [[package]] name = "vitaminc-random-derives" version = "0.2.0-pre" @@ -5234,7 +5351,19 @@ dependencies = [ [[package]] name = "vitaminc-random-derives" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0915e2bfb417ce11e23123fb7ec122af5d27d8cfd22d6ad6a2cf7a1c2dbb4712" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "vitaminc-random-derives" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9ad6bb972f7e1b0889eba0a3ed534afe86dd8e52f197a2de20ebe25ee9e56" dependencies = [ "proc-macro2", "quote", @@ -5260,7 +5389,8 @@ dependencies = [ [[package]] name = "vitaminc-traits" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b31ec300e351a7a59e64899d0a2d7e7227cfa74ac0514f4e745acb2cc9335cee" dependencies = [ "anyhow", "bytes", @@ -5272,6 +5402,22 @@ dependencies = [ "zeroize", ] +[[package]] +name = "vitaminc-traits" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c9f86c930cb4f98eee9288f463a133f01774be039ba05a092f12148b45c430c" +dependencies = [ + "anyhow", + "bytes", + "rmp-serde", + "serde", + "thiserror 2.0.20", + "vitaminc-protected 0.6.0", + "vitaminc-random 0.6.0", + "zeroize", +] + [[package]] name = "vsimd" version = "0.8.0" diff --git a/packages/eql/Cargo.toml b/packages/eql/Cargo.toml index a6e266d4f..ab1492558 100644 --- a/packages/eql/Cargo.toml +++ b/packages/eql/Cargo.toml @@ -35,15 +35,3 @@ default-members = ["tests/sqlx"] [workspace.lints.rust] dead_code = "deny" unused_imports = "deny" - -# Temporary, as in the repository root's Cargo.toml: eql-bindings' -# `stack-encrypt` feature and tests/encryption compile stack-encrypt by path, -# so the vitaminc crates it builds on come from the same vitaminc commit -# (cipherstash/vitaminc#464) until vitaminc releases 0.6. -[patch.crates-io] -vitaminc = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } -vitaminc-aead = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } -vitaminc-encrypt = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } -vitaminc-hmac = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } -vitaminc-prf = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } -vitaminc-protected = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } diff --git a/packages/eql/crates/eql-bindings/Cargo.toml b/packages/eql/crates/eql-bindings/Cargo.toml index 62b400896..93133bf96 100644 --- a/packages/eql/crates/eql-bindings/Cargo.toml +++ b/packages/eql/crates/eql-bindings/Cargo.toml @@ -51,11 +51,11 @@ stack-encrypt = { path = "../../../stack-encrypt", version = "0.2.0", optional = # crate directly (stack-encrypt re-exports only `IntoPrfContext` and # `PrfContext`), and two vitaminc lines in one graph do not interoperate, so # the requirement is stack-encrypt's. -vitaminc-prf = { version = "0.5.0", optional = true } +vitaminc-prf = { version = "0.6.0", optional = true } # `Physical`, `Int`, `Float` and `FromScalar`: the scalar plaintexts in # `encryption::plaintext` describe themselves through vitaminc's `Encrypt` # and read back only their own width through `FromScalar`. -vitaminc-aead = { version = "0.5.1", optional = true } +vitaminc-aead = { version = "0.6.0", optional = true } # The plaintext a target reads out of its `Protected` runtime value is held # in `Zeroizing` until it is sealed, so the copy is wiped like the original. zeroize = { version = "1", optional = true } diff --git a/packages/eql/tests/encryption/Cargo.toml b/packages/eql/tests/encryption/Cargo.toml index 2ce463745..d7d5c6576 100644 --- a/packages/eql/tests/encryption/Cargo.toml +++ b/packages/eql/tests/encryption/Cargo.toml @@ -21,7 +21,7 @@ eql-domains = { path = "../../crates/eql-domains" } # as eql-bindings names it. stack-encrypt = { path = "../../../stack-encrypt", default-features = false, features = ["dynamic"] } # `plaintext::Sink`, for a plaintext type with its own `Encrypt` impl. -vitaminc-aead = "0.5.1" +vitaminc-aead = "0.6.0" stack-kms = { path = "../../../stack-kms", default-features = false, features = ["test-support"] } serde = "1" serde_json = "1" diff --git a/packages/stack-encrypt/CHANGELOG.md b/packages/stack-encrypt/CHANGELOG.md index 5a5b5c6ca..6fab532c5 100644 --- a/packages/stack-encrypt/CHANGELOG.md +++ b/packages/stack-encrypt/CHANGELOG.md @@ -142,6 +142,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 same way. Storing the empty filter tells a reader of the database that the value is empty, separator-only or shorter than the n-gram length (ADR-0009). +- **stack-encrypt requires the vitaminc 0.6 crates.** They are the first + release of vitaminc ADR 0001, which the entries below build on. A crate + that names a vitaminc type stack-encrypt takes or returns (`Encrypt`, + `Decrypt`, `Protected`) moves to 0.6 with it. - **Values describe themselves with vitaminc's new `Encrypt` and read back with `Decrypt` (vitaminc ADR 0001).** `KeysetCipher::encrypt`, `StackCipher::decrypt` and every target take the new `Encrypt`, a source diff --git a/packages/stack-encrypt/Cargo.toml b/packages/stack-encrypt/Cargo.toml index a046919a4..b1124f27d 100644 --- a/packages/stack-encrypt/Cargo.toml +++ b/packages/stack-encrypt/Cargo.toml @@ -24,7 +24,7 @@ stack-auth = { workspace = true, optional = true } # `#[derive(EncryptFrom)]` / `#[derive(DecryptInto)]`, re-exported from `target`. stack-encrypt-derive = { path = "../stack-encrypt-derive", version = "0.2.0" } -# The workspace vitaminc (0.5.1): one canonical context encoding shared by +# The workspace vitaminc (0.6.0): one canonical context encoding shared by # the AEAD and PRF derivations (`IntoContext`, cipherstash/vitaminc#339), # `NonEmpty::with`, `From<integer> for NonEmpty` (#314) and the parts view # (#318), all of which this crate relies on. All five must share one diff --git a/packages/stack-encrypt/fuzz/Cargo.lock b/packages/stack-encrypt/fuzz/Cargo.lock index d0c479f1e..881a949a2 100644 --- a/packages/stack-encrypt/fuzz/Cargo.lock +++ b/packages/stack-encrypt/fuzz/Cargo.lock @@ -500,7 +500,7 @@ dependencies = [ "url", "utoipa", "uuid", - "vitaminc", + "vitaminc 0.5.1", ] [[package]] @@ -2021,8 +2021,8 @@ dependencies = [ "tracing", "url", "uuid", - "vitaminc", - "vitaminc-protected", + "vitaminc 0.6.0", + "vitaminc-protected 0.6.0", "web-time", "zeroize", "zerokms-protocol", @@ -2041,11 +2041,11 @@ dependencies = [ "stack-kms", "thiserror 1.0.69", "uuid", - "vitaminc-aead", - "vitaminc-encrypt", + "vitaminc-aead 0.6.0", + "vitaminc-encrypt 0.6.0", "vitaminc-hmac", "vitaminc-prf", - "vitaminc-protected", + "vitaminc-protected 0.6.0", "zeroize", ] @@ -2066,7 +2066,7 @@ dependencies = [ "libfuzzer-sys", "stack-encrypt", "uuid", - "vitaminc-protected", + "vitaminc-protected 0.6.0", ] [[package]] @@ -2091,8 +2091,8 @@ dependencies = [ "tracing", "url", "uuid", - "vitaminc", - "vitaminc-protected", + "vitaminc 0.6.0", + "vitaminc-protected 0.6.0", "zeroize", "zerokms-protocol", ] @@ -2512,36 +2512,79 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vitaminc" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35042fb7a81d72dc43c53fec835e61027f18b4d01cc0f7e12d6c6a6ffbf3bfb9" +dependencies = [ + "vitaminc-aead 0.5.1", + "vitaminc-context 0.5.1", + "vitaminc-encrypt 0.5.1", + "vitaminc-protected 0.5.1", + "vitaminc-random 0.5.1", + "vitaminc-traits 0.5.1", +] + +[[package]] +name = "vitaminc" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0c6b8dff9bfc94e076fcd5496395958e6c8dddb27605b6504c54123512c7c8e" dependencies = [ - "vitaminc-aead", - "vitaminc-context", - "vitaminc-encrypt", - "vitaminc-protected", - "vitaminc-random", - "vitaminc-traits", + "vitaminc-aead 0.6.0", + "vitaminc-context 0.6.0", + "vitaminc-encrypt 0.6.0", + "vitaminc-protected 0.6.0", + "vitaminc-random 0.6.0", + "vitaminc-traits 0.6.0", ] [[package]] name = "vitaminc-aead" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62cacd4dae485e98cfe5407e919187a8468dd933ea4234be5115b7d13a30f3c6" +dependencies = [ + "bytes", + "serde", + "vitaminc-aead-derive 0.5.1", + "vitaminc-context 0.5.1", + "vitaminc-protected 0.5.1", + "vitaminc-random 0.5.1", + "zeroize", +] + +[[package]] +name = "vitaminc-aead" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcea687f745050d80df32c93fbe7f8d18ed2cc5fb5e029814477dfe31449ec2e" dependencies = [ "bytes", "futures-util", "serde", "thiserror 2.0.18", - "vitaminc-aead-derive", - "vitaminc-context", - "vitaminc-protected", - "vitaminc-random", + "vitaminc-aead-derive 0.6.0", + "vitaminc-context 0.6.0", + "vitaminc-protected 0.6.0", + "vitaminc-random 0.6.0", "zeroize", ] [[package]] name = "vitaminc-aead-derive" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ff34056b70a8417ad3bf9db73b2bd3be97f448794566c6de2080bd8a7cd9b6b" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "vitaminc-aead-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "085afc8c77ffadb4c8a93c9c5fa2d897196a36ab38e17c08fe96f8b9c309e05f" dependencies = [ "proc-macro2", "quote", @@ -2551,52 +2594,81 @@ dependencies = [ [[package]] name = "vitaminc-context" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7df52501fe33cdb3ee9e155b5242fac184c037d147e58c96e5143ed011252a8" dependencies = [ "mutants", - "vitaminc-protected", + "vitaminc-protected 0.5.1", +] + +[[package]] +name = "vitaminc-context" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4d5679e511871da942ba2b29fcb1df198ee2359218bf7f11cfec4fd3dcfeee0" +dependencies = [ + "mutants", + "vitaminc-protected 0.6.0", ] [[package]] name = "vitaminc-encrypt" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbd0e1748055e381e02033e08af385de879c4dcf32e0f6739ac0c0cf0c6f13d" +dependencies = [ + "aes-gcm", + "aws-lc-rs", + "vitaminc-aead 0.5.1", + "vitaminc-protected 0.5.1", + "vitaminc-random 0.5.1", + "zeroize", +] + +[[package]] +name = "vitaminc-encrypt" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "061d55dc4084fcc2eaa7334dd28300cd8d321038339a41085b7bf2dc6ccf5d4c" dependencies = [ "aes-gcm", "aws-lc-rs", - "vitaminc-aead", - "vitaminc-protected", - "vitaminc-random", + "vitaminc-aead 0.6.0", + "vitaminc-protected 0.6.0", + "vitaminc-random 0.6.0", "zeroize", ] [[package]] name = "vitaminc-hmac" -version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "393354b2348eabce0420626fdc7cf70ac779870f82abb566fb491ea19eeea066" dependencies = [ "hmac", "sha2 0.11.0", "vitaminc-prf", - "vitaminc-protected", + "vitaminc-protected 0.6.0", "zeroize", ] [[package]] name = "vitaminc-prf" -version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11de28e524ab1deecedbb023bd328916b6c023e2f52156521ac1df75a689d911" dependencies = [ "mutants", "thiserror 2.0.18", - "vitaminc-context", - "vitaminc-protected", + "vitaminc-context 0.6.0", + "vitaminc-protected 0.6.0", ] [[package]] name = "vitaminc-protected" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7b514f605a63f88874ba46ffea81775fad64994475ab239cb19da159e884b7b" dependencies = [ "bitvec", "digest 0.11.3", @@ -2605,14 +2677,43 @@ dependencies = [ "serde_bytes", "subtle", "thiserror 2.0.18", - "vitaminc-protected-derive", + "vitaminc-protected-derive 0.5.1", + "zeroize", +] + +[[package]] +name = "vitaminc-protected" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efe97c67b61fd749b20d84248416a6cc1dc3f024a4695e3f69eee63aee7cb7c2" +dependencies = [ + "bitvec", + "digest 0.11.3", + "libc", + "serde", + "serde_bytes", + "subtle", + "thiserror 2.0.18", + "vitaminc-protected-derive 0.6.0", "zeroize", ] [[package]] name = "vitaminc-protected-derive" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cb0a3d11afbe2c909f0cfc9996fe37fd89b1196eb0b8d89a66e28716360ee09" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "vitaminc-protected-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0483ecfcb3e874d45c9b2dc04d8e5ed55ff453023a9de98fddc4a24b315d9f6" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -2623,21 +2724,49 @@ dependencies = [ [[package]] name = "vitaminc-random" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7b1bfab35ee322de37269c6fd94d8cd94047a66f97cf6722e53e3321482167" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand 0.10.1", + "thiserror 2.0.18", + "vitaminc-protected 0.5.1", + "vitaminc-random-derives 0.5.1", + "zeroize", +] + +[[package]] +name = "vitaminc-random" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693ae19994e6a79f9839e510220f532daab6f2ab0b4167fdd0d8379a027c653f" dependencies = [ "chacha20", "getrandom 0.4.2", "rand 0.10.1", "thiserror 2.0.18", - "vitaminc-protected", - "vitaminc-random-derives", + "vitaminc-protected 0.6.0", + "vitaminc-random-derives 0.6.0", "zeroize", ] [[package]] name = "vitaminc-random-derives" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0915e2bfb417ce11e23123fb7ec122af5d27d8cfd22d6ad6a2cf7a1c2dbb4712" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "vitaminc-random-derives" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9ad6bb972f7e1b0889eba0a3ed534afe86dd8e52f197a2de20ebe25ee9e56" dependencies = [ "proc-macro2", "quote", @@ -2647,15 +2776,32 @@ dependencies = [ [[package]] name = "vitaminc-traits" version = "0.5.1" -source = "git+https://github.com/cipherstash/vitaminc?rev=2413bd0e4bf7eb8932a31101c635e2373c1096c6#2413bd0e4bf7eb8932a31101c635e2373c1096c6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b31ec300e351a7a59e64899d0a2d7e7227cfa74ac0514f4e745acb2cc9335cee" +dependencies = [ + "anyhow", + "bytes", + "rmp-serde", + "serde", + "thiserror 2.0.18", + "vitaminc-protected 0.5.1", + "vitaminc-random 0.5.1", + "zeroize", +] + +[[package]] +name = "vitaminc-traits" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c9f86c930cb4f98eee9288f463a133f01774be039ba05a092f12148b45c430c" dependencies = [ "anyhow", "bytes", "rmp-serde", "serde", "thiserror 2.0.18", - "vitaminc-protected", - "vitaminc-random", + "vitaminc-protected 0.6.0", + "vitaminc-random 0.6.0", "zeroize", ] diff --git a/packages/stack-encrypt/fuzz/Cargo.toml b/packages/stack-encrypt/fuzz/Cargo.toml index 48683a13f..63378e06c 100644 --- a/packages/stack-encrypt/fuzz/Cargo.toml +++ b/packages/stack-encrypt/fuzz/Cargo.toml @@ -17,7 +17,7 @@ cargo-fuzz = true [dependencies] # `Protected`, for the check_record model's passthrough-of-bytes node: the # same line stack-encrypt builds on. -vitaminc-protected = "0.5.1" +vitaminc-protected = "0.6.0" libfuzzer-sys = "0.4" # Structure-aware inputs for `check_record` and `plan_build`: the mirror # types derive `Arbitrary`, so the fuzzer mutates trees and plans, not bytes. @@ -63,14 +63,3 @@ bench = false [workspace] resolver = "2" - -# Temporary, as in the root Cargo.toml: the vitaminc crates stack-encrypt -# builds on come from the same vitaminc commit (cipherstash/vitaminc#464) -# until vitaminc releases 0.6. -[patch.crates-io] -vitaminc = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } -vitaminc-aead = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } -vitaminc-encrypt = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } -vitaminc-hmac = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } -vitaminc-prf = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } -vitaminc-protected = { git = "https://github.com/cipherstash/vitaminc", rev = "2413bd0e4bf7eb8932a31101c635e2373c1096c6" } diff --git a/packages/stack-encrypt/tests/ui/plaintext_needs_encrypt.stderr b/packages/stack-encrypt/tests/ui/plaintext_needs_encrypt.stderr index d8e66cd28..e08fd10d4 100644 --- a/packages/stack-encrypt/tests/ui/plaintext_needs_encrypt.stderr +++ b/packages/stack-encrypt/tests/ui/plaintext_needs_encrypt.stderr @@ -11,6 +11,17 @@ help: the trait `stack_encrypt::Encrypt` is not implemented for `SerdeOnly` | 4 | struct SerdeOnly { value: String } | ^^^^^^^^^^^^^^^^ +note: there are multiple different versions of crate `vitaminc_aead` in the dependency graph + --> $CARGO/vitaminc-aead-$VERSION/src/plaintext/mod.rs + | + | pub trait Encrypt { + | ^^^^^^^^^^^^^^^^^ this is the expected trait + | + ::: $CARGO/vitaminc-aead-$VERSION/src/encrypt/mod.rs + | + | pub trait Encrypt { + | ----------------- this is the trait that was imported + = help: you can use `cargo tree` to explore your dependency tree = help: the following other types implement trait `stack_encrypt::Encrypt`: &&'b T &'a BTreeMap<K, V> diff --git a/scripts/package-git-pinned-crates.sh b/scripts/package-git-pinned-crates.sh deleted file mode 100755 index ab48882ae..000000000 --- a/scripts/package-git-pinned-crates.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash -# Package crates for crates.io WITHOUT the registry build, for a graph that -# pins a dependency to git through `[patch.crates-io]`. -# -# PINS="<git crates, one per line, from cargo tree>" \ -# scripts/package-git-pinned-crates.sh <title> [cargo package flags...] -- <crate>... -# -# `cargo package` drops `[patch]`, so a verify would compile against the -# registry's version of the pinned crate, a build that tests a dependency the -# tree does not use. This packages with `--no-verify`, which still checks each -# crate's metadata and that every path dependency names a version. -# -# Packaging still resolves every dependency against crates.io. A crate whose -# graph needs a pinned crate that has NO release there yet (vitaminc-normalize -# before vitaminc 0.6) cannot be packaged at all; that crate is left out with a -# warning and the rest are packaged without it. Any other failure fails the -# step. None of this weakens the release: `cargo publish` verifies against the -# registry and refuses these crates until the pins are registry versions. -set -euo pipefail - -title="$1" -shift -flags=() -while [ "$#" -gt 0 ] && [ "$1" != "--" ]; do - flags+=("$1") - shift -done -[ "$#" -gt 0 ] || { echo "usage: $0 <title> [flags...] -- <crate>..." >&2; exit 2; } -shift -remaining=("$@") - -pinned_names="$(awk '{print $1}' <<<"${PINS:?PINS must list the git-pinned crates}" | sort -u)" -printf 'Built against git, so not verifiable against the registry:\n%s\n' "$PINS" -echo "::warning title=${title} not verified against the registry::They build against $(wc -l <<<"$PINS" | tr -d ' ') crate(s) pinned to git ([patch.crates-io]), which cargo package ignores, so the registry build was skipped and only the packaging was checked. The publish refuses them until the pins are registry versions." - -while [ "${#remaining[@]}" -gt 0 ]; do - args=() - for crate in "${remaining[@]}"; do - args+=(-p "$crate") - done - status=0 - out="$(cargo package --locked --no-verify --allow-dirty "${flags[@]}" "${args[@]}" 2>&1)" || status=$? - printf '%s\n' "$out" - [ "$status" -eq 0 ] && exit 0 - - missing="$(sed -nE 's/.*no matching package named `([^`]+)` found.*/\1/p' <<<"$out" | head -n 1)" - needer="$(sed -nE 's/.*required by package `([^ ]+) v.*/\1/p' <<<"$out" | head -n 1)" - if [ -z "$missing" ] || [ -z "$needer" ] || ! grep -qxF "$missing" <<<"$pinned_names"; then - echo "::error title=${title} failed to package::cargo package failed for a reason other than an unreleased git-pinned dependency; see the log above." - exit "$status" - fi - kept=() - for crate in "${remaining[@]}"; do - [ "$crate" = "$needer" ] || kept+=("$crate") - done - if [ "${#kept[@]}" -eq "${#remaining[@]}" ]; then - echo "::error title=${title} failed to package::${needer} needs ${missing}, which has no crates.io release, but ${needer} is not one of the crates being packaged." - exit "$status" - fi - echo "::warning title=${needer} not packaged::${needer} needs ${missing}, pinned to git and not yet released on crates.io, so it cannot be packaged until that release." - remaining=("${kept[@]+"${kept[@]}"}") -done From b6ee8a3ec52250a1ad37be4257d0e16f1677c01b Mon Sep 17 00:00:00 2001 From: Dan Draper <dan@cipherstash.com> Date: Sat, 17 Oct 2026 16:33:30 +1100 Subject: [PATCH 15/15] build(stack-encrypt)!: version stack-encrypt and stack-encrypt-derive 0.3.0 stack-encrypt's API is no longer 0.2.0's (vitaminc ADR 0001 and what this stack builds on it), but its manifest still said 0.2.0, which crates.io has. eql-bindings names stack-encrypt by path and version, so the packaged eql-bindings resolved the published 0.2.0 and failed to compile against it: test-eql.yml's "Verify eql-bindings packages cleanly for crates.io" step, whose registry build the git pin had been skipping. The version group moves to 0.3.0 by hand, as a stack-* version does, and eql-bindings and stack-encrypt name it. Until 0.3.0 is on crates.io the eql-bindings step warns that it cannot build against the registry, which is its documented answer for a stack-encrypt not yet published. --- Cargo.lock | 4 ++-- languages/golang/encrypt/guest/Cargo.lock | 4 ++-- packages/eql/Cargo.lock | 4 ++-- packages/eql/crates/eql-bindings/Cargo.toml | 2 +- packages/stack-encrypt-derive/Cargo.toml | 2 +- packages/stack-encrypt/Cargo.toml | 4 ++-- packages/stack-encrypt/fuzz/Cargo.lock | 4 ++-- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5f523afe0..3891a1ce9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3284,7 +3284,7 @@ dependencies = [ [[package]] name = "stack-encrypt" -version = "0.2.0" +version = "0.3.0" dependencies = [ "base64ct", "cllw-ore", @@ -3309,7 +3309,7 @@ dependencies = [ [[package]] name = "stack-encrypt-derive" -version = "0.2.0" +version = "0.3.0" dependencies = [ "proc-macro2", "quote", diff --git a/languages/golang/encrypt/guest/Cargo.lock b/languages/golang/encrypt/guest/Cargo.lock index ebc293901..926813680 100644 --- a/languages/golang/encrypt/guest/Cargo.lock +++ b/languages/golang/encrypt/guest/Cargo.lock @@ -2073,7 +2073,7 @@ dependencies = [ [[package]] name = "stack-encrypt" -version = "0.2.0" +version = "0.3.0" dependencies = [ "base64ct", "cllw-ore", @@ -2094,7 +2094,7 @@ dependencies = [ [[package]] name = "stack-encrypt-derive" -version = "0.2.0" +version = "0.3.0" dependencies = [ "proc-macro2", "quote", diff --git a/packages/eql/Cargo.lock b/packages/eql/Cargo.lock index 381490a3a..9ab983cb6 100644 --- a/packages/eql/Cargo.lock +++ b/packages/eql/Cargo.lock @@ -4228,7 +4228,7 @@ dependencies = [ [[package]] name = "stack-encrypt" -version = "0.2.0" +version = "0.3.0" dependencies = [ "base64ct", "cllw-ore 0.5.0", @@ -4249,7 +4249,7 @@ dependencies = [ [[package]] name = "stack-encrypt-derive" -version = "0.2.0" +version = "0.3.0" dependencies = [ "proc-macro2", "quote", diff --git a/packages/eql/crates/eql-bindings/Cargo.toml b/packages/eql/crates/eql-bindings/Cargo.toml index 93133bf96..87731ac19 100644 --- a/packages/eql/crates/eql-bindings/Cargo.toml +++ b/packages/eql/crates/eql-bindings/Cargo.toml @@ -46,7 +46,7 @@ schemars = "1" # `dynamic`: `FfiValue` / `ValueKind`, the runtime plaintext a plan field # target takes and the kind names a plan's `"type"` key spells # (`encryption::targets`). -stack-encrypt = { path = "../../../stack-encrypt", version = "0.2.0", optional = true, default-features = false, features = ["dynamic"] } +stack-encrypt = { path = "../../../stack-encrypt", version = "0.3.0", optional = true, default-features = false, features = ["dynamic"] } # The vitaminc line stack-encrypt is built on. `PrfValue` is named from this # crate directly (stack-encrypt re-exports only `IntoPrfContext` and # `PrfContext`), and two vitaminc lines in one graph do not interoperate, so diff --git a/packages/stack-encrypt-derive/Cargo.toml b/packages/stack-encrypt-derive/Cargo.toml index 223cd01bc..0e90b04d0 100644 --- a/packages/stack-encrypt-derive/Cargo.toml +++ b/packages/stack-encrypt-derive/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "stack-encrypt-derive" description = "Derive macros for stack-encrypt's target-directed encryption" -version = "0.2.0" +version = "0.3.0" edition.workspace = true authors.workspace = true repository.workspace = true diff --git a/packages/stack-encrypt/Cargo.toml b/packages/stack-encrypt/Cargo.toml index b1124f27d..0351ca5d7 100644 --- a/packages/stack-encrypt/Cargo.toml +++ b/packages/stack-encrypt/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "stack-encrypt" description = "Encrypt Rust values under per-value ZeroKMS data keys via the vitaminc cipher traits" -version = "0.2.0" +version = "0.3.0" edition.workspace = true authors.workspace = true repository.workspace = true @@ -22,7 +22,7 @@ stack-kms = { path = "../stack-kms", version = "0.1.0", default-features = false # return type names the auto-detected auth strategy. Only needed with `http`. stack-auth = { workspace = true, optional = true } # `#[derive(EncryptFrom)]` / `#[derive(DecryptInto)]`, re-exported from `target`. -stack-encrypt-derive = { path = "../stack-encrypt-derive", version = "0.2.0" } +stack-encrypt-derive = { path = "../stack-encrypt-derive", version = "0.3.0" } # The workspace vitaminc (0.6.0): one canonical context encoding shared by # the AEAD and PRF derivations (`IntoContext`, cipherstash/vitaminc#339), diff --git a/packages/stack-encrypt/fuzz/Cargo.lock b/packages/stack-encrypt/fuzz/Cargo.lock index 881a949a2..bc151b4f7 100644 --- a/packages/stack-encrypt/fuzz/Cargo.lock +++ b/packages/stack-encrypt/fuzz/Cargo.lock @@ -2030,7 +2030,7 @@ dependencies = [ [[package]] name = "stack-encrypt" -version = "0.2.0" +version = "0.3.0" dependencies = [ "base64ct", "cllw-ore", @@ -2051,7 +2051,7 @@ dependencies = [ [[package]] name = "stack-encrypt-derive" -version = "0.2.0" +version = "0.3.0" dependencies = [ "proc-macro2", "quote",