Skip to content

feat(account)!: adopt dig-session 0.5 BIP-39 root; expose the recovery phrase - #5

Merged
MichaelTaylor3d merged 5 commits into
mainfrom
feat/1759-recovery-phrase-and-bip39-root
Jul 29, 2026
Merged

feat(account)!: adopt dig-session 0.5 BIP-39 root; expose the recovery phrase#5
MichaelTaylor3d merged 5 commits into
mainfrom
feat/1759-recovery-phrase-and-bip39-root

Conversation

@MichaelTaylor3d

@MichaelTaylor3d MichaelTaylor3d commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

The consumer half of DIG-Network/dig_ecosystem#1759.

UNBLOCKED — verified against the PUBLISHED crate, not a local path

dig-session 0.5.1 is live on crates.io, so this branch no longer needs the temporary [patch.crates-io] it was validated under. That patch is gone and the lock was re-resolved from the registry:

  • Cargo.tomldig-session = "0.5" (a published version; no path, no git = — crates.io forbids git deps and a path dep blocks publishing).
  • Cargo.lockdig-session 0.5.1, source = "registry+https://github.com/rust-lang/crates.io-index", checksum = ecb21cf421c5f2b1d6e43b35592f71767c72526b4ccf5e7552352c0162ed4ded — byte-identical to the sparse index's cksum for 0.5.1.
  • The lock had been silently wrong. It carried a source-less dig-session entry left by the patch, which is a path resolution: the manifest said "0.5" while the build compiled the local worktree, and cargo update -p dig-session could not even match the package. Fixed in f18b954; the entire lock diff is the two added lines, no other pin moved.
  • Proof the tests exercise the published surface: cargo metadata --locked resolves dig-session to ~/.cargo/registry/src/index.crates.io-*/dig-session-0.5.1/, and cargo test --locked (no re-resolution permitted) passes 141 tests. Clippy -D warnings clean, coverage 95.69% lines — all --locked.

What

An account root is now 32 bytes of BIP-39 entropy, expanded to the 64-byte HD seed before any derivation, so the 24 words a user backs up restore to the same addresses in Sage and every other conforming Chia wallet.

  • UnlockedAccount::recovery_phrase() -> Zeroizing<String> over &self. This is the one secret the public API deliberately exposes — a backup the user cannot see is not a backup — and showing it must not cost them their session. Directly unblocks the dig-app account-setup GUI lane, which had nothing coherent to render.
  • AccountSession::enroll_from_recovery_phrase(store, id, password, phrase, default_ix) (+ the pub(crate) AccountStore path) — the restore-on-a-new-machine counterpart. Fail-closed on an already-enrolled account (never clobbers a live custody root) and on an invalid phrase.
  • SEED_LENENTROPY_LEN at every seam; master_seed() (still pub(crate)) is the expanded 64-byte seed.

BREAKING (0.1.2 → 0.2.0)

  • AccountStore::enroll / AccountSession::enroll take BIP-39 entropy, not a raw seed.
  • The frozen profile-DEK golden vector MOVED (3285f675…55d71eb7…). The HKDF construction is unchanged; its input scalar now derives from the expanded seed.

Correction: the re-pin was justified as "the exposed population is zero". That was FALSE.

The security gate proved it, and the claim is now corrected in keys/dek.rs, SPEC.md §10 and the README. The published dig-session 0.4 / dig-account 0.1 line auto-enrols an account at first boot with no user action (dig-app-core/src/account/boot.rs:122/:149FileBackend::new(brand_dir.join("account"))), and the resulting legacy DIGVK1 blob was found on the reference machine (account.default.dks, 105 bytes). So the population is every installed dig-app on Windows/macOS that has booted once.

The true, narrower reason: no sealed artifact keyed by the old derivation exists — no profiles/ dir, no sealed profile blobs, no wallet store, no funded account (money path unmerged). Nothing encrypted under the old DEK became unreadable, which is the only thing re-pinning a DEK can break. The false claim mattered because it is what the dig-app adoption lane and the next re-pin decision would have trusted — while the very same comment asserted the break must not happen twice. A further change needs a migration, not a second re-pin.

Adopting 0.2.0 REQUIRES a legacy-account path in the host (now normative here)

A legacy account is WEDGED, not merely unreadable: AccountSession::unlock surfaces LegacySeedFormat and never yields an UnlockedAccount, enroll/enroll_from_recovery_phrase at the same AccountId return AlreadyExists because enrolment refuses to overwrite a custody root, and no pre-0.2 release exposed recovery_phrase() — so the user was never shown 24 words either. A host that merely logs the error leaves the account permanently and silently without a signer.

SPEC.md §10 + the README now require: detect that error specifically; PRESERVE the old sealed blob (never delete — it is password-sealed, its password may live in an OS credential store neither crate can read, and a balance cannot be ruled out); surface it in the UI; re-enrol and show the new phrase. The dig-app lane owns the code.

Blast radius checked

Every dig_session::SEED_LEN / master_seed() consumer in this crate: store.rs, session.rs, unlocked.rs, keys/dek.rs, auth/policy.rs, profile_mint.rs, signer.rs, wallet/authorizer.rs. WalletKey::from_seed_at(&seed[..], ix) takes a slice and needed no signature change — it now receives 64 bytes and derives correctly, pinned by the address goldens below. keys/dek.rs::profile_dek still delegates to dig-session's frozen HKDF (never reimplemented), verified by delegates_byte_identically_to_dig_session.

⚠️ Risk: HIGH — custody code, and it moves a stored-secret derivation. Safe only under the zero-population finding.

STOP — read this before touching #1702 (dig-account money path, parked)

#1702 must rebase onto this and take 0.3.0. This PR claims 0.2.0, so #1702's current 0.2.0 will collide. Same repo, single-writer, so ordering is not optional:

Overlapping files Cargo.toml, SPEC.md, src/unlocked.rs — nothing else
src/unlocked.rs deliberately minimal here: one added method (recovery_phrase) plus one return-type widening (master_seed()[u8; MASTER_SEED_LEN]), specifically to keep that rebase cheap
Cargo.toml version 0.2.0, dig-session "0.4""0.5", bip39 added to [dev-dependencies]
SPEC.md new §2.0, additions to §5/§8, and a rewritten §10
Untouched by this PR all of src/wallet/*, src/session_residency.rs, src/error.rs, scripts/probe-guards.sh, tests/compile_fail/*

A substantive change #1702 must absorb, not just a rebase: its money path derives from master_seed(), which is now the BIP-39-expanded 64-byte seed rather than 32 bytes of entropy. Any golden vector on that branch keyed to the old root will move, and it must move to the standard Chia value — that is the whole point of #1759. It should re-pin against the frozen literals below rather than re-deriving.

Coordination detail

Same repo, so single-writer. This branch is cut from main and does not touch #1702's files except three it also edits — Cargo.toml, SPEC.md, src/unlocked.rs — and in unlocked.rs the change is deliberately one added method plus one return-type widening, to keep #1702's rebase cheap. (Restated above, deliberately, because it is easy to miss.) Its src/wallet/*, src/session_residency.rs and src/error.rs changes are untouched here.

Tests

Frozen bech32m address literals, produced independently via chia-wallet-sdk and identical to the ones pinned in dig-session and dig-wallet-backend — three crates now agree on the same two strings:

  • account_sits_at_the_standard_chia_address_for_its_phraseabandon…artxch16grurcglcwcv6arjarr720yd9wqhp9gkx3k8h25lhwg8pl7vl6ysuax0gy.
  • each_accounts_phrase_restores_that_account_and_not_anothertwo accounts with unrelated entropy; each reported phrase must restore ITS OWN frozen address and DEK.
  • recovery_phrase_does_not_consume_the_account.
  • enroll_then_unlock_recovers_the_same_master_seed now asserts the root equals an independently computed BIP-39 expansion (straight from bip39, not through dig-session) AND is not the stored entropy — asserting both legs is what distinguishes "expanded" from "stored verbatim" at this seam.

Why two accounts. The canonical all-zero-entropy mnemonic is blind to which account is in play: an implementation that ignored the live root and derived from zeros would return a self-consistent phrase for the wrong account and pass any single-account round-trip. That exact mutation survived in dig-session before the second actor was added.

Mutations executed (each reverted; 141 tests green after)

mutation result
enroll_from_recovery_phrase ignores the phrase (fixed phrase) each_accounts_phrase_restores… fails
UnlockedAccount::recovery_phrase returns a constant phrase each_accounts_phrase_restores… fails

Docs

SPEC.md gains §2.0 (the root is BIP-39 entropy; the expanded seed is consumed, never re-derived), the enrol/restore/phrase contracts in §5, the deliberate recovery_phrase exception to the no-secret-escapes invariant in §8, and the §10 break record + conformance requirements. README gains the phrase section.

Refs DIG-Network/dig_ecosystem#1759

MichaelTaylor3d and others added 3 commits July 28, 2026 23:25
…overy phrase)

Lane stub so the branch + draft PR exist before implementation (§1.8
push-early). No behaviour change.

Co-Authored-By: Claude <noreply@anthropic.com>
… phrase

An account root is now 32 bytes of BIP-39 **entropy** (dig-session 0.5), expanded
to the 64-byte HD seed before any derivation, so the 24 words a user backs up
restore to the same addresses in Sage and every other conforming Chia wallet
(dig_ecosystem #1759).

- `UnlockedAccount::recovery_phrase()` over `&self` — the one secret the public
  API deliberately exposes, because a backup the user cannot see is not a backup,
  and showing it must not cost them their session.
- `AccountSession::enroll_from_recovery_phrase` (+ the `pub(crate)` store path) —
  the restore-on-a-new-machine counterpart, fail-closed on an existing account and
  on an invalid phrase.
- `SEED_LEN` -> `ENTROPY_LEN` at every seam; `master_seed()` is the expanded
  64-byte seed.

Tests use TWO accounts with unrelated entropy and frozen bech32m address literals
produced independently via chia-wallet-sdk. The all-zero-entropy fixture alone is
blind to which account is in play: a `recovery_phrase()` that ignored the live
root would return a self-consistent phrase for the WRONG account and a
single-account round-trip could not see it.

BREAKING CHANGE: `AccountStore::enroll` / `AccountSession::enroll` take BIP-39
entropy rather than a raw seed, `master_seed()` returns 64 bytes, and the frozen
profile-DEK golden vector MOVED because its input scalar now derives from the
expanded seed. That is a §5.1-class change to a stored-secret derivation,
permissible only because the exposed population is zero (no deployed account
store; the money path is unmerged). It must not happen twice — any future change
needs a migration, not a re-pin.

Refs DIG-Network/dig_ecosystem#1759

Co-Authored-By: Claude <noreply@anthropic.com>
SPEC.md gains §2.0 (the account root is BIP-39 entropy; the expanded seed is
consumed, never re-derived), the enrol/restore/phrase API contracts in §5, the
deliberate recovery_phrase exception to the no-secret-escapes invariant in §8,
and a §10 record of the one stored-secret break that happened — why the DEK
golden vector was re-pinned rather than migrated, and that a further change
needs a migration. Adds the phrase section to the README.

Refs DIG-Network/dig_ecosystem#1759

Co-Authored-By: Claude <noreply@anthropic.com>
… path

Gate fix G1/G2 for dig_ecosystem#1759.

The DEK re-pin was justified as "the exposed population was zero". That is FALSE
and was proved so: the published dig-session 0.4 / dig-account 0.1 line
auto-enrolled an account at FIRST BOOT with no user action, and such sealed blobs
have been verified on real hosts. The claim mattered because it is what the
dig-app adoption lane and the next re-pin decision would have trusted — while the
same comment asserted the break must not happen twice.

The true, narrower reason: no sealed ARTIFACT keyed by the old derivation exists
(no profile blobs, no wallet store, no funded account), so nothing encrypted
under the old DEK became unreadable — which is the only thing re-pinning a DEK
can break. Corrected in `keys/dek.rs` and `SPEC.md` §10.

`SPEC.md` §10 and the README now also state normatively that adopting 0.2.0
REQUIRES a legacy-detection-and-re-enrolment path in the host: a legacy account
is WEDGED (unlock surfaces `LegacySeedFormat`, re-enrolment at the same
`AccountId` returns `AlreadyExists`, and no pre-0.2 release ever showed the user
24 words), so a host that merely logs the error leaves it permanently and
silently without a signer. The old sealed blob MUST be preserved, never deleted —
it may hold value and its password may live in an OS credential store neither
crate can read.

Refs DIG-Network/dig_ecosystem#1759

Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d added a commit to DIG-Network/dig-session that referenced this pull request Jul 29, 2026
…nvelope, recovery_phrase() (#6)

Closes the #1759 decision by implementing option **(b) conform to the
standard Chia/BIP-39 derivation**, plus option (c)'s versioned envelope
so the legacy shape fails closed rather than being reinterpreted.

## The bug

A DIG 24-word recovery phrase resolved to a **different address** than
Sage and every other conforming Chia wallet. The 32 stored bytes were
handed straight to `SecretKey::from_seed`, skipping BIP-39's PBKDF2
expansion. That does not fail — it derives a different, entirely
plausible wallet, so a user who lost their machine and typed their DIG
phrase into Sage would see an empty account, a valid-looking phrase, and
no error at all.

## The fix

The 32 stored bytes are now BIP-39 **entropy**, re-expanded `entropy →
24 words → to_seed("") → SecretKey::from_seed` at `master_seed()`.
Identity, per-profile identity, per-profile DEK and the wallet seed all
read that one expanded seed, so no two can disagree about the root.

**`SEED_LEN` did not need to grow to 64.** BIP-39 entropy for 24 words
IS 32 bytes and re-expands losslessly, so there is no at-rest length
change and no §5.1 stored-secret length break — the ticket's premise
that this requires 32→64 is refuted.

### The versioned envelope, and why legacy must fail closed

A legacy blob's 32 bytes (raw seed) and a current blob's 32 bytes
(entropy) are **byte-for-byte indistinguishable**. Silently
reinterpreting one as the other hands back a *working* handle for the
*wrong* account — strictly worse than an error. So a stored blob now
DECLARES what its bytes mean:

- Sealed via `dig_keystore::opaque` (magic `DIGOP1`) — the same audited
container (Argon2id + AES-256-GCM + CRC-32) every `Keystore<K>` file
uses — with plaintext `version:u8 || kind:u8 || entropy`. Kinds are
append-only (§5.1).
- A pre-envelope `DIGVK1` blob is detected **before** decryption and
rejected with `SessionError::LegacySeedFormat`, which tells the caller
to re-enrol from the phrase.
- An unrecognised version or kind is refused, never guessed.

This needed **no dig-keystore change**: `format::is_known_magic` is a
closed allow-list there, so a new `KeyScheme` defined outside
dig-keystore could not be decoded, and a `KeyScheme`'s fixed
`SECRET_LEN` cannot carry a version tag anyway. `opaque` is the
sanctioned arbitrary-length door into the same container. That avoided a
third-repo write and an extra publish hop while a consumer lane was
blocked.

### New API

- `UnlockedMasterSeed::recovery_phrase() -> Zeroizing<String>` over
**`&self`** — showing a user their backup must not cost them their
session.
- `Session::enroll_from_recovery_phrase(...)` — the
restore-on-a-new-machine path. Accepts any capitalisation and any
whitespace run; rejecting a correct phrase over a capital letter is a
trap, not a safety measure.

## BREAKING (0.4.0 → 0.5.1)

- `SEED_LEN` is **removed**, replaced by `ENTROPY_LEN` (32, stored) and
`MASTER_SEED_LEN` (64, expanded). Removing the ambiguous name is
deliberate: a stale call site now fails to **compile** rather than
deriving a silently wrong key.
- `master_seed()` returns the 64-byte expanded seed.
- For a given 32 stored bytes this **MOVES the identity public key and
the profile DEK**.

### Legacy accounts EXIST — an earlier draft of this PR said the exposed
population was zero. That was FALSE.

The security gate proved it, and the correction now lives in `SPEC.md`
§3.3.0, the `LegacySeedFormat` rustdoc, the README, and dig-account's
`keys/dek.rs` + `SPEC.md` §10:

- dig-app `origin/main` (3.5.0) depends on the **published**
`dig-session = "0.4"` + `dig-account = "0.1.1"`, and
`dig-app-core/src/account/boot.rs:122`/`:149` **auto-enrol
`account.default`** into `FileBackend::new(brand_dir.join("account"))`
at **first boot on every Windows/macOS host, with no user action**.
- The artifact was found on the reference machine:
`%LOCALAPPDATA%\DigNetwork\account\account.default.dks`, **105 bytes**,
magic `DIGVK1` — 53-byte header + (32 secret + 16 GCM tag) + 4 CRC =
exactly one legacy 32-byte raw seed.

So the population is *every installed dig-app on Windows/macOS that has
booted once*, at least one verified.

**The true, narrower reason the re-pin is sound:** no sealed
**artifact** keyed by the old derivation exists — no `profiles/` dir, no
sealed profile blobs, no wallet store, no funded account (money path
parked unmerged on #1702), pre-release per §3.7. Nothing *encrypted*
under the old DEK became unreadable, which is the only thing re-pinning
a DEK can break. A **further** change to a stored-secret derivation
needs a migration, not a second re-pin.

**That legacy file must NOT be treated as safe to delete.** It is
password-sealed and nobody has read the OS credential store, so a
balance cannot be ruled out.

### A legacy account is WEDGED, not merely fail-closed — and the crate
now says so

`unlock_master_seed` returns `LegacySeedFormat` and never a handle
(`session.rs:180` -> `envelope.rs:127`); `enroll_master_seed` at the
same key returns `AlreadyExists` (`envelope.rs:90`) because enrolment
refuses to overwrite a custody root; and no pre-envelope release exposed
`recovery_phrase()`, so the user was never shown 24 words. There is no
in-crate route back.

The code fix belongs on the **dig-app adoption path** (detect ->
preserve/back-up -> re-enrol, surfaced in the UI rather than a
`tracing::warn!` that silently yields "signing channel not started").
What this PR adds is the **contract**, so the wedge cannot be adopted
blindly: the `LegacySeedFormat` rustdoc spells out the four required
remediation steps, and `SPEC.md` §3.3.0 makes a
legacy-detection-and-re-enrolment path a **normative requirement of
adopting 0.5**. dig-account #5 carries the same requirement in its §10 +
README.

## Blast radius checked

`master_seed()` — every in-crate caller (`public_key`, `sign`,
`signing_fn`, `derive_symmetric_key`, `profile_public_key`,
`profile_sign`, `profile_derive_symmetric_key`) plus
`Session::{enroll_master_seed, unlock_master_seed}`, all updated in this
diff. Cross-repo consumers of the changed symbols: **`dig-account`
only** (`store.rs`, `session.rs`, `unlocked.rs`, `auth/policy.rs`,
`keys/dek.rs`, `profile_mint.rs`, `signer.rs`, `wallet/authorizer.rs`) —
migrated in the same unit of work on DIG-Network/dig-account#5.
`dig-wallet-backend` needs **no production change** (already standard);
confirmed by a new test on DIG-Network/dig-wallet-backend#23. Grepped
the whole superproject for `SEED_LEN` / `enroll_master_seed` /
`unlock_master_seed` / `UnlockedMasterSeed` — no other consumer.
`UnlockedIdentity` and the whole `enroll_identity` / `L1WalletBls` path
are untouched, so the 0.2.0 identity blobs are unaffected.

**⚠️ Risk: HIGH** — this is custody code and it moves a stored-secret
derivation. It is safe ONLY under the zero-population finding above.

## Memory hygiene: `bip39`'s `zeroize` feature is now ENABLED

An earlier revision of `master_seed.rs` claimed "bip39 2.x exposes no
`zeroize` feature". **Wrong** — bip39 2.2 derives `Zeroize +
ZeroizeOnDrop` on `Mnemonic` behind an *optional-dependency* feature,
which is why it does not appear in the `[features]` table (I had read
only that table). `expand_entropy` builds a `Mnemonic` on **every**
derivation and its word indices are a complete copy of the account root,
so without the feature un-wiped copies of the root accumulated in
memory. Feature enabled, comment corrected, and **CONF-11 asserts the
trait bound at compile time** — dropping the feature now fails to build.

Residual, documented, upstream-owned: bip39 does not wipe its
intermediate `to_string()` heap buffer or its transient `[u8; 33]`
entropy+checksum stack array.

## Tests (43 total, coverage 97.52% lines)

The load-bearing one is **cross-implementation**: a fixed public
mnemonic → a **hardcoded literal** bech32m address produced
independently via `chia-wallet-sdk`. Computing both sides live would let
a dependency bump move them together and mask a regression (the
`dig-keystore` `l1_wallet_bls.rs` precedent).

| | property |
|---|---|
| CONF-1 | `abandon…art` →
`xch16grurcglcwcv6arjarr720yd9wqhp9gkx3k8h25lhwg8pl7vl6ysuax0gy` (the
Sage address) |
| CONF-2 | non-vacuity: the fixture still reproduces the OLD
`xch1jcvy96pjk…` and the crate no longer does |
| CONF-3 | a legacy blob fails closed — no handle, no address |
| CONF-4 / 4b | phrase round-trip, and with **two accounts** each phrase
restores ITS OWN account |
| CONF-5 | `recovery_phrase()` does not consume the handle |
| CONF-6 | identity + DEK derive from the EXPANDED seed (placement, not
just outcome) |
| CONF-7..10 | invalid phrases rejected without echoing them; user-typed
case/whitespace; no overwrite; unknown envelope version/kind refused |
| CONF-11 | `Mnemonic: ZeroizeOnDrop` — a COMPILE-TIME pin on the
`zeroize` feature |
| CONF-12 | a **valid** 12/15/18/21-word phrase is REJECTED, never a
panic |

CONF-1/4/5/6/8 now drive from the **non-degenerate** fixture rather than
the all-zero one, so the account-identity-blind class has three nets
instead of one. The two `tests/master_seed.rs` DEK goldens are now **hex
literals** (`7bf5db71...`, `eddb8924...`) as their comments always
claimed — both sides had been computed live from `bip39` +
`dig-identity`, so a bump in either could have moved them together; the
independently-reconstructed dig-app reference is still asserted
alongside, since the literal alone cannot distinguish "frozen bytes
drifted" from "the two HKDFs diverged".

### Mutations executed (each reverted; suite green after)

| mutation | result |
|---|---|
| `expand_entropy` returns raw entropy padded to 64 | **9 tests fail** |
| `MASTER_SEED_LEN = 32` + expansion removed (literally the pre-#1759
code) | CONF-1, CONF-2, CONF-6 fail |
| `to_seed("dig")` — non-empty passphrase | CONF-1 fails |
| legacy-magic guard deleted from the envelope reader | CONF-3 fails |
| `recovery_phrase()` ignores `self.entropy` | **SURVIVED at first** —
see below |
| `enroll_from_recovery_phrase` ignores the phrase | CONF-4b fails |
| **delete the `entropy.len() != ENTROPY_LEN` guard**
(`master_seed.rs:178`) | CONF-12 fails — panics at `copy_from_slice` |
| **drop the `bip39` `zeroize` feature** | CONF-11 fails to COMPILE |
| `recovery_phrase()` ignores the root, re-run after widening the
fixtures | now **3** tests fail, not 1 |

The guard-deletion mutation is the second false green this lane found:
**deleting the entropy-length guard left all 41 tests green**, while
making a perfectly valid 12-word BIP-39 phrase a reachable
`copy_from_slice` panic on the restore path — CONF-7's malformed cases
all fail inside bip39 first and never reach the guard. CONF-12 uses
valid shorter phrases, with a fixture check asserting bip39 itself
accepts them, so the rejection provably comes from DIG's 24-word
requirement.

**The first false green, found and fixed earlier:** `recovery_phrase()`
returning a phrase derived from a *fixed* entropy passed every test,
because the fixture was the **all-zero-entropy** mnemonic — the
strongest-looking canonical vector is exactly the one blind to *which*
account is in play. A bug of that shape would hand a user a phrase that
restores someone else's account. Fixed by adding CONF-4b: a second
account with unrelated entropy (`fog spot notable…equal` →
`xch1vpxzuu6aqfu790qcrcppcr2gmju4f5tpuuznuv2lx3g79v2jxc7qxttpzt`) as a
truthful control, so each phrase must resolve to its own frozen address.
The mutation now fails.

## Docs

`SPEC.md` gains §3.3.0 (the root is entropy; every derivation reads the
expanded seed; empty passphrase) and §3.4 (the envelope + the
fail-closed rule), the CONF-1..10 conformance list, and two custody
invariants (no secret in an error message; never log a phrase). README
gains the account/phrase section. `dig-logging`'s BIP-39 redactor
(`redact.rs` `MIN_MNEMONIC_RUN = 12`, case-insensitive) covers the
24-word form, but `recovery_phrase`'s docs now warn that it is **not
protection**: `Zeroizing<String>` derefs to `String`, so
`info!("{phrase}")` compiles, and the redactor runs at **bundle time**,
so a logged phrase sits in plaintext on disk until a bundle is built.
dig-session itself has no logging dependency, so it adds no log surface
of its own.

INFO, no action: every derivation now costs one PBKDF2-HMAC-SHA512/2048
— **0.84 ms measured**. Not an amplification lever, but `sign()` is no
longer free.

Refs DIG-Network/dig_ecosystem#1759

---------

Co-authored-by: Claude <noreply@anthropic.com>
The lock still carried a source-less `dig-session` entry left behind by the
temporary `[patch.crates-io]` used to validate this branch before 0.5.1 was
published. A source-less entry is a PATH resolution: the manifest said
`dig-session = "0.5"` while the lock silently compiled the local worktree, so
`cargo update -p dig-session` could not even match the package. That is the shape
that has bitten this ecosystem before — an intermediate consumer's lock pinning
something other than the published crate makes a fix miss the very path it was
meant to reach.

The stale block is dropped and re-resolved from the registry. The lock now
carries `source = "registry+..."` and checksum
`ecb21cf421c5f2b1d6e43b35592f71767c72526b4ccf5e7552352c0162ed4ded`, which matches
the sparse index's `cksum` for 0.5.1 byte-for-byte. The whole diff is those two
added lines — no other pin moved.

Verified against the PUBLISHED crate, not the path: `cargo metadata --locked`
resolves dig-session 0.5.1 to the registry cache, and `cargo test --locked`
(no re-resolution permitted) passes 141 tests.

Refs DIG-Network/dig_ecosystem#1759

Co-Authored-By: Claude <noreply@anthropic.com>
@MichaelTaylor3d
MichaelTaylor3d marked this pull request as ready for review July 29, 2026 08:57
@MichaelTaylor3d
MichaelTaylor3d merged commit d4f8982 into main Jul 29, 2026
9 checks passed
@MichaelTaylor3d
MichaelTaylor3d deleted the feat/1759-recovery-phrase-and-bip39-root branch July 29, 2026 09:02
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