Oregami
Repositories/oxedyne/ore

oxedyne/ore/store/src/veil.rs

17.6 KiB, 3 runs

created by r2848102244:393, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! The content key, and what a carrier is given instead of the content.
2//!
3//! A repository hosted on a relay is private by the relay's access list, which
4//! is a promise about how the relay behaves. A *veiled* repository is private
5//! whatever the relay does: every operation crosses as a [`Veiled`] entry, whose
6//! identifier and parents are in clear and whose body is encrypted under a key
7//! the relay never holds. The relay can then do the whole of its job -- walk
8//! frontiers, reconcile sketches, carry operations, converge two replicas that
9//! are never awake together -- while being unable to read a byte of what they
10//! say.
11//!
12//! # The key belongs to the readers
13//!
14//! One symmetric key per repository, AES-256-GCM, held in `.ore/veil` beside the
15//! signing key and written under the same file mode. It is never deposited with
16//! a relay, never carried in a request and never named in a segment. A second
17//! replica joins by being given the key through a channel the relay is not part
18//! of, which is `ore key --veil <key>`, and that hand-over is the whole of the
19//! trust decision.
20//!
21//! Wrapping the key to each collaborator's public key, so it could travel
22//! through the relay itself, is the obvious next thing and is deliberately not
23//! here. It waits on a decision about identity rather than on a format, and the
24//! decision was taken on 2026-08-17: a wrap is received by a second key of a
25//! second kind, published by a binding of its own.
26//!
27//! An earlier version of this comment gave the reason as impossibility -- that an
28//! Ore replica key is an Ed25519 *signing* key and so cannot receive a wrapped
29//! secret. **That was wrong.** Ed25519 and X25519 are the same curve in two
30//! coordinate systems and `ed25519-dalek` ships the conversion, so the identity
31//! could have received a wrap directly. It was refused because the library's own
32//! documentation argues against reusing a signing key for key agreement, not
33//! because it could not be done. See `doc/design/key_distribution.md`.
34//!
35//! # Veiling is for the wire, not for the disk
36//!
37//! A working copy's own segments hold plain entries. Veiling them would buy
38//! nothing, since `.ore/veil` sits in the same directory as the plaintext
39//! `.ore/key`, and anyone who can read one can read the other. So a veil goes on
40//! at the moment an entry is handed to a carrier and comes off the moment one
41//! arrives, and every verb but `sync` is untouched by it.
42//!
43//! # What a carrier holds instead
44//!
45//! A relay has to put arriving operations into an [`OpLog`], which holds whole
46//! records, and a veiled entry has no record to give it. So the relay holds a
47//! [`standin`] in the log -- the true header, and a mark saying in words that
48//! the operation is veiled -- and keeps the veiled entry itself beside it, by
49//! identifier, exactly as it keeps envelopes. Everything the relay does with the
50//! log reads the header and never the operation, and everything the relay hands
51//! back or writes down is substituted from the veiled entries it kept. A
52//! stand-in is refused by [`crate::store::Store::append`] if it ever reaches the
53//! disk, because a stand-in written down would be a veiled operation lost.
54//!
55//! # The honest limit
56//!
57//! Veiling hides operation content and the size of it. The shape of the graph,
58//! the number of operations, when they arrived, which replicas wrote them and
59//! which public keys signed them are all still visible to the carrier, because
60//! they are what the carrier is for.
61
62use oxedyne_fe2o3_core::prelude::*;
63use oxedyne_fe2o3_crypto::enc::EncryptionScheme;
64use oxedyne_fe2o3_iop_crypto::keys::KeyManager;
65use oxedyne_fe2o3_jdat::prelude::*;
66use oxedyne_fe2o3_ore::id::OpId;
67use oxedyne_fe2o3_ore::op::{
68 Header,
69 Op,
70 Record,
71};
72use oxedyne_fe2o3_ore::segment::{
73 Entry,
74 Veiled,
75};
76use oxedyne_fe2o3_ore::sync::Message;
77
78use crate::keys::{
79 bytes_of,
80 text_of,
81 write_private,
82};
83
84use std::collections::BTreeMap;
85use std::fs;
86use std::path::{
87 Path,
88 PathBuf,
89};
90
91
92/// Name of the content key file, within the `.ore` directory.
93pub const VEIL_FILE: &str = "veil";
94
95/// Version of the content key file this tool writes.
96pub const VEIL_VERSION: u64 = 1;
97
98/// The name the content key file records the cipher under.
99pub const CIPHER: &str = "AES-256-GCM";
100
101/// Length of a content key, in bytes.
102pub const KEY_LEN: usize = 32;
103
104/// What the content key file says about itself.
105pub const VEIL_COMMENT: &str = "This content key is NOT encrypted. Anyone who can read this \
106 file can read every operation this repository sends to a relay, so the file is written \
107 readable only by its owner (mode 0600). It is never sent to a relay: a replica is given it \
108 by hand.";
109
110
111/// The name a stand-in mark carries, so that one is recognisable wherever it
112/// turns up.
113///
114/// It says what it is in words rather than in a sentinel byte, because the place
115/// it might turn up is somebody's screen.
116pub fn standin_name(id: OpId) -> String {
117 fmt!("veiled operation {}", id)
118}
119
120/// The record a carrier holds in place of one it cannot read.
121///
122/// The header is the true one, which is what every question a carrier asks of a
123/// log is answered from. The operation is a mark, which renders nothing, claims
124/// no byte and mints no atom, so a stand-in that somehow reached a renderer would
125/// add nothing to what it drew; and it is named after the operation it stands
126/// for, so a stand-in that somehow reached a person would say so.
127pub fn standin(head: Header) -> Record {
128 let name = standin_name(head.id());
129 Record::new(head, Op::Mark { name, body: None, time: None })
130}
131
132/// Is this record the stand-in a carrier holds for a veiled operation?
133pub fn is_standin(rec: &Record) -> bool {
134 match &rec.op {
135 Op::Mark { name, body: None, time: None } => *name == standin_name(rec.id()),
136 _ => false,
137 }
138}
139
140
141/// One repository's content key.
142#[derive(Clone, Debug)]
143pub struct Veil {
144 /// The cipher, holding the key.
145 scheme: EncryptionScheme,
146 /// The key itself, kept beside the cipher because installing it elsewhere
147 /// asks for it.
148 key: Vec<u8>,
149}
150
151impl Veil {
152
153 /// Returns the path of the content key file within the `.ore` directory
154 /// `dir`.
155 pub fn path_of(dir: &Path) -> PathBuf {
156 dir.join(VEIL_FILE)
157 }
158
159 /// Mints a fresh content key.
160 pub fn mint()
161 -> Outcome<Self>
162 {
163 let scheme = EncryptionScheme::new_aes_256_gcm();
164 let key = match res!(scheme.get_secret_key()) {
165 Some(sk) => sk.to_vec(),
166 None => return Err(err!(
167 "A freshly minted {} key holds no key, which cannot happen and means \
168 the scheme has changed under this tool.", CIPHER;
169 Bug, Missing, Key)),
170 };
171 Ok(Self { scheme, key })
172 }
173
174 /// Takes a key somebody was handed, refusing one the cipher will not accept.
175 pub fn of(key: &[u8])
176 -> Outcome<Self>
177 {
178 if key.len() != KEY_LEN {
179 return Err(err!(
180 "A content key is {} bytes and this one is {}. It is the text `ore key \
181 --veil` printed in the repository it came from, whole.",
182 KEY_LEN, key.len();
183 Invalid, Input, Key));
184 }
185 Ok(Self {
186 scheme: res!(EncryptionScheme::new_aes_256_gcm_with_key(key)),
187 key: key.to_vec(),
188 })
189 }
190
191 /// Returns the key in the form the file holds it and `ore key --veil` prints
192 /// it.
193 pub fn text(&self) -> String {
194 text_of(&self.key)
195 }
196
197 /// Reads the content key file, which a repository is not obliged to have.
198 ///
199 /// Absence is `None`, because a repository that veils nothing is the ordinary
200 /// case; a file that is there and will not read is an error, because the
201 /// alternative is quietly handing a relay the plaintext of a repository whose
202 /// owner asked for the opposite.
203 pub fn read(dir: &Path)
204 -> Outcome<Option<Self>>
205 {
206 let path = Self::path_of(dir);
207 if !path.is_file() {
208 return Ok(None);
209 }
210 let text = match fs::read_to_string(&path) {
211 Ok(t) => t,
212 Err(e) => return Err(err!(e,
213 "The content key {:?} could not be read.", path;
214 IO, File, Read)),
215 };
216 let dat = match Dat::decode_string(text) {
217 Ok(d) => d,
218 Err(e) => return Err(err!(e,
219 "The content key {:?} is not readable JDAT.", path;
220 Decode, Input)),
221 };
222 let map = match &dat {
223 Dat::Map(m) => m,
224 other => return Err(err!(
225 "The content key {:?} expects a map, got {:?}.", path, other;
226 Decode, Input, Mismatch)),
227 };
228 let field = |key: &str| -> Outcome<Dat> {
229 match map.get(&Dat::Str(fmt!("{}", key))) {
230 Some(d) => Ok(d.clone()),
231 None => Err(err!(
232 "The content key {:?} has no field {:?}.", path, key;
233 Decode, Input, Missing)),
234 }
235 };
236 let version = match res!(field("format")) {
237 Dat::U64(n) => n,
238 Dat::U32(n) => n as u64,
239 Dat::U8(n) => n as u64,
240 other => return Err(err!(
241 "The content key {:?} declares a format of {:?} rather than a number.",
242 path, other;
243 Decode, Input, Mismatch)),
244 };
245 if version != VEIL_VERSION {
246 return Err(err!(
247 "The content key {:?} declares format version {}, and this tool knows \
248 only version {}.", path, version, VEIL_VERSION;
249 Decode, Input, Version, Mismatch));
250 }
251 let named = match res!(field("cipher")) {
252 Dat::Str(s) => s,
253 other => return Err(err!(
254 "The content key {:?} names its cipher {:?} rather than a string.",
255 path, other;
256 Decode, Input, Mismatch)),
257 };
258 if named != CIPHER {
259 return Err(err!(
260 "The content key {:?} is a {} key, and this tool veils with {}.",
261 path, named, CIPHER;
262 Invalid, Input, Mismatch));
263 }
264 let key = match res!(field("key")) {
265 Dat::Str(s) => res!(bytes_of(&s)),
266 other => return Err(err!(
267 "The content key {:?} holds {:?} rather than a string.", path, other;
268 Decode, Input, Mismatch)),
269 };
270 Ok(Some(res!(Self::of(&key))))
271 }
272
273 /// Writes the content key file, readable only by its owner.
274 pub fn write(&self, dir: &Path)
275 -> Outcome<PathBuf>
276 {
277 let mut map = DaticleMap::new();
278 map.insert(Dat::Str(fmt!("comment")), Dat::Str(fmt!("{}", VEIL_COMMENT)));
279 map.insert(Dat::Str(fmt!("format")), Dat::U64(VEIL_VERSION));
280 map.insert(Dat::Str(fmt!("cipher")), Dat::Str(fmt!("{}", CIPHER)));
281 map.insert(Dat::Str(fmt!("key")), Dat::Str(self.text()));
282 let text = res!(Dat::Map(map).jdat_to_lines(" "));
283 let path = Self::path_of(dir);
284 res!(write_private(&path, fmt!("{}\n", text).as_bytes()));
285 Ok(path)
286 }
287
288 /// Puts a veil around every entry a message carries.
289 pub fn veil(&self, msg: Message)
290 -> Outcome<Message>
291 {
292 let entries = match msg {
293 Message::Send { entries } => entries,
294 other => return Ok(other),
295 };
296 let mut out = Vec::with_capacity(entries.len());
297 for entry in entries {
298 out.push(res!(entry.veil(&self.scheme)));
299 }
300 Ok(Message::Send { entries: out })
301 }
302
303 /// Takes the veil off every entry a message carries, and leaves a plain one
304 /// alone.
305 ///
306 /// A message that mixes the two is not a fault: a repository is veiled from
307 /// the moment somebody veils it, and the operations written before that
308 /// crossed in the clear and are still on the relay in the clear.
309 pub fn unveil(&self, msg: Message)
310 -> Outcome<Message>
311 {
312 let entries = match msg {
313 Message::Send { entries } => entries,
314 other => return Ok(other),
315 };
316 let mut out = Vec::with_capacity(entries.len());
317 for entry in entries {
318 out.push(if entry.is_veiled() {
319 res!(entry.unveil(&self.scheme))
320 } else {
321 entry
322 });
323 }
324 Ok(Message::Send { entries: out })
325 }
326}
327
328
329/// Replaces the veiled entries of an arriving message with stand-ins, keeping
330/// each veiled entry by identifier.
331///
332/// What a carrier does instead of reading. The session that follows sees a log of
333/// records and asks it only for headers, and the veiled entries are what goes
334/// back on the wire and to the disk in their place.
335pub fn placehold(msg: Message, kept: &mut BTreeMap<OpId, Veiled>)
336 -> Outcome<Message>
337{
338 let entries = match msg {
339 Message::Send { entries } => entries,
340 other => return Ok(other),
341 };
342 let mut out = Vec::with_capacity(entries.len());
343 for entry in entries {
344 match entry {
345 Entry::Veiled(v) => {
346 kept.insert(v.head.id(), v.clone());
347 out.push(Entry::Bare(standin(v.head)));
348 },
349 other => out.push(other),
350 }
351 }
352 Ok(Message::Send { entries: out })
353}
354
355/// Puts the veiled entries back in place of the stand-ins held for them.
356///
357/// The mirror of [`placehold`], and the only way a stand-in is allowed to leave
358/// the carrier. An entry whose identifier is not one of them is passed through:
359/// a repository that veils holds plain operations too, written before it did.
360pub fn restore(entries: Vec<Entry>, kept: &BTreeMap<OpId, Veiled>)
361 -> Outcome<Vec<Entry>>
362{
363 let mut out = Vec::with_capacity(entries.len());
364 for entry in entries {
365 out.push(res!(restore_one(entry, kept)));
366 }
367 Ok(out)
368}
369
370/// The same substitution, one entry at a time.
371///
372/// The mirror of [`ore_store::store::seal_one`], and it exists for the same
373/// caller: a relay that stops building its reply once it has the reply bound's
374/// worth never looks at the entries past it, so the substitutions cannot be done
375/// over the set entire.
376pub fn restore_one(entry: Entry, kept: &BTreeMap<OpId, Veiled>)
377 -> Outcome<Entry>
378{
379 let id = res!(entry.id());
380 Ok(match kept.get(&id) {
381 Some(v) => Entry::Veiled(v.clone()),
382 None => entry,
383 })
384}
385
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390
391 use oxedyne_fe2o3_ore::id::ReplicaId;
392
393 fn oid(replica: u64, counter: u64) -> OpId {
394 OpId::new(ReplicaId::new(replica), counter)
395 }
396
397 /// A key survives the file form it is written in, and one of the wrong length
398 /// is refused in words a person can act on.
399 ///
400 /// The cipher refuses it too, one call further in, and says only that a slice
401 /// would not become an array. What is asserted here is therefore the sentence
402 /// and not the refusal: somebody who pasted half a key needs to be told that it
403 /// is half a key and where the whole one comes from.
404 #[test]
405 fn a_content_key_is_taken_or_it_is_not() -> Outcome<()> {
406 let veil = res!(Veil::mint());
407 let text = veil.text();
408 let again = res!(Veil::of(&res!(bytes_of(&text))));
409 assert_eq!(again.text(), text);
410 for bad in [0usize, 16, 31, 33] {
411 let refused = match Veil::of(&vec![7u8; bad]) {
412 Ok(_) => return Err(err!(
413 "A content key of {} bytes was accepted.", bad; Test, Invalid)),
414 Err(e) => fmt!("{}", e.plain()),
415 };
416 assert!(refused.contains(&fmt!("{} bytes and this one is {}", KEY_LEN, bad)),
417 "the refusal does not say how long it should have been: {}", refused);
418 assert!(refused.contains("ore key --veil"),
419 "nor where a whole one comes from: {}", refused);
420 }
421 Ok(())
422 }
423
424 /// A stand-in is recognised by the name it carries, and an ordinary mark of
425 /// the same shape is not mistaken for one.
426 #[test]
427 fn a_standin_says_what_it_is() -> Outcome<()> {
428 let head = res!(Header::new(oid(2, 3), vec![oid(1, 1)]));
429 let rec = standin(head.clone());
430 assert!(is_standin(&rec));
431 assert_eq!(rec.head, head, "a stand-in carries the true header");
432 assert!(fmt!("{:?}", rec.op).contains("r2:3"),
433 "and says which operation it stands for: {:?}", rec.op);
434 // The same name against another operation is not that operation's
435 // stand-in, since the name carries the identifier.
436 let borrowed = Record::new(
437 res!(Header::new(oid(2, 4), vec![oid(1, 1)])),
438 rec.op.clone(),
439 );
440 assert!(!is_standin(&borrowed));
441 let ordinary = Record::root(oid(1, 1), Op::Mark {
442 name: fmt!("release 1.0"),
443 body: None,
444 time: None,
445 });
446 assert!(!is_standin(&ordinary));
447 Ok(())
448 }
449
450 /// Every entry a message carries goes under the veil and comes back out of
451 /// it, and a message that carries none is left alone.
452 #[test]
453 fn a_message_veils_and_unveils_whole() -> Outcome<()> {
454 let veil = res!(Veil::mint());
455 let secret: &[u8] = b"the merger closes on Friday";
456 let entries = vec![
457 Entry::Bare(Record::root(oid(1, 1), Op::FileCreate {
458 path: secret.to_vec(),
459 })),
460 Entry::Bare(Record::new(
461 res!(Header::new(oid(1, 2), vec![oid(1, 1)])),
462 Op::Mark { name: fmt!("start"), body: None, time: None },
463 )),
464 ];
465 let sent = res!(veil.veil(Message::Send { entries: entries.clone() }));
466 for entry in sent.entries() {
467 assert!(entry.is_veiled());
468 }
469 let bytes = res!(sent.encode());
470 assert!(
471 !bytes.windows(secret.len()).any(|w| w == secret),
472 "the message on the wire carries the operation's content",
473 );
474 let back = res!(veil.unveil(sent));
475 assert_eq!(back.entries(), &entries[..]);
476 // Anything that is not a batch of operations passes through untouched.
477 assert_eq!(res!(veil.veil(Message::Done)), Message::Done);
478 assert_eq!(res!(veil.unveil(Message::Done)), Message::Done);
479 Ok(())
480 }
481
482 /// A carrier places the veiled entries by stand-in and hands back exactly
483 /// what it was given.
484 #[test]
485 fn a_carrier_stands_in_and_hands_back_the_veil() -> Outcome<()> {
486 let veil = res!(Veil::mint());
487 let entries = vec![
488 Entry::Bare(Record::root(oid(1, 1), Op::FileCreate {
489 path: b"notes.md".to_vec(),
490 })),
491 Entry::Bare(Record::new(
492 res!(Header::new(oid(1, 2), vec![oid(1, 1)])),
493 Op::Mark { name: fmt!("start"), body: None, time: None },
494 )),
495 ];
496 let sent = res!(veil.veil(Message::Send { entries: entries.clone() }));
497 let mut kept: BTreeMap<OpId, Veiled> = BTreeMap::new();
498 let held = res!(placehold(sent.clone(), &mut kept));
499 assert_eq!(kept.len(), 2);
500 for entry in held.entries() {
501 let rec = res!(entry.peek());
502 assert!(is_standin(&rec), "a carrier holds a stand-in, not an operation");
503 }
504 // The graph the carrier placed them in is the true one.
505 let placed: Vec<OpId> = held.entries().iter()
506 .map(|e| match e.id() { Ok(id) => id, Err(_) => oid(0, 0) })
507 .collect();
508 assert_eq!(placed, vec![oid(1, 1), oid(1, 2)]);
509 assert_eq!(res!(held.entries()[1].peek()).parents(), vec![oid(1, 1)]);
510 // And what it hands back is what it was given, byte for byte.
511 let out = res!(restore(held.entries().to_vec(), &kept));
512 assert_eq!(out, sent.entries().to_vec());
513 assert_eq!(res!(veil.unveil(Message::Send { entries: out })).entries(), &entries[..]);
514 Ok(())
515 }
516}