A migrated Go UserEntries read can return invalid replacement-character text in the review field for entries whose stored encrypted review value is exactly 32 hexadecimal characters. Flutter treats a non-empty review for free write, highlight, and lowlight entries as JSON and calls jsonDecode(review), so the malformed value can crash the journal page after entries download.
The user outcome is: journal entry list reads should preserve the legacy JS-visible contract where an encrypted empty review decrypts to the empty string, allowing Flutter to use Review.none() instead of parsing garbage as JSON.
- Active goal state was inspected with
get_goal; it is already initialized from the reusableiterative-code-designgoal template. - Target design document:
~/dev/work/reflection/go/reflection_backend/.ai/docs/js-go-migration/len-32-cyphertext-fix.md. - Minimum effort floor: 5 minutes. This is an earliest-eligible floor only; the design is complete based on evidence and audit, not elapsed time alone.
- Repository guidance inspected:
AGENTS.md: requires searchingPROJECT.mdbefore exploratory repository search; this was done first.PROJECT.md: confirms DDD migration direction, encryption compatibility requirement, Secret Manager ownership, and targeted test practices..ai/docs/architecture-and-system-design-style.md: absent..ai/docs/code-style.md: absent.
- Design-only scope: this document may be edited; runtime/source implementation is not part of this goal.
- Define the safest implementation shape for exactly-32-character encrypted values.
- Preserve Go/JS encryption compatibility for migrated entry read paths.
- Minimize blast radius by changing one ownership boundary instead of adding per-endpoint or frontend special cases.
- Define deterministic unit tests and live validation gates for a future implementation.
- No source-code implementation in this design goal.
- No Firestore data migration or document rewrite.
- No Flutter parser workaround unless later evidence disproves the backend encryption-format cause.
- No redesign of the
UserEntrieswire contract. - No broad clean-up of encryption logging, Secret Manager setup, or entry mapper architecture beyond this defect.
AGENTS.md: project-specific search discipline and validation/review expectations.PROJECT.md: Go backend architecture, DDD migration status, test setup rules, Secret Manager ownership, encryption compatibility, and targeted HTTP test pattern..ai/docs/graphql-to-go-direct-http-migration-framework.md: migrated endpoints should keep thin HTTP handlers, domain/application ownership, targeted validation, and stable client-visible contracts..ai/issues/open/ISSUE-001-entry-user-entries-http-list-fetch.md:UserEntriesmust preserve Flutter-consumed fields, includingreview, and keep entry parsing stable..ai/issues/open/ISSUE-006-entry-migration-cross-cutting-shared-seams.md: entry read shaping is intentionally shared; decryption/defaulting belongs in a shared read seam, not per endpoint..ai/docs/shared-direct-http-client-architecture.md: entries already use direct HTTP through Go Cloud Functions.
internal/infrastructure/encryption/encryption_service.go:Encryptemitshex(16-byte IV) + hex(ciphertext).Decryptcurrently tries standard format only whenlen(value) > 32.- Values with
len(value) == 32skip standard parsing and fall into legacy parsing. - Legacy parsing treats the first 16 characters as an 8-byte IV and the remaining 16 characters as ciphertext, then pads an 8-byte IV to 16 bytes before AES-CTR.
internal/infrastructure/encryption/encryption_service_test.goandinternal/infrastructure/encryption/encryption_test.go:- compatibility tests exist for normal non-empty encrypted values.
- there is no deterministic test for an IV-only encrypted representation.
internal/entries/application/entry_read_shaper.go:TextandReviewboth usedecryptFieldSafely.- empty stored fields return empty without decrypting.
- decrypt errors fall back to ciphertext, but the current exact-32 failure is worse because legacy decrypt returns garbage without an error.
internal/entries/application/entry_write_normalizer.go:- migrated Go writes encrypt
reviewonly when the request review is non-empty. - this avoids creating new encrypted-empty reviews from Go, but does not fix legacy/prod data already stored as IV-only encrypted review values.
- migrated Go writes encrypt
internal/entries/domain/user_entries_types.go:UserEntryResponse.Reviewis a string field in the stable list response.
internal/entries/infrastructure/stored_entry_mapper.go:- Firestore
reviewis preserved as a string and passed to the read shaper.
- Firestore
http_user_entries_handler.goandinternal/entries/interfaces/cloudfunction/user_entries_handler.go:- top-level handler wiring delegates to the entry application service and returns
success: trueplusentries.
- top-level handler wiring delegates to the entry application service and returns
~/.codex/worktrees/4f55/flutter/lib/features/entries/backend/entry_list_http_client.dart:- decodes the Go HTTP envelope and passes each entry row to the parser.
~/.codex/worktrees/4f55/flutter/lib/features/entries/services/entry_remote_service.dart:- maps
payload['review']to a string and constructsEntrywithreviewFromTypeAndJson. - parse failures are logged per entry, but the journal flow can still surface a web crash when bad UTF-8 / invalid JSON propagates through the browser runtime.
- maps
~/.codex/worktrees/4f55/flutter/lib/features/entries/models/entry.dart:reviewFromTypeAndJsonreturnsReview.none()only for null or empty review.- for
free write,highlight, andlowlight, a non-empty review is decoded as JSON.
~/dev/work/reflection/js/reflection_backend/functions/src/helpers/encryption.js:- legacy JS treats strings with length at least 32 and no spaces as encrypted and delegates to Cryptr decrypt.
- this supports the interpretation that 32-character Cryptr values are encrypted values, not plaintext.
~n/dev/work/reflection/js/reflection_backend/functions/src/helpers/encryption.spec.js:- legacy tests cover non-empty encrypt/decrypt but not empty encrypt/decrypt.
- Firestore REST read of the cited entry key
055b418d-e5fb-4014-8a44-e8a3fbf1c794showed:textlength 196, standard encrypted shape, decrypts cleanly to valid HTML.reviewlength 32, hex-looking IV-only shape.
- Local OpenSSL AES-256-CTR probe using the production Secret Manager key material only in-process, without recording the secret, showed:
- standard parse of that 32-character review as
IV + empty ciphertextyields empty plaintext. - current legacy-style parse of the same value yields invalid UTF-8 bytes that render as replacement characters.
- standard parse of that 32-character review as
- Firestore projection query against the current prod test user, selecting only
key,type, andreview, found 25 of 50 sampled entries withreviewlength 32 and 25 with empty review. No journal text or review content was selected. - Attempted local JS helper execution failed because the local legacy JS workspace lacks the
cryptrdependency, so legacy behavior is grounded in inspected source and existing tests rather than a local JS runtime run.
| id | source | decision | location | current assumption | why it matters now | pressure signal |
|---|---|---|---|---|---|---|
| B1 | existing code | Encryption format ownership is centralized in EncryptionService, which exposes only Encrypt and Decrypt to callers |
internal/infrastructure/encryption/encryption_service.go |
Callers should not know IV/ciphertext layout details | The exact-32 ambiguity is a crypto-format interpretation problem, so the first design question is whether to fix it at this boundary or leak format checks into entry shaping | If a future field needs field-specific crypto semantics, EncryptionService may be too generic and need a typed encrypted-field wrapper |
| B2 | existing code | Go standard encryption emits hex(16-byte IV) + hex(ciphertext) |
Encrypt in internal/infrastructure/encryption/encryption_service.go |
Any valid standard encrypted value has at least a 32-character IV prefix; empty ciphertext is representable as exactly 32 characters | The current Decrypt condition len(value) > 32 excludes the valid IV-only representation |
If evidence shows the Go/JS standard cannot produce IV-only values, this design should be revisited |
| B3 | existing code | Decrypt attempts standard parsing only for values longer than 32 characters, then falls back to legacy parsing |
Decrypt in internal/infrastructure/encryption/encryption_service.go |
Encrypted values always contain at least one ciphertext byte after the IV | This assumption is too narrow for encrypted empty strings and causes exact-32 values to be treated as legacy ciphertext | If production data contains legitimate 8-byte-IV legacy values of length 32, exact-32 parsing is ambiguous and needs an explicit compatibility decision |
| B4 | existing code | The legacy fallback supports 8-byte IVs by padding them to AES block size | Decrypt in internal/infrastructure/encryption/encryption_service.go |
Some older stored values may not use the standard 16-byte IV format | The fix must not delete broad legacy support for values that genuinely fail standard parsing | If logs or fixtures prove common 8-byte-IV values, add explicit tests before changing fallback behavior |
| B5 | existing code | Empty stored entry fields bypass decryption; decrypt errors fall back to ciphertext | EntryReadShaper.decryptFieldSafely in internal/entries/application/entry_read_shaper.go |
Returning stored ciphertext is safer than failing the whole entry list when decryption errors | Exact-32 values currently do not error; they produce garbage plaintext, so this safety net does not activate | If more bad decrypts return syntactically valid but semantically wrong text, the shaper may need validation after decryption |
| B6 | existing code | Go create/update normalizer only encrypts review when the incoming review is non-empty |
internal/entries/application/entry_write_normalizer.go |
New Go writes should not create encrypted-empty review values | The bug is primarily about reading legacy/prod data, not new Go write behavior | If Flutter starts sending meaningful empty-review tombstones that must be encrypted, write normalization may need a separate decision |
| B7 | existing code | UserEntries response preserves the legacy client-visible review string field |
internal/entries/domain/user_entries_types.go, internal/entries/interfaces/cloudfunction/user_entries_handler.go |
Flutter owns model parsing; backend owns decrypted field values and stable field names | The fix should make review empty when there is no review data, not alter the envelope or add a new field |
If Flutter changes to tolerate malformed review strings, backend should still preserve the cleaner contract rather than rely on client tolerance |
| B8 | existing code | Firestore mapping passes review through as a string without interpreting encryption format |
internal/entries/infrastructure/stored_entry_mapper.go |
Persistence mapping should coerce storage types, not decrypt or validate field semantics | The mapper should remain unchanged; moving the fix here would mix persistence coercion with crypto semantics | If Firestore starts storing typed encrypted blobs instead of strings, mapper ownership would need review |
| B9 | existing code | Flutter treats empty review as Review.none() and non-empty review for standard entry types as JSON |
~/.codex/worktrees/4f55/flutter/lib/features/entries/models/entry.dart |
Backend returns either empty string or valid review JSON for standard entry types | Returning invalid non-empty review violates this contract and triggers the observed crash path | If Flutter changes the model contract to accept opaque review text, the backend contract still needs explicit renegotiation |
| B10 | existing code | Legacy JS encryption helper treats strings with length at least 32 and no spaces as encrypted values | ~/dev/work/reflection/js/reflection_backend/functions/src/helpers/encryption.js |
A 32-character no-space string is a decryptable encrypted representation, not plaintext | The Go compatibility layer should not reject exact-32 encrypted values before standard decrypt | If legacy Cryptr is proven to use a different empty-string format in production, this assumption must be retested |
| B11 | existing tests | Existing Go encryption tests cover non-empty compatibility but not IV-only ciphertext | internal/infrastructure/encryption/encryption_service_test.go |
Round-trip tests with non-empty text are enough to prove compatibility | The current bug passed because exact-32 was not in the test matrix | If tests remain only integration/live-data based, this bug can regress silently |
| B12 | issue doc | Entry migration requires one shared read-shaping seam for decrypting stored entry fields and endpoint-specific DTOs above it | .ai/issues/open/ISSUE-006-entry-migration-cross-cutting-shared-seams.md |
Read decryption/defaulting should not be duplicated per endpoint | A field-specific workaround in UserEntries would violate the shared seam and leave future entry read paths inconsistent |
If only one endpoint ever reads this field, shared seam pressure weakens, but current docs say shared read ownership is intentional |
For a future implementation, EncryptionService.Decrypt should treat a valid 32-character hexadecimal string as the standard AES-CTR representation hex(16-byte IV) + hex(empty ciphertext) and return the empty string.
More precisely:
- Preserve the current behavior for
value == "": return the existing invalid-data error. Entry read shaping already converts empty stored fields to empty strings before decrypting. - Attempt standard format parsing for any value with
len(value) >= 32, not onlylen(value) > 32. - Standard parse succeeds only when:
- the first 32 characters decode to a 16-byte IV;
- the remaining string is valid hex, including the allowed empty remainder;
- the remainder length is even.
- If standard parse succeeds with an empty ciphertext remainder, AES-CTR decrypt returns
"". - If standard parse fails, preserve the existing legacy fallback path and 8-byte-IV padding behavior.
- Do not change the
UserEntriesresponse shape, Flutter parser, Firestore data, or entry mapper.
This changes the narrowest correct ownership boundary: the crypto-format parser. It avoids adding len(review) == 32 checks in EntryReadShaper, UserEntries, or Flutter.
internal/infrastructure/encryption/encryption_service.go- Owns AES-256-CTR format parsing and compatibility with legacy Cryptr-like stored values.
- Should receive the semantic fix.
- A small private parsing helper is acceptable if it improves readability and makes tests precise, for example
parseStandardCiphertext(value string) (iv []byte, ciphertext []byte, ok bool, err error).
internal/infrastructure/encryption/encryption_service_test.go- Should add deterministic tests for the exact-32 IV-only case using the existing mocked secret-manager test service.
- Should add a regression test that invalid short/non-hex values still error as before.
- Should add a compatibility assertion that a known non-empty legacy fixture still decrypts.
internal/entries/application/entry_read_shaper.go- Should remain a field shaper and error fallback boundary, not a crypto-format parser.
internal/entries/infrastructure/stored_entry_mapper.go- Should remain storage coercion only.
internal/entries/interfaces/cloudfunction/user_entries_handler.goandhttp_user_entries_handler.go- Should remain thin HTTP wiring.
- Flutter files under
~/.codex/worktrees/4f55/flutter- Should not carry backend decryption compatibility workarounds for this defect.
| id | decision | location | reason | assumption | related baseline ids |
|---|---|---|---|---|---|
| D1 | Fix exact-32 handling inside EncryptionService.Decrypt by allowing standard parsing for len(value) >= 32 |
internal/infrastructure/encryption/encryption_service.go |
The encrypted-value format is owned by the encryption service; this keeps crypto mechanics out of entry and Flutter layers | Exact-32 valid hex is the standard IV-only representation for encrypted empty plaintext | B1, B2, B3, B10 |
| D2 | Preserve the legacy fallback path for values whose standard parse fails | internal/infrastructure/encryption/encryption_service.go |
Avoids broad compatibility regression for older stored values while correcting the standard parser gate | Genuine legacy 8-byte-IV values either fail standard parsing or are rare enough to be covered by explicit fixtures | B3, B4 |
| D3 | Do not add a review-specific workaround in EntryReadShaper or UserEntries |
internal/entries/application/entry_read_shaper.go, internal/entries/interfaces/cloudfunction/user_entries_handler.go |
A local workaround would duplicate crypto-format knowledge and leave other decrypt callers inconsistent | The bug is format-level, not entry-list-specific | B5, B7, B8, B12 |
| D4 | Add deterministic unit coverage for IV-only encrypted values in the encryption package | internal/infrastructure/encryption/encryption_service_test.go |
The current test matrix missed the exact boundary case; a package unit test prevents false green live-only validation | The existing mocked test service can construct a service with stable key material without Secret Manager | B11 |
| D5 | Use live validation only as a post-unit confirmation and never mutate Firestore | UserEntries deployed endpoint, Firestore projection probes |
The defect manifests with production data, but the safe fix should be read-only and data-preserving | A read of the cited entry after deployment is sufficient to prove client-visible shape changed from garbage to empty review | B5, B6, B7, B9 |
| D6 | Keep the UserEntries wire contract unchanged: review remains a string and is empty when no review data exists |
internal/entries/domain/user_entries_types.go, Flutter entry.dart parser |
The migrated endpoint promise is stable legacy-compatible row shape; changing DTO fields would widen blast radius | Flutter expects empty string or valid review JSON for standard entry types | B7, B9, B12 |
| review id | decision id | trigger | finding | action | design update required |
|---|---|---|---|---|---|
| C1 | B1 | The proposed fix touches encryption parsing | The existing central encryption boundary is exactly the right place for the behavior; adding entry-level parsing would create a parallel crypto path | keep | Final design keeps exact-32 parsing in EncryptionService.Decrypt through D1 |
| C2 | B2 | Exact-32 values are valid under the documented standard IV layout | The existing assumption that standard values must be longer than 32 is too narrow | rewrite | D1 rewrites the parser gate from > 32 to >= 32 while preserving the same standard format |
| C3 | B3 | The current fallback receives exact-32 values before standard parsing | Keeping this decision unchanged forces the implementation to add field workarounds elsewhere | rewrite | D1 and D2 update the parser order so standard parse gets first chance for exact-32 values and fallback remains only after standard parse failure |
| C4 | B4 | Legacy fallback remains relevant | The design reuses the same fallback behavior and does not delete 8-byte-IV compatibility | keep | D2 explicitly preserves fallback behavior for values that fail standard parsing |
| C5 | B5 | Entry read shaper currently cannot detect garbage plaintext returned without error | Adding plaintext validation in the shaper would be broader and less precise than fixing the parser | keep | D3 keeps the shaper unchanged because D1 prevents this specific garbage plaintext |
| C6 | B6 | Go writes skip empty review encryption | This decision still fits and limits future creation of exact-32 review values by Go writes | keep | No code shape change required; validation should still cover legacy/prod reads |
| C7 | B7 | Wire contract must stay stable | Changing the DTO would force Flutter and backend contract changes near the same path | keep | D6 keeps review as a string with empty meaning no review data |
| C8 | B8 | Mapper passes raw review string forward | Persistence mapping does not need crypto knowledge; keeping it avoids mixing responsibilities | keep | D3 leaves mapper ownership unchanged |
| C9 | B9 | Flutter parses non-empty standard-entry review as JSON | The backend must satisfy the existing client contract rather than requiring Flutter tolerance | keep | D6 explicitly targets empty string or valid JSON |
| C10 | B10 | Legacy JS treats length-32 no-space strings as encrypted | The Go parser should be widened to match compatibility expectations; local JS execution was unavailable, so source evidence and prod data guide this decision | keep | D1 implements the compatibility interpretation in Go; validation includes live endpoint proof |
| C11 | B11 | Existing tests missed exact boundary | Keeping only non-empty tests would leave the same false-green risk | rewrite | D4 adds deterministic IV-only coverage |
| C12 | B12 | Shared read seam discourages per-endpoint fixes | A UserEntries-only branch would violate the shared seam and create nearby duplicated behavior |
keep | D3 keeps endpoint/read-shaper code free of crypto-format special cases |
| C13 | D1 | Proposed parser gate could affect all decrypt callers | This is intentional because the invalid assumption lives in the shared parser; blast radius is constrained by tests and by only changing successful standard parsing for valid IV-only values | keep | Validation plan includes encryption package tests plus a live UserEntries probe |
| C14 | D2 | Legacy and standard exact-32 formats are theoretically ambiguous | The design preserves legacy fallback for parse failures but exact-32 valid hex cannot be both standard-detected and legacy-detected without external metadata | defer | Risk owner: implementation reviewer. Before merge, scan or sample production for known 8-byte-IV length-32 values outside empty reviews if evidence suggests such values exist; otherwise accept standard-first compatibility as the cleaner contract |
| C15 | D3 | Avoiding a local workaround means fixing a shared service | Shared service fix is smaller and more consistent than a review-field branch | keep | No additional local branch is needed |
| C16 | D4 | New unit tests touch existing package test setup | The package already has one TestMain; tests must reuse it and not add another |
keep | Implementation guidance calls out no additional TestMain |
| C17 | D5 | Live validation reads production data | Read-only projection and endpoint probes are sufficient; no data write or migration is needed | keep | Validation plan forbids Firestore mutation |
| C18 | D6 | Keeping the wire contract unchanged constrains any frontend workaround | That constraint is correct because the issue is backend compatibility, not a new client contract | keep | Final behavior keeps the row shape stable |
Compression actions taken in this design:
- Rewrote the too-narrow standard parser assumption from
len > 32tolen >= 32in the proposed behavior. - Rewrote the test strategy to add deterministic boundary coverage at the encryption package, rather than relying on live Flutter reproduction.
- Deferred only the theoretical exact-32 legacy 8-byte-IV ambiguity, with a named implementation-review owner and risk check.
- What functional behavior must change? Exact-32 encrypted values that represent empty plaintext must decrypt to
"", so Flutter seesReview.none()rather than invalid JSON. - Does B3 make an existing assumption too narrow? Yes.
len(value) > 32assumes at least one ciphertext byte; encrypted empty plaintext violates that. - Should the fix live in entry read shaping? No. B1 and B12 point to centralized encryption and shared read seams; a field workaround would be a parallel path.
- What edge case matters most? Ambiguous exact-32 valid hex could theoretically be 8-byte-IV legacy data. The design handles this as a review risk, not by scattering workaround branches.
- What proof is needed? Unit proof of IV-only decrypt plus live proof that the cited
UserEntriesrow returns empty review.
Research added in this iteration: PROJECT.md, AGENTS.md, encryption service/tests, entry read shaper, entry write normalizer, user entries types, Flutter entry parser, legacy JS encryption helper, and Firestore/OpenSSL probes.
- Does D1 touch a broad shared service? Yes, but it touches the exact ownership boundary for encrypted format parsing and avoids broader endpoint/client changes.
- How does D2 protect B4? Legacy fallback remains for values that fail standard parsing, including short or malformed standard values.
- Can D4 avoid adding fragile Secret Manager dependence? Yes. Existing mocked secret-manager tests can construct
EncryptionService; no newTestMainis needed. - Does B6 mean Go cannot create this defect going forward? Mostly for review fields, but legacy/prod data remains and other encrypted fields can still hit the parser boundary.
- What false-green risk remains? A test that encrypts only non-empty strings would pass while exact-32 remains broken. The validation plan requires an explicit constructed exact-32 fixture.
Research added in this iteration: issue docs for entry list and shared seams, direct HTTP architecture, Firestore review length projection, and git history/blame for the decrypt parser boundary.
- No persistent application state should be migrated.
- No Firestore documents should be updated.
- No replay/idempotency path changes are required.
- Existing entry create/update replay hashes are unaffected because this is read-path decryption behavior only.
- Cloud Function lifecycle is unchanged; only deployed code behavior inside the existing encryption service would change after implementation.
- Flutter UI behavior should improve without any UI code change: affected entries should parse instead of crashing or being dropped due to invalid review JSON.
- No command/tool API is affected.
- Google Secret Manager remains the provider for encryption key retrieval; the design does not change secrets access.
- Firestore remains the persistence provider; the design uses only read-only validation probes.
- Browser/web platform impact is limited to avoiding invalid replacement-character strings reaching
jsonDecodein Flutter web.
| case | expected future behavior | validation |
|---|---|---|
| Empty stored string | EntryReadShaper returns "" before decrypt; direct Decrypt("") keeps existing invalid-data error |
Existing shaper tests plus no encryption behavior change |
| Valid exactly 32 hex characters | Standard parse as IV-only encrypted value and return "" |
New deterministic encryption unit test |
| Valid longer standard encrypted value | Decrypt as before | Existing round-trip/fixture tests |
| Invalid non-hex string shorter than 16 | Return invalid encrypted value error as before | Existing decrypt test |
| Invalid non-hex string length 16 or more | Attempt legacy and return decode error as before | Existing/new regression test |
| Genuine legacy 8-byte-IV value longer than 32 | If standard parse fails, legacy fallback handles it as before | Existing legacy fixture test |
| Theoretical genuine legacy 8-byte-IV value exactly 32 | Ambiguous; standard-first design would return empty if all 32 chars are valid hex | Deferred risk owned by implementation reviewer; sample data or fixture evidence should trigger reassessment |
| Decrypt returns invalid UTF-8 without error from another malformed value | Existing shaper still cannot detect this generically | Out of scope unless reproduced after exact-32 fix |
Run from the backend repo:
go test ./internal/infrastructure/encryption -run 'TestCryptrService_Decrypt|TestCryptrService_EncryptionCompatibility|TestCryptrService_LegacyDecryptionCompatibility' -count=1Required new/updated test assertions:
- Construct an
encryptionServicewith a deterministic test key or use the existingcreateTestServicehelper. - Build an exact-32 value from a known 16-byte IV and no ciphertext.
- Assert
Decrypt(exact32)returns""with no error. - Assert non-empty encrypted round trips still pass.
- Assert the known legacy fixture still decrypts.
- Assert invalid values from the existing test table keep returning errors.
- Do not add a second
TestMainininternal/infrastructure/encryption; the package already owns one inencryption_test.go.
If implementation changes only encryption service, entry application tests do not need new format-specific stubs. Optional but useful:
go test ./internal/entries/... -count=1This confirms shared entry read shaping still compiles and existing list/shared tests keep passing. It does not replace the encryption package boundary test because entry tests use stubs and cannot prove AES parsing.
Read-only validation only:
- Call deployed
UserEntriesfor the authenticated test account that reproduced the crash. - Inspect the cited entry key
055b418d-e5fb-4014-8a44-e8a3fbf1c794in the response. - Confirm its
reviewis""and itstextis still valid decrypted HTML. - Reload Flutter web journal page and confirm entries load without the replacement-character JSON error.
- Optionally run a Firestore projection query selecting only
key,type, andreviewlengths before/after for sample accounting; do not select journal text and do not mutate data.
- Passing
go test ./internal/entries/...alone can be false green because entry tests stub encryption. - Passing non-empty encryption round trips alone can be false green because the boundary bug is exact length 32.
- A successful endpoint HTTP 200 can be false green if individual
reviewfield values are not inspected. - A Flutter reload can be false green if the local database cache is not refreshed from remote entries.
- Open
internal/infrastructure/encryption/encryption_service.go. - Refactor the standard parse condition from
len(value) > 32tolen(value) >= 32, preferably via a small private helper that makes the exact-32 case obvious. - Ensure an empty
value[32:]remainder is valid ciphertext input and results in an empty plaintext byte slice. - Preserve the existing legacy fallback when standard parse fails.
- Add the deterministic exact-32 decrypt unit test in
internal/infrastructure/encryption/encryption_service_test.gowithout adding a new packageTestMain. - Run the encryption package test gate.
- Run targeted entry tests if implementation touched any entry code; otherwise run them as a confidence check, not as the primary proof.
- Deploy the affected endpoints only after unit proof is green. Since
EncryptionServiceis linked into multiple functions, at minimum redeployUserEntries; consider whether other already-deployed entry read functions also need redeploy if they share the same defect path. - Perform read-only live validation for the cited entry and Flutter journal page.
- The only unresolved risk is the theoretical existence of meaningful 8-byte-IV legacy values whose total encoded length is exactly 32. The implementation reviewer owns the decision to accept standard-first behavior or pause for a broader production sample if new evidence appears. Current evidence favors standard-first because the reproduced production values are empty reviews and legacy JS treats length-32 no-space strings as encrypted values.
PROJECT.mdDDD guidance: satisfied. The design keeps HTTP handlers thin, avoids adding logic to top-level handlers, and places encryption-format behavior in infrastructure.PROJECT.mdencryption compatibility guidance: satisfied. The design restores compatibility for a valid IV-only encrypted representation.PROJECT.mdtesting guidance: satisfied. The design uses targeted package tests and avoids broad root-level tests that can trigger unrelated init/Secret Manager failures.AGENTS.mdsearch guidance: satisfied.PROJECT.mdwas searched before exploratory repository search.- Entry migration issue guidance: satisfied. The design preserves stable
UserEntriesrow shape and shared read-shaping boundaries. - Style guide files:
.ai/docs/architecture-and-system-design-style.mdand.ai/docs/code-style.mdare absent, so no additional local style constraints apply.