Skip to content

Items: multi-field entries, 22 categories, and bidirectional 1Password sync - #16

Merged
vaulttec-dev merged 5 commits into
developfrom
items-v2
Sep 20, 2026
Merged

vaulttec-dev merged 5 commits into
developfrom
items-v2

Conversation

@vaulttec-dev

@vaulttec-dev vaulttec-dev commented Sep 20, 2026 •

Copy link
Copy Markdown
Owner

Replaces the single-secret entry model with multi-field items, supports all 22 1Password categories, and enables bidirectional vault synchronization through the op CLI.

Summary

The previous storage architecture carried at most one secret per entry (SECRET_MAX 256 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):

  • All 22 categories: from Logins and Servers to Credit Cards and Identities.
  • Multi-field structure: each item carries up to 32 fields with section, label, kind, and class.
  • Class-based access control: Open (PIN only), Secret (requires tap), and Seed (tap produces a TOTP code; double tap exports the seed in the clear).
  • Packed flash storage: variable-length packed records in flash with an in-RAM index (store.rs), ending slot-based space waste.
  • Full 1Password sync: vkey op mirrors vaults both ways; vkey export writes items back to 1Password via op item create.
  • Seed export under double tap: explicitly allows TOTP seeds to leave the device under the dedicated double-tap export gesture so entries can be restored to 1Password intact.

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-RAM Index with offset/length tracking.
  • device.rs: Field retrieval filtered by Reach (Open, Secret, Seed); per-field class validation and gesture gating.
  • proto.rs & wire.rs: ItemPut and ItemGet commands 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

  • Full unit test suite (cargo test) passes across vaultkey-core (68 tests) and cli (32 tests).
  • All static analysis, formatting, and lint checks pass (cargo fmt, clippy -D warnings, cargo doc, shellcheck, mypy --strict).
  • Image reproducibility and Secure Boot signature verification verified locally via 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 op CLI.

What changed

  • Fixed 324-byte entry slots became variable-length packed records with an in-RAM index; store reads now handle corrupt image heads and blank sectors.
  • Field classes (Open, Secret, Seed) control what leaves the device, replacing per-entry kinds; category match on write.
  • TOTP seeds leave the device only under the double-tap gesture (now also for edits), the seed period propagates to generated codes, and vkey get copies codes to the clipboard automatically.
  • Imports preserve 1Password custom URL fields; notes are stored as Open fields, .env detection and export formatting use notesPlain, and legacy logins are UTF-8 validated during conversion.
  • docs/items-v2.md documents the new item model; docs/threat-model.md covers the seed export trade-off.
  • Firmware images are rebuilt reproducibly (byte-for-byte with CI) and signed for Secure Boot v2.

Migration

  • Legacy single-secret backups are converted on import and restore; the device no longer reads the old storage format.

Written for commit 611306e. Summary will update on new commits.

Review in cubic

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.
@coderabbitai

coderabbitai Bot commented Sep 20, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 7c88dbb7-d2e1-4c7a-86d9-b1383a326c28


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread cli/src/backup.rs
Comment thread cli/src/main.rs
Comment thread firmware/core/src/wire.rs
/// 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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Comment thread cli/src/item.rs
"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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Comment thread cli/src/shell.rs Outdated
state_a: 0x11_1000,
state_b: 0x12_6000,
env: 0x13_B000,
state_b: 0x14_6000,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Comment thread docs/items-v2.md Outdated
Comment thread docs/threat-model.md
Comment thread firmware/core/src/proto.rs Outdated
Comment thread README.md Outdated
| 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 |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
| 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.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread cli/src/op.rs Outdated
Comment thread cli/src/backup.rs Outdated
Comment thread cli/src/backup.rs Outdated
Comment thread cli/src/main.rs Outdated
Comment thread firmware/core/src/proto.rs Outdated
Comment thread cli/src/op.rs
Comment thread cli/src/check.rs Outdated
Comment thread docs/threat-model.md Outdated
Comment thread docs/threat-model.md Outdated
Comment thread README.md
| 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 |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread cli/src/backup.rs Outdated
Comment thread cli/src/op.rs Outdated
@vaulttec-dev
vaulttec-dev merged commit 2e2d6cb into develop Sep 20, 2026
11 checks passed
@vaulttec-dev
vaulttec-dev deleted the items-v2 branch September 20, 2026 18:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant