1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
//! Cross-cutting value types, used across every domain module - and the
//! home for primitives (once implemented) reused by both
//! `access_control`'s `AccessToken` and `bootstrap`'s `BootstrapSecret`
//! (`generate_token_secret`, `secret_matches`, `generate_token_id`). See
//! docs/architecture.md §3.2 and `specs/skilj.allium`'s Value Types
//! section, which this module mirrors closely.
use JsonSchema;
use ;
/// Names a payload field to filter, tag, or mark sensitive: a bare
/// top-level name, or a two-segment dotted path reaching one leaf inside
/// the spec's one permitted level of nesting (e.g. `"address.country"`).
/// See the payload schema shape note above `entity CommandType` in
/// `specs/skilj.allium`.
pub type FieldPath = String;
/// See `value Metadata` in the spec. `version` and every other
/// sequence-adjacent integer in this codebase is `i64` - see
/// docs/architecture.md §2.2.1.
/// `value` is `None` (absent) rather than the tag being omitted entirely
/// when the mapped payload field was itself absent - see `Tag.value` in
/// the spec for why that's deliberate, not an oversight.
/// `subject_field` is never encrypted - it's the plaintext identifier the
/// `EncryptionKey` is looked up or derived by. Only `field` is what gets
/// swapped for ciphertext (at the leaf, for a dotted path - see
/// `protect_sensitive_fields` in `event_store`).
/// See `value PrivateField` in the spec - the private-field mechanism's
/// own three-way visibility rule, standing beside `SensitiveField` rather
/// than inside it: a plain read-time redaction, no `EncryptionKey`
/// anywhere behind it. `team`/`addressee_field` are each set exactly when
/// `kind` calls for them, checked by `event_store::valid_private_fields`
/// at registration - `None` for whichever the other two kinds leave
/// unused.
/// See `enum PrivateFieldKind` in the spec for what each variant means in
/// full - `Own` (default reader: the record's own creator),
/// `Team` (default reader: any `Role` named `PrivateField.team`),
/// `Addressed` (default reader: the party `PrivateField.addressee_field`
/// names in the payload). A fourth kind, `Draft` - visible only until an
/// external status changes - is a real, deliberately deferred future
/// kind: it needs a live `Projection` lookup at read time, a materially
/// different evaluability shape from these three, which are decidable
/// from the record and the reading caller's own identity alone. Nothing
/// here is shaped around it and nothing here forecloses it.
/// The whole contract a bounded context's `decide()` returns - see
/// `value CommandDecision` in the spec, and docs/architecture.md
/// §4.1/§5.4/§7.3 for why a rejection is typed data, never a
/// GraphQL/HTTP-level error.
/// One event a `decide()` call wants appended, on acceptance.
// `secret_matches` below was added propagating `bootstrap::create_superadmin`
// - unlike `generate_token_secret`/`generate_token_id` below, it needed no
// caller-supplied-output treatment (it takes strings, not producing an
// opaque one), so it was real from the start rather than deferred.
/// Generates the opaque `id` half of an `AccessToken`/`Role` - a UUIDv4,
/// hyphens stripped (32 lowercase hex characters), matching every other
/// identifier this codebase already treats as an opaque `String` rather
/// than a typed newtype. Not a secret itself - safe to log, appear in a
/// URL path, etc. - only `generate_token_secret` below carries the
/// confidentiality requirement `secret_matches` is built to compare
/// without leaking.
/// Generates the `secret` half of an `AccessToken`/`BootstrapSecret` -
/// two concatenated UUIDv4s (244 bits of randomness, comfortably more
/// than a single v4's 122), hyphens stripped. `uuid`'s `v4` feature reads
/// from the OS CSPRNG (`getrandom`), so this is cryptographically
/// unpredictable, not merely unique like `generate_token_id` only needs
/// to be.
/// Hashes the `secret` half of an `AccessToken` for storage - see the
/// REST token presentation note above the surfaces in the spec: "the
/// secret half is then verified against that row's stored hash... Both
/// the hashing scheme and the comparison are black boxes here, the same
/// register as generate_token_secret and generate_token_id: the spec
/// owns that the secret is never stored or compared in plaintext, not
/// which primitive does it." `generate_token_secret` already gives 244
/// bits of CSPRNG randomness (see its own doc comment) - high-entropy,
/// unlike a user-chosen password - so a plain cryptographic hash
/// (SHA-256, via `ring`, already a dependency for `encryption`) is the
/// appropriate primitive: brute-forcing a 244-bit secret back out of its
/// hash is infeasible regardless of how fast the hash is, and a slow
/// password KDF (bcrypt/argon2/scrypt) would only add latency to every
/// request's auth check for no security benefit. Base64-encoded
/// (`base64::engine::general_purpose::STANDARD`), the same encoding
/// `encryption::encrypt` already uses for its own opaque byte output.
/// Deterministic (no per-secret salt) - safe here specifically because
/// the input is never a low-entropy human-chosen value a rainbow table
/// could target, unlike a password hash.
/// See the `secret_matches` note above `rule CreateSuperadmin`: compares
/// two secrets in time that depends only on their lengths, never on where
/// they first differ, so a failed presentation leaks nothing about how
/// much of the real secret was guessed correctly. Real rather than
/// deferred - simple enough that there's no trivial/reachable-case split
/// the way `protect_sensitive_fields`/`derive_tags` etc. have. A length
/// mismatch still short-circuits (the length itself isn't the secret -
/// only its content is), but every byte of an equal-length comparison is
/// folded in regardless of an early difference.