TL;DR: iOS 15 brought more forensics-meaningful changes to Apple Notes than recent major version releases.
Structural Changes
Between iOS 14.8 and iOS 15.0, Apple added the following 25 columns Apple Notes’ ZICCLOUDSYNCINGOBJECT
table, bringing that massive table to 181 columns. This post will examine how each of those columns is used in the new features added to Apple Notes.
ZACCOUNT5
ZCREATIONDATE2
ZCREATIONDATE3
ZDISPLAYTEXT
ZISSYSTEMPAPER
ZLASTACTIVITYRECENTUPDATESVIEWEDDATE
ZLASTACTIVITYSUMMARYVIEWEDDATE
ZLASTATTRIBUTIONSVIEWEDDATE
ZLINKPRESENTATIONARCHIVEDMETADATA
ZMENTIONNOTIFICATIONATTEMPTCOUNT
ZMENTIONNOTIFICATIONSTATE
ZPAPERASSETSURL
ZPAPERDATABASEURL
ZREPLICAIDTOBUNDLEIDENTIFIER
ZSERVERSIDEUPDATETASKCONTINUATIONTOKEN
ZSERVERSIDEUPDATETASKFAILURECOUNT
ZSERVERSIDEUPDATETASKLASTATTEMPTEDBUILD
ZSERVERSIDEUPDATETASKLASTATTEMPTEDVERSION
ZSERVERSIDEUPDATETASKLASTCOMPLETEDBUILD
ZSERVERSIDEUPDATETASKLASTCOMPLETEDVERSION
ZSMARTFOLDERQUERYJSON
ZSTANDARDIZEDCONTENT
ZSYNAPSEDATA
ZTOKENCONTENTIDENTIFIER
ZURLEXPIRED
Changelog
Apple’s changelog for iOS 15 lists the following updates for Notes1 and these categories are the primary way we will look at the new columns:
- Tags
- Tag Browser
- Custom Smart Folders
- Activity View
- Highlights
- Mentions
- Quick Note: Easy to Access
Tags
Apple users can now use hashtags to organize their notes. Under the hood, Apple created an entirely new uniform type identifier (UTI) to represent this object: com.apple.notes.inlinetextattachment.hashtag
and these appear to be the most important columns from ZICCLOUDSYNCINGOBJECT
for hashtags:
Column | New? | Description |
---|---|---|
ZALTTEXT | Yes | The ZALTTEXT column contains the text that is actually displayed in the note itself, cased as it was entered. |
ZCREATIONDATE2 | Yes | The ZCREATIONDATE2 column indicates when this embedded object was created. |
ZNOTE1 | While other embedded objects point to the note they belong to in the ZNOTE column, hashtags and mentions use ZNOTE1 . | |
ZTOKENCONTENTIDENTIFIER | Yes | The ZTOKENCONTENTIDENTIFIER column contains the fully-capitalized version of the hashtag, minus the hash symbol, likely as a normalized way of looking up the tag. |
ZTYPEUTI1 | Because this UTI behaves differently than other embedded objects, it is in the ZTYPEUTI1 column2 instead of ZTYPEUTI as other embedded objects use. |
To see which notes used which hashtags, you can run the following SQLite query on the NoteStore.sqlite
database:
ZNOTE1 | ZALTTEXT | ZTOKENCONTENTIDENTIFIER |
---|---|---|
35 | #NewHashtag | NEWHASHTAG |
190 | #Test | TEST |
212 | #Test | TEST |
215 | #Test | TEST |
220 | #Test2 | TEST2 |
222 | #NewHashtag | NEWHASHTAG |
224 | #Test | TEST |
228 | #Test2 | TEST2 |
231 | #Test | TEST |
272 | #NewHashtag | NEWHASHTAG |
Tag Browser
While tags made an interesting change to the Notes database, the tag browser itself is mainly a UI change that reflects the addition of tags. When you create a tag, an entry is created in the ZICCLOUDSYNCINGOBJECT
database with a Z_ENT
value of 7, the ZDISPLAYTEXT
column set to the hashtag as you entered it, and the ZSTANDARDIZEDCONTENT
column set to an all capital letters version of the hashtag, if the language has capital letters. The overall tag creation time appears to now be stored in the ZCREATIONDATE1
field, which was used prior to iOS 15 for the overall note creation time. I couldn’t find anywhere these appear to be referenced later by the notes or actual use of the hashtag, so these likely are how the UI is able to display the tag choices to the user.
Z_PK | Z_ENT | ZDISPLAYTEXT | ZSTANDARDIZEDCONTENT | ZCREATIONDATE1 |
---|---|---|---|---|
209 | 7 | Test | TEST | 654049803.242126 |
218 | 7 | NoNotes | NONOTES | 654050083.315863 |
219 | 7 | Test2 | TEST2 | 654050096.184235 |
247 | 7 | NewHashtag | NEWHASHTAG | 654050173.539261 |
Custom Smart Folders
Custom smart folders allow you to define what you want to see in a folder without having to move notes to the folder itself. Unlike other folders where each note will point to one folder’s ZICCLOUDSYNCINGOBJECT.Z_PK
, the contents of a smart folder are identified by looking at the JSON contained in the ZSMARTFOLDERQUERYJSON
column. You can select the folder’s settings with this SQLite query:
In my example, I named the folder “New Smart Folder for Tags” and added three tags to it: “#Test2”, “#NoNotes”, and “#Test”. The JSON that is found in the new column was as follows:
Activity View
Apple added in the ability to see who edited what on a given note. This obviously adds potentially rich insight to the note contents, but took a lot of time to experiment with. At the end of that time, I felt that the information present could mostly be found using other aspects of the database in an easier way. These appear to be the most important columns from ZICCLOUDSYNCINGOBJECT
for the activity view:
Column | New? | Description |
---|---|---|
ZACTIVITYEVENTSDATA | GZIP’d protobuf that possibly conforms to the same proto definition that mergeable items use3. | |
ZLASTACTIVITYRECENTUPDATESVIEWEDDATE | Yes | Timestamp indicating when the user viewed updates on the note. |
ZLASTACTIVITYSUMMARYVIEWEDDATE | Yes | Timestamp indicating when the user viewed the activity summary. |
ZLASTATTRIBUTIONSVIEWEDDATE | Yes | Timestamp indicating when the user viewed who edited the note. |
Most of these are straight forward, although the already existing ZACTIVITYEVENTSDATA
warrants a comment. While this GZIP’d protobuf appeared interesting at first, it seems to contain minimal “new” information in and of itself. For example, at its heart are messages that point to the folder, note, and participant’s identities, but those are already referenced by other columns on the same row. Consider these JSONs from one of my test notes and the value they give, as compared to the columns noted below in the “Mentions” section.
{
"activity": {
"type": "mention",
"participants": [
{
"recordName": "_12345678901234567890123456789012",
"mentionRecordName": "7B9C22DD-B791-43F5-ADD0-961A042F7D43"
}
]
},
"object": {
"type": "note",
"recordName": "0AE73BAB-A205-4EDD-8885-103BE3EDC00B"
},
"timestamp": 656723690.242377,
"participant": {
"recordName": "_09876543210987654321098765432109"
}
}
{
"object": {
"type": "note",
"recordName": "0AE73BAB-A205-4EDD-8885-103BE3EDC00B"
},
"timestamp": 656723690.242377,
"minimumSupportedNotesVersion": 6,
"typeName": "mention",
"participant": {
"recordName": "_09876543210987654321098765432109"
},
"localizedFallbackTitle": "Somebody mentioned 1 people"
}
Highlights
Similar to the tag browser and tags, highlights reflects more of a UI change to reveal activity to the user in an easy way, not a significant change to the underlying database.
Mentions
Mentions have a potentially significant impact on reviewing notes because, similar to tags, they add a new type of inlined, embedded object which won’t show up if you just look at the text of a note. Further, unlike tags, the object’s token is only a pointer to the person being mentioned, you’ll need to do some CloudKit work to determine the actual account being referenced. The new UTI for mentions is: com.apple.notes.inlinetextattachment.mention
and these appear to be the most important columns from ZICCLOUDSYNCINGOBJECT
for mentions:
Column | New? | Description |
---|---|---|
ZALTTEXT | Yes | The ZALTTEXT column contains the text that is actually displayed in the note itself, typically @[User first name] . |
ZCREATIONDATE2 | Yes | Just like tags, the ZCREATIONDATE2 column indicates when this embedded object was created. |
ZMENTIONNOTIFICATIONATTEMPTCOUNT | Yes | This column indicates how many times the mentioned user was attempted to be contacted. |
ZMENTIONNOTIFICATIONSTATE | Yes | This column likely indicates whether the mentioned user was notified of the mention yet. I have observed values 1, 2, and 3 where 3 might indicate the user was notified and 2 likely indicates at least one attempt was made. I need a lot more iCloud accounts to play with. |
ZNOTE1 | Just like tags, mentions use ZNOTE1 to denote which note they belong to. | |
ZTOKENCONTENTIDENTIFIER | Yes | Unlike tags, the ZTOKENCONTENTIDENTIFIER column for mentions contains a pointer the CloudKit identifier for the account, not something immediately recognizable as the account username. |
ZTYPEUTI1 | Just like tags, mentions use the ZTYPEUTI1 column instead of ZTYPEUTI to denote their type. |
To see which notes mentioned which users, you could run the following:
ZNOTE1 | ZALTTEXT | Account | Notifications | State |
---|---|---|---|---|
16 | @Tom | _09876543210987654321098765432109 | 1 | 2 |
222 | @Tom | _09876543210987654321098765432109 | 1 | 2 |
224 | @Mark | _12345678901234567890123456789012 | 0 | 3 |
228 | @Tom | _09876543210987654321098765432109 | 1 | 2 |
If you want to know how to move further with that CloudKit ID, it would be worthwhile to read this post about CloudKit data. Apple Notes Parser cheats slightly and reads the CloudKit data from the ZSERVERSHAREDATA
column to then look up the value. For the sake of this experiment, it is likely only important to know that “@Mark” was the local test account and “@Tom” was a test account that was not logged into. As such, I am assuming that a state of ‘3’ either indicates the user was notified or that it is a local account and ‘2’ indicates that the user was attempted to be notified.
Quick Note: Easy to access
Like the tag browser and highlights, this is a UI change, not a significant change. I did not find any database changes that I thought were related to this.
Other Changes
ZACCOUNT*
Every other major release of iOS brings a new ZACCOUNT
field and now we’re adding ZCREATIONDATE
and ZTYPEUTI
as well. I have yet to decide if I think this is an intentional design choice by the Apple developers or an accidental result of some build tool that the developer doesn’t realize is occuring under the hood. As of iOS 15, we are up to ZACCOUNT5
, and this is getting slightly ridiculous.
[notta@cuppa ~]$ awk -F '|' '{print $2}' notestore_15.0.schema \
| grep "ZACCOUNT\\|ZCREATIONDATE\|ZTYPEUTI"
ZACCOUNT
ZACCOUNT1
ZACCOUNT2
ZACCOUNT3
ZACCOUNT4
ZACCOUNT5
ZCREATIONDATE
ZCREATIONDATE1
ZCREATIONDATE2
ZCREATIONDATE3
ZTYPEUTI
ZTYPEUTI1
The really annoying aspect is specific values keep moving, based on the version. For example, the note account owner that was in ZACCOUNT3
in iOS 14 is now in ZACCOUNT4
in iOS 15 and the folder owner that was in ZACCOUNT4
is now in ZACCOUNT5
. This means that on every major iOS release, you should expect to be updating your scripts and it means you need to make sure you can appropriately fingerprint the correct version of the database.
iOS Version | Note Owner | Folder Owner |
---|---|---|
11 / 12 | ZACCOUNT2 | ZACCOUNT3 |
13 / 14 | ZACCOUNT3 | ZACCOUNT4 |
15 | ZACCOUNT4 | ZACCOUNT5 |
ZCREATIONDATE*
As seen above, ZCREATIONDATE2
and ZCREATIONDATE3
are new in iOS 15. ZCREATIONDATE2
has already been identified as being used by Hashtags and Mentions, but ZCREATIONDATE3
contains the same value on note rows in 15.0 as the ZCREATIONDATE1
column did in 14.8. Confusingly, now ZCREATIONDATE1
is used by tags for the tag browser. I ended up making myself this table to keep things straight:
Column | iOS 14 | iOS 15 |
---|---|---|
ZCREATIONDATE | Object embedded inside note | Object embedded inside note |
ZCREATIONDATE1 | Note creation time | Overall Tag creation time |
ZCREATIONDATE2 | - | Hashtag and Mention creation time inside note |
ZCREATIONDATE3 | - | Note creation time |
Replica Bundle ID
This doesn’t appear related to iOS 15 changes as much as continuing work on how data is replicated across accounts. There already were two other ZREPLICAID*
columns, but ZREPLICAIDTOBUNDLEIDENTIFIER
is new in iOS 15. This contains an NSKeyedArchive that maps a com.apple.mobilenotes.SharingExtension
to com.apple.mobilenotes.IntentsExtension
and Apple Notes itself. I only found this value on the ZICCLOUDSYNCINGOBJECT
columns that represented an iCloud account.
[notta@cuppa /tmp]$ ruby ~/bplister/nskeyed_archive_list.rb replica_id_to_bundle.bin
{"root"=>
{"NS.keys"=>
["com.apple.mobilenotes.SharingExtension",
"com.apple.mobilenotes.IntentsExtension",
"com.apple.mobilenotes"],
"NS.objects"=>
[{"NS.uuidbytes"=>"\xC4?T\x983\x9EC\x16\x9Fg\x037\xDC!\xE0o",
"$class"=>{"$classname"=>"NSUUID", "$classes"=>["NSUUID", "NSObject"]}},
{"NS.uuidbytes"=>"\x0A\x51\x84\xA8$\xCBJ\x62\x01@\xD7\xF5\xDCl\x02\xB7",
"$class"=>{"$classname"=>"NSUUID", "$classes"=>["NSUUID", "NSObject"]}},
{"NS.uuidbytes"=>"\x36\xA2\x57\a!?I\xCF\xA2\xB7d\xBBY)\xA3g",
"$class"=>{"$classname"=>"NSUUID", "$classes"=>["NSUUID", "NSObject"]}}],
"$class"=>
{"$classname"=>"NSDictionary", "$classes"=>["NSDictionary", "NSObject"]}}}
In addition, these six fields showed up on the actual account row which seemed related to account information and based on naming might be related to replicating data:
Column | New? | Description |
---|---|---|
ZSERVERSIDEUPDATETASKCONTINUATIONTOKEN | Yes | [I have not yet been able to get a value here] |
ZSERVERSIDEUPDATETASKFAILURECOUNT | Yes | [I have not yet been able to get a value here] |
ZSERVERSIDEUPDATETASKLASTATTEMPTEDBUILD | Yes | iOS build number for last iOS attempted update, for example, 19A404. |
ZSERVERSIDEUPDATETASKLASTATTEMPTEDVERSION | Yes | iOS version number for last iOS attempted update, for example, 15.0.2. |
ZSERVERSIDEUPDATETASKLASTCOMPLETEDBUILD | Yes | iOS build number for last iOS update, for example, 19A404. |
ZSERVERSIDEUPDATETASKLASTCOMPLETEDVERSION | Yes | iOS version number for last iOS update, for example, 15.0.2. |
Expiring URLs
There is a new column called ZURLEXPIRED
which would seem to indicate whether a URL is expired or not. iOS 12 allowed users to share photos and videos by providing a URL which would expire in 30 days, maybe this functionality is coming to Notes in the future? None of my test data has this as anything but ‘0’ and I haven’t had iOS 15 long enough for something to expire.
Embedded Icons
An interesting new column is ZLINKPRESENTATIONARCHIVEDMETADATA
. This column is an NSKeyedArchive that contains the information necessary to show a link to an embedded object, such as a VCard or a URL. Interestingly. it embeds a PNG that is what comes up when you see the object, and it can contain links to the original favicons, if it was a URL. I only found this on UTIs that inherited from public.data
.
[notta@cuppa /tmp]$ ruby ~/bplister/plist_parse.rb link_presentation_metadata_23.bin
{"$version"=>100000,
"$archiver"=>"NSKeyedArchiver",
"$top"=>{"root"=>1},
"$objects"=>
["$null",
{"$class"=>14, "title"=>2, "icon"=>3, "specialization2"=>8, "version"=>1},
"Apple Inc.",
{"data"=>4, "imageType"=>5, "MIMEType"=>6, "$class"=>7},
{"NS.data"=> "\x89PNG\r\n [snipped the embedded image]",
"$class"=>5},
{"$classname"=>"NSMutableData",
"$classes"=>["NSMutableData", "NSData", "NSObject"]},
"image/png",
{"$classname"=>"LPImage", "$classes"=>["LPImage", "NSObject"]},
{"size"=>10,
"creationDate"=>11,
"icon"=>3,
"name"=>2,
"type"=>9,
"$class"=>13},
"public.vcard",
357,
{"NS.time"=>621217431.0, "$class"=>12},
{"$classname"=>"NSDate", "$classes"=>["NSDate", "NSObject"]},
{"$classname"=>"LPFileMetadata",
"$classes"=>["LPFileMetadata", "LPSpecializationMetadata", "NSObject"]},
{"$classname"=>"LPLinkMetadata",
"$classes"=>["LPLinkMetadata", "NSObject"]}]}
“Paper”
Also in the “potentially a future update” category are three columns related to “paper”. Based on the names, and the fact that ZPAPERSTYLETYPE
already exists and appears to track which of the “Lines and Grids” backgrounds I have chosen for handwriting, I’m wondering if support for external lines and grids will be available in the future. These are the “PAPER” columns:
[notta@cuppa ~]$ awk -F '|' '{print $2}' notestore_15.0.schema \
| grep "PAPER"
ZISSYSTEMPAPER
ZPAPERASSETSURL
ZPAPERDATABASEURL
ZPAPERSTYLETYPE
“Synapse”
In the category of “I have no idea” is the new column named ZSYNAPSEDATA
. I have not yet been able to generate any test data that has this filled in with a value and the name isn’t leading to any obvious places on Apple’s developer pages. This could easily be a feature that has not yet fully rolled out, or one that I simply haven’t hit on the right combination of test actions to find. My test phone’s only case of the word “synapse” is this NoteStore.sqlite database.
[notta@cuppa itunes_backup]$ grep -i -r synapse
grep: 4f/4f98687d8ab0d6d1a371110e6b7300f6e465bef2: binary file matches
Conclusion
This post took a lot longer to get out than I had hoped. Given how many columns were added in this release to ZICCLOUDSYNCINGOBJECT
, it will hopefully be useful, even if it was delayed. The final score of how the 25 new columns are used is as follows:
Column | New? | Use |
---|---|---|
ZACCOUNT5 | Yes | Folder creation time |
ZCREATIONDATE2 | Yes | Hashtag and Mentions |
ZCREATIONDATE3 | Yes | Note creation date |
ZDISPLAYTEXT | Yes | Hashtags |
ZISSYSTEMPAPER | Yes | Uncertain, likely lines and grids |
ZLASTACTIVITYRECENTUPDATESVIEWEDDATE | Yes | Activity View |
ZLASTACTIVITYSUMMARYVIEWEDDATE | Yes | Activity View |
ZLASTATTRIBUTIONSVIEWEDDATE | Yes | Activity View |
ZLINKPRESENTATIONARCHIVEDMETADATA | Yes | Icon used when sharing |
ZMENTIONNOTIFICATIONATTEMPTCOUNT | Yes | Mentions |
ZMENTIONNOTIFICATIONSTATE | Yes | Mentions |
ZPAPERASSETSURL | Yes | Uncertain, likely lines and grids |
ZPAPERDATABASEURL | Yes | Uncertain, likely lines and grids |
ZREPLICAIDTOBUNDLEIDENTIFIER | Yes | iCloud account |
ZSERVERSIDEUPDATETASKCONTINUATIONTOKEN | Yes | Account information |
ZSERVERSIDEUPDATETASKFAILURECOUNT | Yes | Account information |
ZSERVERSIDEUPDATETASKLASTATTEMPTEDBUILD | Yes | Account information |
ZSERVERSIDEUPDATETASKLASTATTEMPTEDVERSION | Yes | Account information |
ZSERVERSIDEUPDATETASKLASTCOMPLETEDBUILD | Yes | Account information |
ZSERVERSIDEUPDATETASKLASTCOMPLETEDVERSION | Yes | Account information |
ZSMARTFOLDERQUERYJSON | Yes | Smart Folders |
ZSTANDARDIZEDCONTENT | Yes | Hashtags |
ZSYNAPSEDATA | Yes | Unknown |
ZTOKENCONTENTIDENTIFIER | Yes | Hashtags and Mentions |
ZURLEXPIRED | Yes | Uncertain, possibly sharing URLs |
Footnotes
-
From Apple’s iOS 15 feature preview. ↩
-
Interestingly, this field has been in Apple Notes since at least 14.7.1, perhaps preparing the way for this release. ↩
-
A partial protobuf definition that appears to work for most of these fields can be found here. ↩