| 7 |
7 |
|
(GoingsOn, Balanced Breakfast, audiofiles) use this crate to push and pull
|
| 8 |
8 |
|
changelog entries without the server ever seeing plaintext data.
|
| 9 |
9 |
|
|
| 10 |
|
- |
Version: 0.4.0.
|
|
10 |
+ |
Version: 0.5.0.
|
| 11 |
11 |
|
|
| 12 |
12 |
|
## Crate structure
|
| 13 |
13 |
|
|
| 154 |
154 |
|
Legacy and v2 ciphertext coexist with no flag-day; key rotation re-encrypts
|
| 155 |
155 |
|
legacy entries into the v2 form opportunistically.
|
| 156 |
156 |
|
|
|
157 |
+ |
The AAD is never passed as raw bytes. Callers name a context -- `AeadContext::Entry
|
|
158 |
+ |
{ table, row_id }` or `AeadContext::Blob { hash }` -- and the crypto layer derives
|
|
159 |
+ |
the AAD from it. The byte-level builder is private, so an empty or mismatched AAD
|
|
160 |
+ |
is not expressible at the call site.
|
|
161 |
+ |
|
|
162 |
+ |
### Envelope version dispatch (`__skver`)
|
|
163 |
+ |
|
|
164 |
+ |
The decrypted entry payload is an envelope `{ __skver: 2, __skhlc, data }`.
|
|
165 |
+ |
Splitting it back into `(hlc, data)` dispatches on the explicit `__skver` tag via
|
|
166 |
+ |
a typed `WireVersion`: an unknown future version is a hard error, not a silent
|
|
167 |
+ |
fall-through to a bare-row read. A tag-less payload carrying an embedded `__skhlc`
|
|
168 |
+ |
is a gen-1 envelope; anything else is a legacy bare row.
|
|
169 |
+ |
|
|
170 |
+ |
### Chunked blob format (`sk3:`)
|
|
171 |
+ |
|
|
172 |
+ |
Blobs use a chunked AEAD format so a reader decrypts and verifies one chunk at a
|
|
173 |
+ |
time. Layout: `sk3:` `version(u8=3)` `chunk_size(u32 LE)` `total_len(u64 LE)`
|
|
174 |
+ |
followed by sealed 1 MiB chunks. Each chunk's AAD binds `(content_hash,
|
|
175 |
+ |
chunk_index, chunk_count)`, so reorder, truncation, duplication, and cross-blob
|
|
176 |
+ |
substitution all fail closed. `blob_download` streams the body and decrypts
|
|
177 |
+ |
chunk-by-chunk (peak memory is the plaintext plus one in-flight chunk, not
|
|
178 |
+ |
ciphertext *and* plaintext together); legacy `sk2:`/untagged blobs still decrypt
|
|
179 |
+ |
whole-buffer with no flag-day.
|
|
180 |
+ |
|
| 157 |
181 |
|
### Push
|
| 158 |
182 |
|
|
| 159 |
183 |
|
1. Caller provides a `Vec<ChangeEntry>` with plaintext `data` fields and an HLC.
|
| 188 |
212 |
|
every device converges on the same winner. An *exactly* equal HLC normally means
|
| 189 |
213 |
|
a same-device echo; if two installs share a `node` UUID (cloned config), the tie
|
| 190 |
214 |
|
is broken deterministically on the canonical payload bytes so both devices still
|
| 191 |
|
- |
converge. `detect_conflicts` only flags rows with an *un-pushed* local edit;
|
| 192 |
|
- |
"clean" changes must still be HLC-gated by the caller against committed local
|
| 193 |
|
- |
state before applying.
|
|
215 |
+ |
converge.
|
|
216 |
+ |
|
|
217 |
+ |
`detect_conflicts` only flags rows with an *un-pushed* local edit. Its clean set
|
|
218 |
+ |
is returned as an opaque `CleanChanges`, not a bare `Vec`: the only way to read
|
|
219 |
+ |
the applicable entries out is `gated(committed_hlc)`, which drops any clean change
|
|
220 |
+ |
older-or-equal to the row's committed HLC. (`row_keys()` lets the caller pre-fetch
|
|
221 |
+ |
those clocks first.) This makes "apply a clean change without checking the
|
|
222 |
+ |
committed clock" — which would let an older remote edit clobber a newer local
|
|
223 |
+ |
value — unrepresentable rather than a contract the caller has to remember.
|
| 194 |
224 |
|
|
| 195 |
225 |
|
### Sequence numbers
|
| 196 |
226 |
|
|