fix: use random IVs for stored keys (put/get), matching the Dart SDK#526
Open
cconstab wants to merge 16 commits into
Open
fix: use random IVs for stored keys (put/get), matching the Dart SDK#526cconstab wants to merge 16 commits into
cconstab wants to merge 16 commits into
Conversation
added 7 commits
July 2, 2026 16:11
Previously put/get encrypted every stored value under a static all-zero IV (the default of aes_encrypt/decrypt_from_base64) — IV reuse across all data. Match the Dart at_client behavior: - shared keys: put always generates a random 16-byte IV, stored as ivNonce in the key metadata (serialized into the update command); - self keys: keep the legacy zero IV unless the caller set ivNonce (Dart does not auto-generate for self keys); - get: fetch metadata via llookup:all / lookup:all, use ivNonce when present, else fall back to the legacy zero IV — so existing data (and Dart-written legacy data) still decrypts; - Metadata: keep iv_nonce as raw bytes internally (decode incoming base64) so it round-trips with generate_iv_nonce()/__str__. Selection is purely by presence of ivNonce in metadata, matching Dart. Network-free unit tests in test/put_get_iv_test.py; cross-SDK interop verified separately against the Dart reference SDK.
…ilder Self keys now also get a random IV (stored as ivNonce), not just shared keys. Dart's current SelfKeyEncryption uses the zero IV for self keys, which is the same IV-reuse weakness; this is interop-safe because get falls back to the legacy zero IV when ivNonce is absent, and Dart's self decrypt honors ivNonce when present. Required fixing UpdateVerbBuilder, which silently dropped iv_nonce (set_metadata and _build_metadata_str never carried it) — so a self-key ivNonce would not have been persisted. Now round-trips. Tests: self put generates+persists a 16-byte IV; self encrypt->decrypt via ivNonce.
Move IV generation into put(), before dispatch to the per-type encryptor, matching Dart's structure (it randomizes ivNonce for every put in _putInternal). Keeps the per-type ??= backstop so direct calls stay safe. Adds a test for the put() layer.
…n CI test/interop_test.py exercises random-IV shared-key put/get both directions against the Dart reference at_client. Skipped unless AT_INTEROP=1 and Dart is on PATH, so the normal unittest run and fork CI are unaffected. Dart helper + pubspec under test/interop/. Draft manual workflow in .github/workflows/interop.yml starts the ephemeral env, onboards two atSigns, and runs it.
…gotcha Defensive rm of $HOME/.atsign/storage before the interop run (harmless on a fresh runner; avoids stale-key decrypt failures when re-running against a recreated EE).
cpswan
reviewed
Jul 3, 2026
cpswan
left a comment
Member
There was a problem hiding this comment.
Code is OK, but a lot of nits where Claude hasn't been told how to be a proper Atsign developer.
…via poetry/uv, dependabot, lint) - interop.yml: pin actions to SHAs (checkout v7.0.0, setup-python v6.3.0, setup-dart v1.7.2, setup-uv v8.2.0); python 3.14; drop the README.PyPI.md hack and install deps from poetry.lock via uv (repo's own pattern); drop DRAFT framing - move the inline CRAM onboarding into test/interop/onboard.py - gitignore test/interop/.dart_tool/ (untracked; keep pubspec.lock) - dependabot: add pub ecosystem for /test/interop - lint test/interop/README.md (<=80 cols, blanks around headings) - fix E702 semicolons in put_get_iv_test.py
…at_python into fix/put-get-random-iv
Member
Author
|
Thanks @cpswan — all addressed:
Re-validated end-to-end on Actions after the changes — green, both directions ran ( |
Member
Author
|
Note: the required checks got stuck as "Expected — waiting for status" because a prior commit message contained |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
put/getencrypted every stored value under a static all-zero IV (the defaultof
aes_encrypt_from_base64/aes_decrypt_from_base64), i.e. IV reuse across alldata. This makes the Python SDK match the Dart reference
at_client:putalways generates a random 16-byte IV and stores it asivNoncein the key metadata (serialized into the update command).ivNonce) — matching current Dart,which randomizes the IV for every put in
AtClientImpl._putInternal(
at_client_impl.dart:973) before dispatching to the encryptor. (Dart's per-typeSelfKeyEncryptionstill has a dead zero-IV branch; a port must randomize self keysor it writes zero-IV data — which released atsdk does.) Interop-safe:
getfallsback to the zero IV when
ivNonceis absent.iv_nonce—set_metadata/_build_metadata_strsilently dropped it, so a self-key
ivNoncewould never have been persisted.llookup:all/lookup:all, useivNoncewhen present,else fall back to the legacy zero IV — so existing data (and Dart-written legacy
data) still decrypts.
iv_noncekept as raw bytes internally (decode incoming base64) so itround-trips with
generate_iv_nonce()/__str__.Selection is purely by presence of
ivNonce, matching Dart(
legacy_encryption.dart/legacy_decryption.dart:ivNonce != null ? fromBase64 : generateIVLegacy(); legacy IV = 16 zero bytes).Tests
test/put_get_iv_test.py— network-free: AES round-trip with a random IV; legacy IVis 16 zero bytes;
Metadataiv_nonce base64<->bytes round-trip;_iv_from_fetched;and
_put_shared_keygenerates + persists a 16-byte IV.test/interop_test.py(with a Dart helperunder
test/interop/) runs both directions against the Dart referenceat_client:putshared → Dartgetshared ✅putshared → Pythongetshared ✅Guarded: skipped unless
AT_INTEROP=1+ Dart on PATH, so the normalunittestrun and fork CI are unaffected.
CI / CD note
.github/workflows/interop.ymlruns this interop test on GitHub Actions: it stands upthe ephemeral-environment service container, adds the
vip.ve.atsign.zonehosts entry,installs Python + Dart, installs the SDK, onboards two atSigns via CRAM, and runs the
test with
AT_INTEROP=1. It's currentlyworkflow_dispatch(manual) so it's opt-in anddoesn't change the default pipeline.
It has been validated in a real GitHub Actions run — green in ~1m20s, with both
directions actually executing (
Ran 2 tests ... OK, not skipped). Recommendation:promote it into regular CI/CD — e.g. run on PRs that touch the crypto/
atclientpaths,or on a nightly schedule — so Python↔Dart wire compatibility (IVs and beyond) stays
guaranteed rather than checked once. (Note:
workflow_dispatchis only triggerable oncethe workflow lands on the default branch.)
Backward compatibility: absent
ivNonce⇒ zero IV, so all pre-existing data(Python- or Dart-written) still decrypts.