Skip to content

Commit 712e294

Browse files
ericmigiclaude
andcommitted
README: soften unverified claims; add ENCRYPTED-field clarification
Audit pass against actual code surfaced overconfident claims: - "Create new note Works only with v4 UUIDs" → softened to "code path works; end-to-end Mac round-trip not yet user-confirmed". The v4 fix is a strong hypothesis from byte-diff, not a confirmed end-to-end test. - Headline section now flags as "what we think we learned" with explicit caveat. Lists three follow-up investigations if v4 alone isn't enough (substring tree shape, attribute_runs count, counter2 field). - Delete: forceDelete doesn't truly wipe; Mac surfaces it as Recently Deleted. Reworded. - Auto-save: link to actual line in AppleNotesApp.kt for verifiability. Added "A note on ENCRYPTED field names" section: the field names are misleading, content is plain base64-encoded UTF-8 / gzipped proto. Removed "does not break encryption" from disclaimer since there's no client-side encryption to begin with. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 1aa6465 commit 712e294

1 file changed

Lines changed: 44 additions & 17 deletions

File tree

README.md

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,37 @@ report for the next person (or agent) who picks it up.
1717
| Read note content (decode topotext proto) | Works for `NOTE_STORE_PROTO`; not the modern `MergableData` shape |
1818
| Append text | Works (round-trips with Mac) |
1919
| Mid-text splice (insert/replace/delete) | Works (matches iCloud.com's slot-promotion pattern) |
20-
| Create new note (FAB → save) | Works **only with v4 UUIDs** — see "The UUID gotcha" below |
21-
| Auto-save on lifecycle pause | Works |
22-
| Share OUT (Android intent chooser) | Works (plaintext) |
23-
| Delete | Works (`forceDelete` removes the record entirely) |
20+
| Create new note (FAB → save) | Code path works (deferred-create + v4 UUID); end-to-end Mac round-trip not yet user-confirmed |
21+
| Auto-save on lifecycle pause | Works (lifecycle `ON_PAUSE` only — see [AppleNotesApp.kt:1052](app/src/main/java/com/example/applenotes/ui/AppleNotesApp.kt)) |
22+
| Share OUT (Android intent chooser) | Works (plaintext only) |
23+
| Delete | Uses CloudKit `forceDelete`; Mac surfaces as "Recently Deleted" (server-managed, not a hard wipe) |
2424
| Format display (headings, lists, checkboxes) | Decoded but not rendered yet (Phase D1 done; D2 pending) |
2525
| Format input toolbar | Not built |
2626
| Image attachments | Renders `` stub; no image display |
2727
| Conflict UX | Auto-retry on CONFLICT/oplock; no concurrent-edit merge yet |
2828

29-
## The most important thing you'll learn here
30-
31-
**Apple's replica UUID must be RFC 4122 v4.** This is not documented anywhere.
32-
We spent hours diffing protos before noticing.
33-
34-
When you create a note from a foreign client, Apple Notes on Mac runs notesync,
35-
adds housekeeping fields, and **silently sets `Deleted=1`** if the sole replica's
36-
UUID isn't v4 (byte 6 high nibble = 0x4, byte 8 high two bits = 0b10). The body
37-
proto is left intact. The note appears in Recently Deleted within a few minutes.
29+
## The most important thing we think we learned (not yet end-to-end confirmed)
30+
31+
**Strong hypothesis: Apple's replica UUID must be RFC 4122 v4.** This is the
32+
last byte-level difference between an Android-created note that got trashed
33+
(`ab-fresh-162944`) and an iCloud.com-created note that survived (`ic-create-test-001`).
34+
Both had identical proto structure, same fields, same compression. Only the
35+
UUID's version nibble differed.
36+
37+
When we created a note from Android with our previous (raw-random) UUID, Mac's
38+
notesync added housekeeping fields, **left the body proto bytes intact**, and
39+
**silently set `Deleted=1`** within a few minutes — the note appeared in
40+
Recently Deleted on Mac. iCloud.com-created notes (which use v4 UUIDs) survive
41+
the same flow.
42+
43+
**Caveat**: we shipped the v4 UUID fix in commit `3ee1a60` but haven't yet
44+
confirmed end-to-end that a v4-UUID-created note survives Mac quit/reopen. If
45+
you pick this up and v4 turns out not to be the only thing Mac validates, the
46+
next things to investigate are (a) the substring tree shape (iCloud.com always
47+
emits multiple substrings with tombstone separators around line breaks; we emit
48+
one substring), (b) attribute_runs count (iCloud.com splits at trailing `\n`;
49+
we don't), and (c) the per-replica `counter2` field (iCloud.com uses a
50+
non-trivial value; we use 1).
3851

3952
`SecureRandom().nextBytes(16)` produces raw bytes — only ~1/16 chance the version
4053
nibble is 4. Use `java.util.UUID.randomUUID()`, or set the bits explicitly:
@@ -330,13 +343,27 @@ empty-body new-note flow). Don't relitigate without reason.
330343
- **No subscription / push.** Polls when the user opens the app. Background sync,
331344
CloudKit subscriptions, and a send queue are all unimplemented.
332345

346+
## A note on "ENCRYPTED" field names
347+
348+
CloudKit serializes `TitleEncrypted`, `SnippetEncrypted`, and `TextDataEncrypted`
349+
as `ENCRYPTED_STRING` / `ENCRYPTED_BYTES` types — but the actual byte values
350+
sent over the wire are just **base64-encoded UTF-8** (for Title/Snippet) and
351+
**gzipped protobuf** (for TextData). There is no client-side encryption layer
352+
involved. The names appear to be a CloudKit historical artifact; iCloud's E2EE
353+
("Advanced Data Protection") is a separate keystore mechanism we're not
354+
exercising.
355+
356+
If you `kotlin.io.encoding.Base64.decode(titleField.value).decodeToString()`
357+
you get the cleartext title. That's the entire "decryption" step.
358+
333359
## Disclaimer
334360

335361
This project is unaffiliated with Apple. It uses Apple's public CloudKit Web
336-
Services API with the user's own credentials. It does not break encryption — it
337-
reads notes the authenticated user already has access to. The proto formats
338-
were inferred from observation of the user's own data round-tripping with their
339-
own iCloud account; no Apple-internal documentation was consulted.
362+
Services API with the user's own credentials. It reads and writes notes the
363+
authenticated user already has access to. The proto formats were inferred from
364+
observation of round-tripping the user's own data through their own iCloud
365+
account; no Apple-internal documentation or reverse-engineered binaries were
366+
consulted.
340367

341368
If you're going to publish anything based on this, be respectful. Don't redistribute
342369
captured proto data from other users' accounts. Don't ship anything that would

0 commit comments

Comments
 (0)