Repository navigation
Items: multi-field entries, 22 categories, and bidirectional 1Password sync - #16
Conversation
The key mirrors a 1Password vault whole — every category, every field — and can write an entry back into 1Password through the op CLI. - Replaces fixed 324-byte record slots with variable-length packed item storage in flash, keeping only an index in RAM. - Supports all 22 1Password categories instead of only password entries. - Introduces field classes (Open, Secret, Seed) to determine what may leave the key, independent of entry category. - Allows TOTP seeds to leave the device in the clear under a distinct double-tap export gesture to support bidirectional 1Password sync. - Updates CLI shell, inspection, import, and backup flows to support multi-field items. - Updates threat model and documentation to reflect the seed export trade-offs and multi-field model.
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
6 issues found across 28 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="firmware/boards/esp32c6-zero/src/main.rs">
<violation number="1" location="firmware/boards/esp32c6-zero/src/main.rs:46">
P3: The new layout makes the board documentation incorrect: `docs/hardware.md` still describes 21-sector A/B images and a separate `.env` region. Update the hardware flash-layout and RAM descriptions to match the 53-sector packed images and `Index` buffer.</violation>
</file>
<file name="firmware/core/src/wire.rs">
<violation number="1" location="firmware/core/src/wire.rs:62">
P1: When a packed item is larger than 8,158 bytes and its name is long enough, backup export fails even though this wire limit advertises it as supported. Allocate a backup-sized working buffer or reduce the advertised maximum to the size the device can actually process.</violation>
</file>
<file name="cli/src/item.rs">
<violation number="1" location="cli/src/item.rs:47">
P2: When a stored field contains non-UTF-8 bytes, `OwnedField::text()` silently replaces them before export, so `vkey export` writes corrupted field data. Make the conversion fallible and reject or explicitly encode binary values instead of using a lossy conversion.</violation>
<violation number="2" location="cli/src/item.rs:213">
P1: When a 1Password `PASSWORD` field has no `type`, this fallback records it as `FieldKind::String`. `vkey export` then emits `type: STRING` without the password purpose, recreating a visible text field instead of a concealed password; preserve the purpose or infer `Concealed` before serialization.</violation>
</file>
<file name="firmware/core/src/store.rs">
<violation number="1" location="firmware/core/src/store.rs:425">
P1: When both copies contain a non-erased image with an unknown magic, `image_head` reports them as blank, so setting a PIN can overwrite an old or corrupted vault without an explicit wipe. Treat only an erased head as blank and preserve `Corrupt` for unrecognized nonblank heads.</violation>
</file>
<file name="README.md">
<violation number="1" location="README.md:152">
P3: `vkey export` does not always use a double tap: seedless items use a tap for secrets or no gesture for open-only items. Mark this row as seed-only and document the tap/no-gesture path.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| /// by its u16 length alone. | ||
| pub const MAX_PAYLOAD: usize = ITEM_MAX; | ||
| /// The longest backup item: an item with its category and name in front, sealed. | ||
| pub const BACKUP_ITEM_MAX: usize = 2 + NAME_MAX + ITEM_MAX + OVERHEAD; |
There was a problem hiding this comment.
P1: When a packed item is larger than 8,158 bytes and its name is long enough, backup export fails even though this wire limit advertises it as supported. Allocate a backup-sized working buffer or reduce the advertised maximum to the size the device can actually process.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At firmware/core/src/wire.rs, line 62:
<comment>When a packed item is larger than 8,158 bytes and its name is long enough, backup export fails even though this wire limit advertises it as supported. Allocate a backup-sized working buffer or reduce the advertised maximum to the size the device can actually process.</comment>
<file context>
@@ -56,36 +58,32 @@ pub fn frame_head(tag: u8, len: u16) -> [u8; 7] {
-/// by its u16 length alone.
-pub const MAX_PAYLOAD: usize = ITEM_MAX;
+/// The longest backup item: an item with its category and name in front, sealed.
+pub const BACKUP_ITEM_MAX: usize = 2 + NAME_MAX + ITEM_MAX + OVERHEAD;
+/// The longest request: an `ImportItem` carrying the biggest backup item, which
+/// outgrows an `ItemPut` by the AEAD overhead. Requests only; a response is bounded by
</file context>
| "FILE" => FieldKind::File, | ||
| // A type this CLI has not met is text until proven otherwise; `purpose` and the | ||
| // concealed flag are what actually decide the class, and both are checked above. | ||
| _ => FieldKind::String, |
There was a problem hiding this comment.
P1: When a 1Password PASSWORD field has no type, this fallback records it as FieldKind::String. vkey export then emits type: STRING without the password purpose, recreating a visible text field instead of a concealed password; preserve the purpose or infer Concealed before serialization.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cli/src/item.rs, line 213:
<comment>When a 1Password `PASSWORD` field has no `type`, this fallback records it as `FieldKind::String`. `vkey export` then emits `type: STRING` without the password purpose, recreating a visible text field instead of a concealed password; preserve the purpose or infer `Concealed` before serialization.</comment>
<file context>
@@ -0,0 +1,318 @@
+ "FILE" => FieldKind::File,
+ // A type this CLI has not met is text until proven otherwise; `purpose` and the
+ // concealed flag are what actually decide the class, and both are checked above.
+ _ => FieldKind::String,
+ }
+}
</file context>
| state_a: 0x11_1000, | ||
| state_b: 0x12_6000, | ||
| env: 0x13_B000, | ||
| state_b: 0x14_6000, |
There was a problem hiding this comment.
P3: The new layout makes the board documentation incorrect: docs/hardware.md still describes 21-sector A/B images and a separate .env region. Update the hardware flash-layout and RAM descriptions to match the 53-sector packed images and Index buffer.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At firmware/boards/esp32c6-zero/src/main.rs, line 46:
<comment>The new layout makes the board documentation incorrect: `docs/hardware.md` still describes 21-sector A/B images and a separate `.env` region. Update the hardware flash-layout and RAM descriptions to match the 53-sector packed images and `Index` buffer.</comment>
<file context>
@@ -43,21 +43,27 @@ const VERSION: &str = vaultkey_core::version!("esp32c6-zero");
state_a: 0x11_1000,
- state_b: 0x12_6000,
- env: 0x13_B000,
+ state_b: 0x14_6000,
};
+const _: () = assert!(
</file context>
| | Tap | amber | one TOTP code, the secret fields of one item, one `.env`, or one `vkey auth` login | | ||
| | Hold 5 s | red | factory wipe: every secret and the PIN | | ||
| | Double tap | blue | the encrypted backup file | | ||
| | Double tap | blue | the encrypted backup file, or an export back into 1Password | |
There was a problem hiding this comment.
P3: vkey export does not always use a double tap: seedless items use a tap for secrets or no gesture for open-only items. Mark this row as seed-only and document the tap/no-gesture path.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At README.md, line 152:
<comment>`vkey export` does not always use a double tap: seedless items use a tap for secrets or no gesture for open-only items. Mark this row as seed-only and document the tap/no-gesture path.</comment>
<file context>
@@ -147,19 +147,24 @@ line to add by hand.
+| Tap | amber | one TOTP code, the secret fields of one item, one `.env`, or one `vkey auth` login |
| Hold 5 s | red | factory wipe: every secret and the PIN |
-| Double tap | blue | the encrypted backup file |
+| Double tap | blue | the encrypted backup file, or an export back into 1Password |
A tap never wipes and never exports, so a hostile host cannot swap a code request for a
</file context>
| | Double tap | blue | the encrypted backup file, or an export back into 1Password | | |
| | Double tap | blue | the encrypted backup file, or a seed-bearing export back into 1Password; seedless exports use the tap/no-gesture reach | |
- Clamp VALUE_MAX to 8056 bytes so field pushes within bounds never fail. - Expand store ITEM_BUF_LEN to hold maximum-sized backup items with labels. - Tighten store image_head corrupt state detection and blank sector handling. - Verify category match during item put in core device. - Propagate TOTP seed period to device code response and CLI. - Add legacy v1 backup conversion during restore. - Support seed export and editing under double tap gesture. - Preserve 1Password custom URL fields and notes classification. - Add tests for corrupt items and ciphertext tampering. - Rebuild and sign firmware images, and update documentation.
There was a problem hiding this comment.
2 existing issues remain and 1 new issue found across 23 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="README.md">
<violation number="1" location="README.md:165">
P3: This limit update leaves conflicting 8128-byte guidance in the CLI, shell, and threat model, so users can prepare an `.env` that the firmware rejects. Update those remaining descriptions to the enforced 8056-byte `VALUE_MAX`.</violation>
</file>
Requires human review: Auto-approval blocked because this review re-detected 2 unresolved issues already reported by Cubic.
Re-trigger cubic
| | Login, password, note | 255 bytes each, 256 per entry in total | | ||
| | Items (every category together) | 256 | | ||
| | Fields per item | 32 | | ||
| | One field | 8056 bytes — a `.env`, an SSH key, a note | |
There was a problem hiding this comment.
P3: This limit update leaves conflicting 8128-byte guidance in the CLI, shell, and threat model, so users can prepare an .env that the firmware rejects. Update those remaining descriptions to the enforced 8056-byte VALUE_MAX.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At README.md, line 165:
<comment>This limit update leaves conflicting 8128-byte guidance in the CLI, shell, and threat model, so users can prepare an `.env` that the firmware rejects. Update those remaining descriptions to the enforced 8056-byte `VALUE_MAX`.</comment>
<file context>
@@ -161,7 +162,7 @@ in `docs/threat-model.md`.
| Items (every category together) | 256 |
| Fields per item | 32 |
-| One field | 8128 bytes — a `.env`, an SSH key, a note |
+| One field | 8056 bytes — a `.env`, an SSH key, a note |
| One item | 8192 bytes, packed |
| The vault | ~210 KB, two copies; a write costs what the vault holds, not what it might |
</file context>
The local environment had the rust-src component installed, which caused rustc to resolve standard library paths to the local sysroot rather than the upstream commit paths embedded in CI builds without rust-src. Removing rust-src aligns the local build byte-for-byte with CI.
- Propagate OS RNG failures during legacy restore and zeroize buffers. - Restore automatic clipboard copying for TOTP codes in vkey get. - Zeroize the output buffer in Cmd::Code stack on the key. - Explicitly select notesPlain for .env detection and export formatting. - Fix TOTP test vector for 60s period in vkey check. - Remove invalid references to vkey get --seed in documentation. - Align Category in AAD explanation with AEAD verification semantics. - Update remaining 8128-byte references to 8056 bytes.
There was a problem hiding this comment.
All reported issues were addressed across 11 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
Replaces the single-secret entry model with multi-field items, supports all 22 1Password categories, and enables bidirectional vault synchronization through the
opCLI.Summary
The previous storage architecture carried at most one secret per entry (
SECRET_MAX256 B) in fixed 324-byte flash slots. That model could not accommodate real-world vault entries (such as Identities or Credit Cards with 15–20 fields) and could not mirror entries back into 1Password without flattening them.This change introduces the Items v2 model (
docs/items-v2.md):Open(PIN only),Secret(requires tap), andSeed(tap produces a TOTP code; double tap exports the seed in the clear).store.rs), ending slot-based space waste.vkey opmirrors vaults both ways;vkey exportwrites items back to 1Password viaop item create.Changes
Firmware (
firmware/core)item.rs:Item,Field,Class,FieldKind,Category,Writer, and wire/storage serialization.store.rs: Variable-length packed image writer and parser; in-RAMIndexwith offset/length tracking.device.rs: Field retrieval filtered byReach(Open,Secret,Seed); per-field class validation and gesture gating.proto.rs&wire.rs:ItemPutandItemGetcommands with reach bytes and presence bitmasks.CLI (
cli)item.rs: Client-side representation and field parsing.op.rs: Full item conversion to/from 1Password JSON schemas; bidirectional sync.shell.rs: Interactive UI rendering for items, field inspection, and class indicators.auth.rs,backup.rs,check.rs,import.rs,totp.rs: Updated to use the item model.Documentation & Security
docs/items-v2.md: Design document detailing the motivation, flash layout, and security model.docs/threat-model.md: Updated to reflect the seed export trade-offs and multi-field item guarantees.firmware/boards/esp32c6-zero/images/: Rebuilt and signed firmware binaries for Secure Boot v2.Testing
cargo test) passes acrossvaultkey-core(68 tests) andcli(32 tests).cargo fmt,clippy -D warnings,cargo doc,shellcheck,mypy --strict).tools/checks.sh.Summary by cubic
Replaces the single-secret entry model with multi-field items, adds all 22 1Password categories, and makes vault sync bidirectional through the
opCLI.What changed
Open,Secret,Seed) control what leaves the device, replacing per-entry kinds; category match on write.vkey getcopies codes to the clipboard automatically.Openfields,.envdetection and export formatting usenotesPlain, and legacy logins are UTF-8 validated during conversion.docs/items-v2.mddocuments the new item model;docs/threat-model.mdcovers the seed export trade-off.Migration
Written for commit 611306e. Summary will update on new commits.