@forgesworn/fold-kit 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +6 -1
- package/dist/epoch.d.ts +14 -0
- package/dist/epoch.js +38 -10
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/seal.d.ts +20 -0
- package/dist/seal.js +25 -0
- package/docs/seal-key.md +39 -0
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/vectors/seal-vectors.json +304 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,20 @@ This kit is pre-1.0 (see AGENTS.md "Release Notes"); a behaviour change on
|
|
|
5
5
|
byte-identical copy of a KithMoot function (see EXTRACTION.md) and this is
|
|
6
6
|
the one place its behaviour has deliberately diverged.
|
|
7
7
|
|
|
8
|
+
## 0.13.0
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- One-time senders for rekeys (1462) and authority grants (20469):
|
|
13
|
+
`oneTimeSender` on `encodeRekeyEvent` and `encodeEpochGrant` seals every
|
|
14
|
+
copy from a fresh key named in a `from` tag, so a later copy of the
|
|
15
|
+
authority's key no longer opens past copies. Off by default; off, events
|
|
16
|
+
are byte-identical to 0.12.0. Readers (`decodeRekeyEvent`,
|
|
17
|
+
`decodeEpochGrant`) open either form now, through the new `sealSender`
|
|
18
|
+
(also exported, with `SENDER_TAG`). Turn writers on only once these readers
|
|
19
|
+
have shipped everywhere. See docs/seal-key.md, "One-time senders"; vectors
|
|
20
|
+
in `seal-vectors.json`.
|
|
21
|
+
|
|
8
22
|
## 0.12.0
|
|
9
23
|
|
|
10
24
|
### Fixed
|
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ vectors (see EXTRACTION.md). Framework-free: owns no storage, UI, relay
|
|
|
13
13
|
defaults or identity keys, and no relay I/O of its own - callers inject a
|
|
14
14
|
transport.
|
|
15
15
|
|
|
16
|
-
**Status:** Version 0.
|
|
16
|
+
**Status:** Version 0.13.0 on `main`. Published on npm as
|
|
17
17
|
`@forgesworn/fold-kit`, but `main` can run ahead of the npm `latest` release, so
|
|
18
18
|
compare `npm view @forgesworn/fold-kit version` with this repository's
|
|
19
19
|
`package.json` before relying on an API listed here. KithMoot has cut over to
|
|
@@ -310,6 +310,11 @@ decision completes, so approval cannot extend an expired request's authority.
|
|
|
310
310
|
`sealSks` on the readers; `credentialFor` on both desks;
|
|
311
311
|
`credentialSeal`, `sealTarget`, `sealTo`, `openSealed`, `newerCredential`,
|
|
312
312
|
`sealCredential`. Vectors: `vectors/seal-vectors.json`
|
|
313
|
+
- One-time senders (`docs/seal-key.md`, "One-time senders"):
|
|
314
|
+
`oneTimeSender` on `encodeRekeyEvent` and `encodeEpochGrant` seals every
|
|
315
|
+
copy from a fresh key named in a `from` tag, so a later copy of the
|
|
316
|
+
authority's key opens no past copy; readers use `sealSender`. Off by
|
|
317
|
+
default until every client's readers have shipped.
|
|
313
318
|
- Scheduled rekeys and the history window (`docs/scheduled-rekey.md`):
|
|
314
319
|
`encodeRekeyEvent({ scheduled: true })` marks a turn of the key that
|
|
315
320
|
removes nobody, read back as `RekeyNotice.scheduled` and
|
package/dist/epoch.d.ts
CHANGED
|
@@ -134,6 +134,13 @@ export interface EncodeRekeyOptions {
|
|
|
134
134
|
/** A conference room's end, in unix seconds: the event carries it as a
|
|
135
135
|
* NIP-40 expiration (see `withExpiration`). Omit for a room with no end. */
|
|
136
136
|
expiresAt?: number;
|
|
137
|
+
/** Seal the copies from a one-time key, named in a `from` tag and
|
|
138
|
+
* discarded once they are sealed, instead of from the authority's key.
|
|
139
|
+
* Readers from fold-kit 0.13.0 open either; an older reader opens no copy
|
|
140
|
+
* from a one-time key, so turn this on only once readers have shipped
|
|
141
|
+
* (docs/seal-key.md, "One-time senders"). Omitted, the event is
|
|
142
|
+
* byte-identical to before. */
|
|
143
|
+
oneTimeSender?: boolean;
|
|
137
144
|
}
|
|
138
145
|
/** A rekey recipient: a device, and the newest credential the sender holds
|
|
139
146
|
* for it. */
|
|
@@ -330,6 +337,13 @@ export interface EncodeEpochGrantOptions {
|
|
|
330
337
|
/** A conference room's end, in unix seconds: the event carries it as a
|
|
331
338
|
* NIP-40 expiration (see `withExpiration`). Omit for a room with no end. */
|
|
332
339
|
expiresAt?: number;
|
|
340
|
+
/** Seal the copies from a one-time key, named in a `from` tag and
|
|
341
|
+
* discarded once they are sealed, instead of from the authority's key.
|
|
342
|
+
* Readers from fold-kit 0.13.0 open either; an older reader opens no copy
|
|
343
|
+
* from a one-time key, so turn this on only once readers have shipped
|
|
344
|
+
* (docs/seal-key.md, "One-time senders"). Omitted, the event is
|
|
345
|
+
* byte-identical to before. */
|
|
346
|
+
oneTimeSender?: boolean;
|
|
333
347
|
}
|
|
334
348
|
/** Answer one request: the current epoch sealed to the asking device, or a
|
|
335
349
|
* refusal it alone can read. */
|
package/dist/epoch.js
CHANGED
|
@@ -5,14 +5,14 @@ import { bytesToHex, hexToBytes, randomBytes } from '@noble/hashes/utils';
|
|
|
5
5
|
import { schnorr } from '@noble/curves/secp256k1.js';
|
|
6
6
|
import { base64urlnopad } from '@scure/base';
|
|
7
7
|
import { nip44 } from 'nostr-tools';
|
|
8
|
-
import { finalizeEvent, getPublicKey } from 'nostr-tools/pure';
|
|
8
|
+
import { finalizeEvent, generateSecretKey, getPublicKey } from 'nostr-tools/pure';
|
|
9
9
|
import { hexEquals, normaliseHex } from './hex.js';
|
|
10
10
|
import { KINDS } from './kinds.js';
|
|
11
11
|
import { deriveRoom } from './room.js';
|
|
12
12
|
import { verifyDeviceCredential } from './credential.js';
|
|
13
13
|
import { evaluateAccess } from './access.js';
|
|
14
14
|
import { verifyEventUncached } from './verify.js';
|
|
15
|
-
import { newerCredential, openSealed, sealTo } from './seal.js';
|
|
15
|
+
import { SENDER_TAG, newerCredential, openSealed, sealSender, sealTo } from './seal.js';
|
|
16
16
|
import { withExpiration } from './expiration.js';
|
|
17
17
|
import { epochCommitment } from './epoch-commit.js';
|
|
18
18
|
import { DEFAULT_REPLY_DELAY_MS, REFUSE_AGAIN_AFTER_SECONDS, RefusalLimiter, defaultReplySchedule, replyDelay } from './quiet-reply.js';
|
|
@@ -135,6 +135,14 @@ function memberListOf(members, removed) {
|
|
|
135
135
|
out.delete(normaliseHex(p));
|
|
136
136
|
return [...out].sort();
|
|
137
137
|
}
|
|
138
|
+
/** The key a rekey or grant seals from: the authority's own, or, with
|
|
139
|
+
* `oneTimeSender`, a fresh one named in a `from` tag and zeroed once used. */
|
|
140
|
+
function oneTimeSenderOf(opts) {
|
|
141
|
+
if (!opts.oneTimeSender)
|
|
142
|
+
return { sk: opts.authoritySk, tags: [], discard: () => { } };
|
|
143
|
+
const sk = generateSecretKey();
|
|
144
|
+
return { sk, tags: [[SENDER_TAG, getPublicKey(sk)]], discard: () => sk.fill(0) };
|
|
145
|
+
}
|
|
138
146
|
/** Announce the next epoch. See the module comment for what this is. */
|
|
139
147
|
export function encodeRekeyEvent(opts) {
|
|
140
148
|
require32(opts.authoritySk, 'authority secret key');
|
|
@@ -151,10 +159,16 @@ export function encodeRekeyEvent(opts) {
|
|
|
151
159
|
const keys = {};
|
|
152
160
|
const sealed = { v: 1, secret: base64urlnopad.encode(opts.next.secret) };
|
|
153
161
|
const plaintext = JSON.stringify(sealed);
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const
|
|
157
|
-
|
|
162
|
+
const sender = oneTimeSenderOf(opts);
|
|
163
|
+
try {
|
|
164
|
+
for (const raw of opts.closed ? [] : opts.recipients) {
|
|
165
|
+
const recipient = typeof raw === 'string' ? { device: raw } : raw;
|
|
166
|
+
const device = requireHex32(recipient.device, 'recipient device');
|
|
167
|
+
keys[device] = sealTo(plaintext, sender.sk, device, recipient.credential);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
sender.discard();
|
|
158
172
|
}
|
|
159
173
|
const body = {
|
|
160
174
|
v: 1,
|
|
@@ -174,6 +188,7 @@ export function encodeRekeyEvent(opts) {
|
|
|
174
188
|
tags: withExpiration([
|
|
175
189
|
['d', roomId],
|
|
176
190
|
['epoch', String(epoch)],
|
|
191
|
+
...sender.tags,
|
|
177
192
|
], opts.expiresAt),
|
|
178
193
|
content: nip44.v2.encrypt(JSON.stringify(body), opts.current.key),
|
|
179
194
|
}, opts.authoritySk);
|
|
@@ -239,13 +254,14 @@ export function decodeRekeyEvent(event, opts) {
|
|
|
239
254
|
notice.members = members;
|
|
240
255
|
const device = getPublicKey(opts.deviceSk);
|
|
241
256
|
const mine = Object.entries(body.keys).find(([to]) => hexEquals(to, device))?.[1];
|
|
242
|
-
|
|
257
|
+
const sender = sealSender(event);
|
|
258
|
+
if (typeof mine === 'string' && sender !== null) {
|
|
243
259
|
// A copy none of this device's keys opens is no copy: sealed to a
|
|
244
260
|
// seal key it has since erased, or one it never had. The rest of the
|
|
245
261
|
// rekey still says who was removed, so it is read without the secret,
|
|
246
262
|
// and the device asks for the epoch.
|
|
247
263
|
try {
|
|
248
|
-
const sealed = JSON.parse(openSealed(mine,
|
|
264
|
+
const sealed = JSON.parse(openSealed(mine, sender, opts.deviceSk, opts.sealSks));
|
|
249
265
|
if (sealed.v === 1 && typeof sealed.secret === 'string') {
|
|
250
266
|
const secret = base64urlnopad.decode(sealed.secret);
|
|
251
267
|
if (secret.length === 32)
|
|
@@ -506,14 +522,23 @@ export function encodeEpochGrant(opts) {
|
|
|
506
522
|
if (opts.passed !== undefined && opts.passed.length > 0)
|
|
507
523
|
body.passed = passedListOf(opts.passed, body.epoch);
|
|
508
524
|
}
|
|
525
|
+
const sender = oneTimeSenderOf(opts);
|
|
526
|
+
let content;
|
|
527
|
+
try {
|
|
528
|
+
content = sealTo(JSON.stringify(body), sender.sk, device, opts.credential);
|
|
529
|
+
}
|
|
530
|
+
finally {
|
|
531
|
+
sender.discard();
|
|
532
|
+
}
|
|
509
533
|
return finalizeEvent({
|
|
510
534
|
kind: KINDS.EPOCH_GRANT,
|
|
511
535
|
created_at: opts.now,
|
|
512
536
|
tags: withExpiration([
|
|
513
537
|
['d', roomId],
|
|
514
538
|
['p', device],
|
|
539
|
+
...sender.tags,
|
|
515
540
|
], opts.expiresAt),
|
|
516
|
-
content
|
|
541
|
+
content,
|
|
517
542
|
}, opts.authoritySk);
|
|
518
543
|
}
|
|
519
544
|
/** Read the authority's answer to this device's own request. Null for
|
|
@@ -535,7 +560,10 @@ export function decodeEpochGrant(event, opts) {
|
|
|
535
560
|
const addressed = event.tags.find((t) => t[0] === 'p')?.[1];
|
|
536
561
|
if (addressed === undefined || !hexEquals(addressed, device))
|
|
537
562
|
return null;
|
|
538
|
-
const
|
|
563
|
+
const sender = sealSender(event);
|
|
564
|
+
if (sender === null)
|
|
565
|
+
return null;
|
|
566
|
+
const body = JSON.parse(openSealed(event.content, sender, opts.deviceSk, opts.sealSks));
|
|
539
567
|
if (body.v !== 1 || typeof body.request !== 'string' || !hexEquals(body.request, opts.request))
|
|
540
568
|
return null;
|
|
541
569
|
if (body.refused === 'removed' || body.refused === 'closed' || body.refused === 'unknown')
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export type { DeviceCredential, AccessTier, AgentRule, RoomPolicy, KindredProof
|
|
|
7
7
|
export type { RelayTransport } from './transport.js';
|
|
8
8
|
export { createDeviceCredential, verifyDeviceCredential, PERSON_CREDENTIAL_MAX_SECONDS, RestampedCredentialExpiryError, } from './credential.js';
|
|
9
9
|
export type { CreateCredentialOptions, VerifyResult } from './credential.js';
|
|
10
|
-
export { SEAL_TAG, isSealPubkey, generateSealKey, credentialSeal, sealTarget, sealTo, openSealed, newerCredential, } from './seal.js';
|
|
10
|
+
export { SEAL_TAG, SENDER_TAG, sealSender, isSealPubkey, generateSealKey, credentialSeal, sealTarget, sealTo, openSealed, newerCredential, } from './seal.js';
|
|
11
11
|
export type { SealKey } from './seal.js';
|
|
12
12
|
export { deriveScoped, SCOPED_LABEL_PATTERN } from './scoped.js';
|
|
13
13
|
export type { ScopedKeys } from './scoped.js';
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ export { verifyEventUncached, boundedEventVerifier } from './verify.js';
|
|
|
5
5
|
export { localIdentity } from './identity.js';
|
|
6
6
|
export { KINDS } from './kinds.js';
|
|
7
7
|
export { createDeviceCredential, verifyDeviceCredential, PERSON_CREDENTIAL_MAX_SECONDS, RestampedCredentialExpiryError, } from './credential.js';
|
|
8
|
-
export { SEAL_TAG, isSealPubkey, generateSealKey, credentialSeal, sealTarget, sealTo, openSealed, newerCredential, } from './seal.js';
|
|
8
|
+
export { SEAL_TAG, SENDER_TAG, sealSender, isSealPubkey, generateSealKey, credentialSeal, sealTarget, sealTo, openSealed, newerCredential, } from './seal.js';
|
|
9
9
|
export { deriveScoped, SCOPED_LABEL_PATTERN } from './scoped.js';
|
|
10
10
|
export { createSubKeyCertificate, verifySubKeyCertificate, SUB_KEY_CERTIFICATE_SCOPE, } from './sub-cert.js';
|
|
11
11
|
export { generateRoomSecret, deriveRoom, encodeJoinUrl, decodeJoinUrl, parseRoomPolicy, ROOM_LABELS, } from './room.js';
|
package/dist/seal.d.ts
CHANGED
|
@@ -56,6 +56,26 @@ export declare function credentialSeal(credential: DeviceCredential): string | u
|
|
|
56
56
|
* exists to shut out.
|
|
57
57
|
*/
|
|
58
58
|
export declare function sealTarget(device: string, credential?: DeviceCredential): string;
|
|
59
|
+
/**
|
|
60
|
+
* The tag on a rekey (kind 1462) or an authority's grant (kind 20469) that
|
|
61
|
+
* names the one-time key it was sealed from. The event is still signed by
|
|
62
|
+
* the authority, so the tag is the authority's word; the one-time secret is
|
|
63
|
+
* discarded once the copies are sealed, so whoever later takes the
|
|
64
|
+
* authority's key cannot recompute them. Forward secrecy step 1, see
|
|
65
|
+
* docs/seal-key.md, "One-time senders".
|
|
66
|
+
*/
|
|
67
|
+
export declare const SENDER_TAG = "from";
|
|
68
|
+
/**
|
|
69
|
+
* Who sealed the copies in `event`: the key its single `from` tag names, or
|
|
70
|
+
* its signer when it carries none, which is every rekey and grant written
|
|
71
|
+
* before one-time senders. Null when the tags are unusable: more than one
|
|
72
|
+
* `from`, or one that is not a point on the curve. Only the authority could
|
|
73
|
+
* have signed either, so a reader treats null as a copy it cannot open.
|
|
74
|
+
*/
|
|
75
|
+
export declare function sealSender(event: {
|
|
76
|
+
pubkey: string;
|
|
77
|
+
tags: string[][];
|
|
78
|
+
}): string | null;
|
|
59
79
|
/** Seal `plaintext` from `senderSk` to `device`, by `sealTarget`. */
|
|
60
80
|
export declare function sealTo(plaintext: string, senderSk: Uint8Array, device: string, credential?: DeviceCredential): string;
|
|
61
81
|
/**
|
package/dist/seal.js
CHANGED
|
@@ -84,6 +84,31 @@ export function sealTarget(device, credential) {
|
|
|
84
84
|
return to;
|
|
85
85
|
return credentialSeal(credential) ?? to;
|
|
86
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* The tag on a rekey (kind 1462) or an authority's grant (kind 20469) that
|
|
89
|
+
* names the one-time key it was sealed from. The event is still signed by
|
|
90
|
+
* the authority, so the tag is the authority's word; the one-time secret is
|
|
91
|
+
* discarded once the copies are sealed, so whoever later takes the
|
|
92
|
+
* authority's key cannot recompute them. Forward secrecy step 1, see
|
|
93
|
+
* docs/seal-key.md, "One-time senders".
|
|
94
|
+
*/
|
|
95
|
+
export const SENDER_TAG = 'from';
|
|
96
|
+
/**
|
|
97
|
+
* Who sealed the copies in `event`: the key its single `from` tag names, or
|
|
98
|
+
* its signer when it carries none, which is every rekey and grant written
|
|
99
|
+
* before one-time senders. Null when the tags are unusable: more than one
|
|
100
|
+
* `from`, or one that is not a point on the curve. Only the authority could
|
|
101
|
+
* have signed either, so a reader treats null as a copy it cannot open.
|
|
102
|
+
*/
|
|
103
|
+
export function sealSender(event) {
|
|
104
|
+
const tags = event.tags.filter((t) => t[0] === SENDER_TAG);
|
|
105
|
+
if (tags.length === 0)
|
|
106
|
+
return normaliseHex(event.pubkey);
|
|
107
|
+
if (tags.length > 1)
|
|
108
|
+
return null;
|
|
109
|
+
const value = tags[0][1];
|
|
110
|
+
return isSealPubkey(value) ? normaliseHex(value) : null;
|
|
111
|
+
}
|
|
87
112
|
/** Seal `plaintext` from `senderSk` to `device`, by `sealTarget`. */
|
|
88
113
|
export function sealTo(plaintext, senderSk, device, credential) {
|
|
89
114
|
return nip44.v2.encrypt(plaintext, nip44.v2.utils.getConversationKey(senderSk, sealTarget(device, credential)));
|
package/docs/seal-key.md
CHANGED
|
@@ -89,6 +89,41 @@ Not covered here:
|
|
|
89
89
|
- `decodeEpochRequest` and `decodeMemberEpochRequest` return what they did in
|
|
90
90
|
0.6.0. The desks read the credential internally.
|
|
91
91
|
|
|
92
|
+
## One-time senders
|
|
93
|
+
|
|
94
|
+
Forward secrecy step 1 (KithMoot `docs/2026-10-06-key-schedule-next-steps.md`,
|
|
95
|
+
phase 2b). Every rekey copy and every authority grant used to be sealed from
|
|
96
|
+
the authority's long-lived key, so whoever later took that key could
|
|
97
|
+
recompute every past copy's conversation key and open it with nothing else.
|
|
98
|
+
|
|
99
|
+
With `oneTimeSender: true`, `encodeRekeyEvent` and `encodeEpochGrant` draw a
|
|
100
|
+
fresh key, seal every copy from it, name its public half in one
|
|
101
|
+
`["from", <x-only hex>]` tag, and zero the secret before signing. The event
|
|
102
|
+
is still signed by the authority, so the tag is the authority's word and a
|
|
103
|
+
forger's `from` is never read (the signer check comes first).
|
|
104
|
+
|
|
105
|
+
- A reader opens a copy with `sealSender(event)`: the single `from` tag's key,
|
|
106
|
+
or the signer when there is none. More than one `from`, or one off the
|
|
107
|
+
curve, is null: a rekey is then read without a secret (the device asks for
|
|
108
|
+
the epoch), and a grant is refused.
|
|
109
|
+
- Taking the authority's key later no longer opens a copy. It still lets the
|
|
110
|
+
taker sign new rekeys and grants; that is the healing question, not this
|
|
111
|
+
one.
|
|
112
|
+
- A copy is still sealed to the recipient's seal key, so a past copy stays
|
|
113
|
+
open to whoever holds that seal secret. Retiring seal keys older than the
|
|
114
|
+
history window is step 3.
|
|
115
|
+
- The member grant (20472) has always been signed by a one-time key, and is
|
|
116
|
+
unchanged.
|
|
117
|
+
|
|
118
|
+
**Readers first.** A reader older than 0.13.0 opens no copy from a one-time
|
|
119
|
+
key. For a rekey that costs a round trip (it asks for the epoch); for a grant
|
|
120
|
+
it costs the epoch. So `oneTimeSender` stays off in every writer until readers
|
|
121
|
+
have shipped on every client and had a release in the field. Off, every
|
|
122
|
+
event is byte-identical to before.
|
|
123
|
+
|
|
124
|
+
`hostRoomEpoch` has no switch for it yet; adding one is part of turning
|
|
125
|
+
writers on, alongside the keeper's own rekeys.
|
|
126
|
+
|
|
92
127
|
## Vectors
|
|
93
128
|
|
|
94
129
|
`vectors/seal-vectors.json` (generated by `scripts/generate-seal.mjs`,
|
|
@@ -102,3 +137,7 @@ checked by `vectors/verify-seal.test.ts`):
|
|
|
102
137
|
(byte-identical to a rekey with bare devices);
|
|
103
138
|
- `epoch-grant-seal` and `member-grant-seal` (the thief replays the copied
|
|
104
139
|
credential; the desk seals to the renewed one).
|
|
140
|
+
- `rekey-one-time-sender` and `epoch-grant-one-time-sender` (the same rekey
|
|
141
|
+
and grant from a one-time key; the authority's key no longer opens a copy),
|
|
142
|
+
and the refusals `rekey-from-doubled`, `rekey-from-off-curve` and
|
|
143
|
+
`epoch-grant-from-off-curve`.
|
package/llms.txt
CHANGED
|
@@ -10,7 +10,7 @@ ESM-only, Node.js 22.13+. Four entry points: `@forgesworn/fold-kit` (circle prim
|
|
|
10
10
|
npm install @forgesworn/fold-kit
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
This file describes `main` (version 0.
|
|
13
|
+
This file describes `main` (version 0.13.0), which can run ahead of the npm `latest` release; compare `npm view @forgesworn/fold-kit version` with the version of the API you need. To use an unreleased change, pin an immutable Git commit (a full SHA, for example from `git ls-remote https://github.com/forgesworn/fold-kit main`); the `prepare` script builds `dist/` on install:
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
16
|
npm install "github:forgesworn/fold-kit#<commit>"
|
|
@@ -139,7 +139,7 @@ Invitation versions: v1 (`encodeJoinUrl`, the room secret in the link) is legacy
|
|
|
139
139
|
- Quiet replies (see `docs/quiet-replies.md`): `hostRoomEpoch` waits a random `replyDelayMs` (`[min, max]` ms, default `DEFAULT_REPLY_DELAY_MS` = `[500, 2000]`, `[0, 0]` answers at once) before every grant or refusal, deciding when the wait ends; closing cancels waits. A participant is sent `removed`/`closed` at most once per `refuseAgainAfter` seconds (default `REFUSE_AGAIN_AFTER_SECONDS` = 3600) and `unknown` once per `reportUnknownEvery` (60); at most `maxRefusalsPerMinute` refusals (default `MAX_REFUSALS_PER_MINUTE` = 20) go out in any minute in all. Grants are never limited. A suppressed refusal still calls `onRefused`/`onUnknown`; its asker times out with the plain error. `random` and `schedule` (`ReplySchedule`) are injectable. `RefusalLimiter` (bounded to `REFUSAL_LIMITER_CAPACITY` = 256 keys) and `replyDelay(range, random)` are exported. Events and vectors are unchanged
|
|
140
140
|
- Member epoch catch-up (see `docs/member-epoch-catch-up.md`): `encodeRekeyEvent({ ..., commit: true })` writes `epochCommitment(roomId, epoch, secret)` into the rekey body; `hostMemberEpochDesk(opts)` lets any current member answer; `requestRoomEpoch({ ..., members: memberEpochSource(opts) })` asks the authority and members, first verifiable answer wins; `requestMemberEpoch(opts)` asks members alone. Kinds `MEMBER_EPOCH_KINDS` = `{ REQUEST: 20471, GRANT: 20472 }`. A member grant carries the authority-signed rekeys from the asker's epoch to the current one; the asker verifies the chain itself (`decodeMemberEpochGrant`) and refuses a last epoch with no commitment (legacy: authority only), a chain that removes it or closes the room, and one that stops short of `expected`. A verified grant is `{ epoch, removed, passed }`: `passed` holds the epochs between the asker's and `epoch`, oldest first, and `requestRoomEpoch` carries it as an optional `passed` when a member answered. Codecs: `encodeMemberEpochRequest`, `decodeMemberEpochRequest`, `encodeMemberEpochGrant`, `readRekeyEvidence`, `deriveMemberEpochRequestKey`, `MAX_MEMBER_EPOCH_CHAIN` (32)
|
|
141
141
|
- The known-members gate (#207, see `docs/member-epoch-catch-up.md`): once a room has removed anybody (`removed()` not empty), `hostRoomEpoch` and `hostMemberEpochDesk` grant only to a participant for which `known(participant)` is true; anybody else is reported through `onUnknown` (once per participant, and again every `reportUnknownEvery` seconds, default `REPORT_UNKNOWN_EVERY_SECONDS` = 60, while they keep asking) and refused: the authority answers `refused: 'unknown'` (an `EpochRefusal`), a member stays silent. `unknown` is not final: `requestRoomEpoch` keeps asking, calls its `onUnknown` once, and rejects with `EpochRefusedError('unknown')` only at its timeout; a desk looks at the requester's next ask afresh, so letting somebody in is making `known` say yes. No `known` after a removal means nobody is known. Member lists: `encodeRekeyEvent({ members })` writes the authority's list (removed dropped, sorted) into the body, read back as `RekeyNotice.members` and `RekeyEvidence.members`, and as `MemberEpochGrant.members` (newest in the chain); `hostRoomEpoch({ members })` puts it in its grants (`EpochGrant.members`). `readMemberList(raw)` reads one, undefined when malformed. Without `members` and with no removal every event is byte-identical to 0.5.1
|
|
142
|
-
- Seal keys (see `docs/seal-key.md`): `createDeviceCredential({ ..., seal })` appends `["seal", <x-only pubkey>]` last, under the participant's signature; mint a fresh one per credential with `generateSealKey()` and keep its secret as you keep the device key (old copies are sealed to old seal keys; healing needs only that each new one is fresh). `encodeRekeyEvent` recipients may be `{ device, credential }`; `encodeEpochGrant` and `encodeMemberEpochGrant` take `credential`; each copy is sealed to `sealTarget(device, credential)`: the credential's seal key, else the device key (never back to the device key because the credential lapsed). Readers take `sealSks` (`decodeRekeyEvent`, `decodeEpochGrant`, `decodeMemberEpochGrant`) or a `sealSks()` getter (`requestRoomEpoch`, `memberEpochSource`) and try them before the device key (`openSealed`); a rekey copy nothing opens reads as no copy. Desks take `credentialFor(device)`, the newest credential the room has seen (keep it monotone), and seal to the newer of that and the presented one (`sealCredential`, `newerCredential`: later expiration, then later created_at). `credentialSeal(c)` is the pubkey, undefined for none, null when doubled or off the curve (sealed to the device key). Without seal keys every event is byte-identical to 0.6.0
|
|
142
|
+
- Seal keys (see `docs/seal-key.md`): `createDeviceCredential({ ..., seal })` appends `["seal", <x-only pubkey>]` last, under the participant's signature; mint a fresh one per credential with `generateSealKey()` and keep its secret as you keep the device key (old copies are sealed to old seal keys; healing needs only that each new one is fresh). `encodeRekeyEvent` recipients may be `{ device, credential }`; `encodeEpochGrant` and `encodeMemberEpochGrant` take `credential`; each copy is sealed to `sealTarget(device, credential)`: the credential's seal key, else the device key (never back to the device key because the credential lapsed). Readers take `sealSks` (`decodeRekeyEvent`, `decodeEpochGrant`, `decodeMemberEpochGrant`) or a `sealSks()` getter (`requestRoomEpoch`, `memberEpochSource`) and try them before the device key (`openSealed`); a rekey copy nothing opens reads as no copy. Desks take `credentialFor(device)`, the newest credential the room has seen (keep it monotone), and seal to the newer of that and the presented one (`sealCredential`, `newerCredential`: later expiration, then later created_at). `credentialSeal(c)` is the pubkey, undefined for none, null when doubled or off the curve (sealed to the device key). Without seal keys every event is byte-identical to 0.6.0 - One-time senders (see `docs/seal-key.md`, "One-time senders"): `encodeRekeyEvent` and `encodeEpochGrant` take `oneTimeSender: true`, sealing every copy from a fresh key named in one `["from", <x-only pubkey>]` tag (still authority-signed) and zeroing it, so a later copy of the authority key opens no past copy; readers open with `sealSender(event)` (the `from` key, else the signer; null when doubled or off the curve: a rekey reads without a secret, a grant is refused). Keep writers off until every client's readers have shipped; off, events are byte-identical to 0.12.0.
|
|
143
143
|
- Scheduled rekeys and the history window (see `docs/scheduled-rekey.md`): `encodeRekeyEvent({ ..., scheduled: true })` writes `"scheduled": true` inside the encrypted body (never a tag), and throws beside a non-empty `removed` or `closed`; `decodeRekeyEvent` (`RekeyNotice.scheduled`) and `readRekeyEvidence` (`RekeyEvidence.scheduled`) report `true` only when `removed` is empty and the room is not closed, so a contradictory body is still announced. `HISTORY_WINDOW_SECONDS` (30 days) and `MAX_HISTORY_EPOCHS` (16); `epochsInWindow(left, now)` keeps entries with `leftAt >= now - HISTORY_WINDOW_SECONDS`, newest first by epoch, first of a doubled epoch, at most 16, never throws. `encodeEpochGrant({ ..., passed })` takes `LeftEpoch[]` (`{ epoch, secret, leftAt }`) and writes `passed: [{ epoch, secret (base64url), left }]` oldest first, only when non-empty; it throws on more than 16, epoch 0 (the room secret is never carried), an epoch not below the granted one, a doubled epoch, a non-32-byte secret or a bad `leftAt`. `hostRoomEpoch({ past })` is asked on every grant, filtered to what the encoder accepts and to `epochsInWindow`, so a bad `past()` costs the history, not the grant. `decodeEpochGrant` returns `EpochGrant.passed` (`leftAt` set) only when the list is exactly in that form, else drops the field and keeps the grant; `decodeMemberEpochGrant` sets `leftAt` on each passed epoch from the next rekey's `created_at`. `MAX_MEMBER_EPOCH_CHAIN` (32) is a different limit. Without the marker and `passed` every event is byte-identical to 0.7.0, and a 0.7.0 reader ignores both
|
|
144
144
|
- `canonicalAdmins(admins)` / `signAdmins(opts)` / `verifyAdmins(opts)` - the authority's signed admin list
|
|
145
145
|
- `canonicalChannels(channels)` / `signChannels(opts)` / `verifyChannels(opts)` - the authority's signed channel list; `CHANNEL_NAME`, `RESERVED_CHANNELS`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgesworn/fold-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Circle primitives for private Nostr groups nobody operates: epoch keys, link invites, removal by rekey, device credentials.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -623,6 +623,310 @@
|
|
|
623
623
|
},
|
|
624
624
|
"thief": null
|
|
625
625
|
}
|
|
626
|
+
},
|
|
627
|
+
{
|
|
628
|
+
"name": "rekey-one-time-sender",
|
|
629
|
+
"kind": "positive",
|
|
630
|
+
"note": "rekey-seal's rekey with `oneTimeSender`: tags d, epoch, then [\"from\", x-only pubkey of a key made for this rekey alone], still signed by the authority, so the tag is the authority's word. Every copy is sealed from that one-time key to the recipient's seal key (or device key), and the one-time secret is discarded, so whoever later takes the authority's key cannot recompute the copies (`authorityOpens` false). A reader opens its copy with the key the single `from` tag names (`sealSender`), and with the signer when there is none, which is every earlier rekey. `randomHex`: the 48-byte seed for the one-time key (`senderSkHex`), one seal nonce per recipient in order, the body nonce, the signature aux-rand. A reader older than this opens no copy, reads the rekey without a secret and asks for the epoch.",
|
|
631
|
+
"input": {
|
|
632
|
+
"roomId": "58c0b5a427b80559f5b67f35e74c686f630259dcdafca2ab03e3b082f4a1740c",
|
|
633
|
+
"authority": "4fca1ba4b39e24ea0acea325af3e6a62315903952c277e197cccfe8f0143c533",
|
|
634
|
+
"authoritySkHex": "bb058b43370d37b7ed3f09e3a36a6ffcc88a8dad56c5ee204b1715240a1466e0",
|
|
635
|
+
"previousKeyHex": "09126b23a6148694280325e5a433c1badbc853026c8e71c0833ffd4bd0ea08ab",
|
|
636
|
+
"next": {
|
|
637
|
+
"epoch": 1,
|
|
638
|
+
"secretHex": "cc4e6941abd893daab2fdce0bbb182816b531fbc04b9531fdc5d03fd742fb26b"
|
|
639
|
+
},
|
|
640
|
+
"recipients": [
|
|
641
|
+
{
|
|
642
|
+
"device": "f29e30f72c9aaaa5066aa366afd4f0970e13853d10922cf5ba2ece21dc18ee5b",
|
|
643
|
+
"credential": {
|
|
644
|
+
"id": "3c3f64a769c826a89f2712ed9898e43afacc5f2784201999e8ca0ed5cdcf3d6c",
|
|
645
|
+
"pubkey": "3683caad0bf9bb5a653582155c0ba9192fd23d7d6c717a7b0d233c8356a7042f",
|
|
646
|
+
"created_at": 1800000000,
|
|
647
|
+
"kind": 20460,
|
|
648
|
+
"tags": [
|
|
649
|
+
[
|
|
650
|
+
"d",
|
|
651
|
+
"58c0b5a427b80559f5b67f35e74c686f630259dcdafca2ab03e3b082f4a1740c"
|
|
652
|
+
],
|
|
653
|
+
[
|
|
654
|
+
"device",
|
|
655
|
+
"f29e30f72c9aaaa5066aa366afd4f0970e13853d10922cf5ba2ece21dc18ee5b"
|
|
656
|
+
],
|
|
657
|
+
[
|
|
658
|
+
"expiration",
|
|
659
|
+
"1800043200"
|
|
660
|
+
],
|
|
661
|
+
[
|
|
662
|
+
"seal",
|
|
663
|
+
"63a40c9bc84c065f4aab7e63208ea37c3edcd3ab47a334b157cf000409c7eaaf"
|
|
664
|
+
]
|
|
665
|
+
],
|
|
666
|
+
"content": "",
|
|
667
|
+
"sig": "fdd0c5f23d2f1531594100470d6fe6c87d6c28d2185898b49844b664ffcb65f3c76ce0b9fcef3166708baca068d7f9d2ee74a827f56140b6cef11591ce942049"
|
|
668
|
+
}
|
|
669
|
+
},
|
|
670
|
+
"1cddb17ae843752d2469ea50b2a6bd1468edbb75b7913ea58ba7847663cb1b8e"
|
|
671
|
+
],
|
|
672
|
+
"deviceSkHex": "678d943bfdab0df34f3945be66ca867a4c15b507d7a9c125651f9018a905f305",
|
|
673
|
+
"bareDeviceSkHex": "32796464cb7434a5d397f03cce5055df28f9be8d15ce4bf14760d764bbd2e156",
|
|
674
|
+
"renewedSealSkHex": "7729de6b913094fc7fd121d12671448494184b9eff28633869a0c81567291035",
|
|
675
|
+
"copiedSealSkHex": "22b29538be86feaf0dbe57fcd046cc95ca39082e813d450d8d9d653a2434a374",
|
|
676
|
+
"createdAt": 1800000000,
|
|
677
|
+
"event": {
|
|
678
|
+
"id": "3f38d5f8a93cb0818f9a62fccc8ba59b44ebc413c1369205447f840e46bfb474",
|
|
679
|
+
"pubkey": "4fca1ba4b39e24ea0acea325af3e6a62315903952c277e197cccfe8f0143c533",
|
|
680
|
+
"created_at": 1800000000,
|
|
681
|
+
"kind": 1462,
|
|
682
|
+
"tags": [
|
|
683
|
+
[
|
|
684
|
+
"d",
|
|
685
|
+
"58c0b5a427b80559f5b67f35e74c686f630259dcdafca2ab03e3b082f4a1740c"
|
|
686
|
+
],
|
|
687
|
+
[
|
|
688
|
+
"epoch",
|
|
689
|
+
"1"
|
|
690
|
+
],
|
|
691
|
+
[
|
|
692
|
+
"from",
|
|
693
|
+
"02f71d0f7235cbe6e623bc1542539c9db10601a3a892af483783614c00c7fd99"
|
|
694
|
+
]
|
|
695
|
+
],
|
|
696
|
+
"content": "AmcNeMrs/5cl6E/Z1PoCcQXOxzckQIRSjE9XOZJKvrsqFN0ZfMu/0hPX0dWFqdgAWRDZ5/K+9O3ajNsKlDmnx5zcAtlrzeJUTDGtN6YrwcorfL6NRMeSz9bnic0jAGAxo+Zj5n0FUV9qOzAkjKktseXy6y2oQeQ3gN2uPW1GXSOcaH++d3YwmiND6c48I8SlsNB5wRVawNb5Dm6UViC4h0wpbBR9sLqA08+p8IMFx2HxmENDbNNMfeQVcMhNGZ+XobZcsH4Zd0JNVxRVUGF0TOYCfaOQFAzO3LxfXI9tcC9vaisCA/DbGoGiz5nqOwU4xnsF5RgdX6yX+xIQXSzCtne69p/tdjePhsL8WmlENE131aSBdY/L7bUAumu+FJxfVJBzNCz+y/o0noBLirOrUuUIqWxFnQ/zoWsE+gYCU9Um9LvtbhK+m1+COhl6K09ruh/qlcZBn7OEciUywGZSkRadpm4kktc5cCfGbdrlkfVPwgrJiRVYLkIC+O+N+xqx037XLOtI3wC460QDnaoii207OrsdGip8F5K1sZ8ZV/Mc+gNmcCKAZJEGaoyowPvP4cYXMyos7j3MCZ0lpwi1E8jwyXYJZK17JKXmykEpBywPpYLSgBQR9C9yJ6oCfaVsxkOBVgbEiPKpA+hGngsiDmGBoy41VdgpA9ZCBTIH5W8B2z1xqCk+dHVkczJqYNpM+Dad0Xagq42rnK1kLff2bFsIKxly3ak1uFRYBCQbS74BEo2kbczOAn1XLfPYMLiEiMCdjXgILzVgqe4TVjue9Nv7Zak5iheUPqLVZfxTbuQUWIWrLakQWSVHfQ8c+/L98/5M9TsgYSIV5BS5JCABvbCVe4RHeq4ibECcsf41I2qPUeCSXxHuTvJ26L/T3ONpAEzXRw2+8tJNaK3Plczm5f9TfYPL3yuZGcplKsgPwogm0us=",
|
|
697
|
+
"sig": "a6e036a4b6ac94703f747337bf28c308a295fcfc2ab3c028d5a35cc50969ba19807a9ba14252ef7782e4b4bfd3395c71986eea1cdcdc36e2fe18a77d9ce1219d"
|
|
698
|
+
},
|
|
699
|
+
"randomHex": [
|
|
700
|
+
"a110c4cffaadabb2fedbf6279627cc857757f036ecaffdcb89452e2eb12acc8349c693f9cce385720b83f75629ae26e6",
|
|
701
|
+
"6d4bbae698b19d076387d818c6a9b8aaaa23ab42850bb933ab1ab0ab4a36ce91",
|
|
702
|
+
"a25e6555c2cbbf30733f70ba666453a3712cf5dbefd4c1cea47fa9b70b1b691a",
|
|
703
|
+
"670d78caecff9725e84fd9d4fa027105cec737244084528c4f5739924abebb2a",
|
|
704
|
+
"18c1fc0b6372dc662bcafd2d4704e5c1f4bcc9c61b1e129a91dec165a6296212"
|
|
705
|
+
],
|
|
706
|
+
"senderSkHex": "4405467674110e0cb5278a657356cac70724f7442d2dc3ade9e350768088ff67"
|
|
707
|
+
},
|
|
708
|
+
"output": {
|
|
709
|
+
"sealSender": "02f71d0f7235cbe6e623bc1542539c9db10601a3a892af483783614c00c7fd99",
|
|
710
|
+
"renewedDevice": {
|
|
711
|
+
"epoch": 1,
|
|
712
|
+
"secretHex": "cc4e6941abd893daab2fdce0bbb182816b531fbc04b9531fdc5d03fd742fb26b"
|
|
713
|
+
},
|
|
714
|
+
"thief": {
|
|
715
|
+
"epoch": 1
|
|
716
|
+
},
|
|
717
|
+
"bareDevice": {
|
|
718
|
+
"epoch": 1,
|
|
719
|
+
"secretHex": "cc4e6941abd893daab2fdce0bbb182816b531fbc04b9531fdc5d03fd742fb26b"
|
|
720
|
+
},
|
|
721
|
+
"authorityOpens": {
|
|
722
|
+
"f29e30f72c9aaaa5066aa366afd4f0970e13853d10922cf5ba2ece21dc18ee5b": false,
|
|
723
|
+
"1cddb17ae843752d2469ea50b2a6bd1468edbb75b7913ea58ba7847663cb1b8e": false
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
},
|
|
727
|
+
{
|
|
728
|
+
"name": "rekey-from-doubled",
|
|
729
|
+
"kind": "negative",
|
|
730
|
+
"note": "rekey-one-time-sender's content re-signed by the authority with two `from` tags. Only the authority could sign it, so it is a broken writer, not an attack: `sealSender` is null, the rekey is still read for its epoch and removals, and no copy is opened (no secretHex); the device asks for the epoch.",
|
|
731
|
+
"input": {
|
|
732
|
+
"roomId": "58c0b5a427b80559f5b67f35e74c686f630259dcdafca2ab03e3b082f4a1740c",
|
|
733
|
+
"authority": "4fca1ba4b39e24ea0acea325af3e6a62315903952c277e197cccfe8f0143c533",
|
|
734
|
+
"previousKeyHex": "09126b23a6148694280325e5a433c1badbc853026c8e71c0833ffd4bd0ea08ab",
|
|
735
|
+
"deviceSkHex": "678d943bfdab0df34f3945be66ca867a4c15b507d7a9c125651f9018a905f305",
|
|
736
|
+
"renewedSealSkHex": "7729de6b913094fc7fd121d12671448494184b9eff28633869a0c81567291035",
|
|
737
|
+
"event": {
|
|
738
|
+
"id": "fc8be7e8e1965a5888ca5351706456d76c8e548c3fc0a9378e0bb1caf4be23f6",
|
|
739
|
+
"pubkey": "4fca1ba4b39e24ea0acea325af3e6a62315903952c277e197cccfe8f0143c533",
|
|
740
|
+
"created_at": 1800000000,
|
|
741
|
+
"kind": 1462,
|
|
742
|
+
"tags": [
|
|
743
|
+
[
|
|
744
|
+
"d",
|
|
745
|
+
"58c0b5a427b80559f5b67f35e74c686f630259dcdafca2ab03e3b082f4a1740c"
|
|
746
|
+
],
|
|
747
|
+
[
|
|
748
|
+
"epoch",
|
|
749
|
+
"1"
|
|
750
|
+
],
|
|
751
|
+
[
|
|
752
|
+
"from",
|
|
753
|
+
"02f71d0f7235cbe6e623bc1542539c9db10601a3a892af483783614c00c7fd99"
|
|
754
|
+
],
|
|
755
|
+
[
|
|
756
|
+
"from",
|
|
757
|
+
"4fca1ba4b39e24ea0acea325af3e6a62315903952c277e197cccfe8f0143c533"
|
|
758
|
+
]
|
|
759
|
+
],
|
|
760
|
+
"content": "AmcNeMrs/5cl6E/Z1PoCcQXOxzckQIRSjE9XOZJKvrsqFN0ZfMu/0hPX0dWFqdgAWRDZ5/K+9O3ajNsKlDmnx5zcAtlrzeJUTDGtN6YrwcorfL6NRMeSz9bnic0jAGAxo+Zj5n0FUV9qOzAkjKktseXy6y2oQeQ3gN2uPW1GXSOcaH++d3YwmiND6c48I8SlsNB5wRVawNb5Dm6UViC4h0wpbBR9sLqA08+p8IMFx2HxmENDbNNMfeQVcMhNGZ+XobZcsH4Zd0JNVxRVUGF0TOYCfaOQFAzO3LxfXI9tcC9vaisCA/DbGoGiz5nqOwU4xnsF5RgdX6yX+xIQXSzCtne69p/tdjePhsL8WmlENE131aSBdY/L7bUAumu+FJxfVJBzNCz+y/o0noBLirOrUuUIqWxFnQ/zoWsE+gYCU9Um9LvtbhK+m1+COhl6K09ruh/qlcZBn7OEciUywGZSkRadpm4kktc5cCfGbdrlkfVPwgrJiRVYLkIC+O+N+xqx037XLOtI3wC460QDnaoii207OrsdGip8F5K1sZ8ZV/Mc+gNmcCKAZJEGaoyowPvP4cYXMyos7j3MCZ0lpwi1E8jwyXYJZK17JKXmykEpBywPpYLSgBQR9C9yJ6oCfaVsxkOBVgbEiPKpA+hGngsiDmGBoy41VdgpA9ZCBTIH5W8B2z1xqCk+dHVkczJqYNpM+Dad0Xagq42rnK1kLff2bFsIKxly3ak1uFRYBCQbS74BEo2kbczOAn1XLfPYMLiEiMCdjXgILzVgqe4TVjue9Nv7Zak5iheUPqLVZfxTbuQUWIWrLakQWSVHfQ8c+/L98/5M9TsgYSIV5BS5JCABvbCVe4RHeq4ibECcsf41I2qPUeCSXxHuTvJ26L/T3ONpAEzXRw2+8tJNaK3Plczm5f9TfYPL3yuZGcplKsgPwogm0us=",
|
|
761
|
+
"sig": "6f034c009f3eb02e069355d5a065f3aa72d1fe92dd5e6bc1bedbd905dfca08370fdc39b21470341397eafc2a5ad1c977c4658811aab3198937099ce844f0db37"
|
|
762
|
+
}
|
|
763
|
+
},
|
|
764
|
+
"output": {
|
|
765
|
+
"sealSender": null,
|
|
766
|
+
"renewedDevice": {
|
|
767
|
+
"epoch": 1
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
},
|
|
771
|
+
{
|
|
772
|
+
"name": "rekey-from-off-curve",
|
|
773
|
+
"kind": "negative",
|
|
774
|
+
"note": "The same with one `from` tag that is not the x coordinate of any point (x = 5): `sealSender` null, read without a secret.",
|
|
775
|
+
"input": {
|
|
776
|
+
"roomId": "58c0b5a427b80559f5b67f35e74c686f630259dcdafca2ab03e3b082f4a1740c",
|
|
777
|
+
"authority": "4fca1ba4b39e24ea0acea325af3e6a62315903952c277e197cccfe8f0143c533",
|
|
778
|
+
"previousKeyHex": "09126b23a6148694280325e5a433c1badbc853026c8e71c0833ffd4bd0ea08ab",
|
|
779
|
+
"deviceSkHex": "678d943bfdab0df34f3945be66ca867a4c15b507d7a9c125651f9018a905f305",
|
|
780
|
+
"renewedSealSkHex": "7729de6b913094fc7fd121d12671448494184b9eff28633869a0c81567291035",
|
|
781
|
+
"event": {
|
|
782
|
+
"id": "3d0722b71fa5a04938936d6fd89ba718830a59c89ecb060931f442394e1702ab",
|
|
783
|
+
"pubkey": "4fca1ba4b39e24ea0acea325af3e6a62315903952c277e197cccfe8f0143c533",
|
|
784
|
+
"created_at": 1800000000,
|
|
785
|
+
"kind": 1462,
|
|
786
|
+
"tags": [
|
|
787
|
+
[
|
|
788
|
+
"d",
|
|
789
|
+
"58c0b5a427b80559f5b67f35e74c686f630259dcdafca2ab03e3b082f4a1740c"
|
|
790
|
+
],
|
|
791
|
+
[
|
|
792
|
+
"epoch",
|
|
793
|
+
"1"
|
|
794
|
+
],
|
|
795
|
+
[
|
|
796
|
+
"from",
|
|
797
|
+
"0000000000000000000000000000000000000000000000000000000000000005"
|
|
798
|
+
]
|
|
799
|
+
],
|
|
800
|
+
"content": "AmcNeMrs/5cl6E/Z1PoCcQXOxzckQIRSjE9XOZJKvrsqFN0ZfMu/0hPX0dWFqdgAWRDZ5/K+9O3ajNsKlDmnx5zcAtlrzeJUTDGtN6YrwcorfL6NRMeSz9bnic0jAGAxo+Zj5n0FUV9qOzAkjKktseXy6y2oQeQ3gN2uPW1GXSOcaH++d3YwmiND6c48I8SlsNB5wRVawNb5Dm6UViC4h0wpbBR9sLqA08+p8IMFx2HxmENDbNNMfeQVcMhNGZ+XobZcsH4Zd0JNVxRVUGF0TOYCfaOQFAzO3LxfXI9tcC9vaisCA/DbGoGiz5nqOwU4xnsF5RgdX6yX+xIQXSzCtne69p/tdjePhsL8WmlENE131aSBdY/L7bUAumu+FJxfVJBzNCz+y/o0noBLirOrUuUIqWxFnQ/zoWsE+gYCU9Um9LvtbhK+m1+COhl6K09ruh/qlcZBn7OEciUywGZSkRadpm4kktc5cCfGbdrlkfVPwgrJiRVYLkIC+O+N+xqx037XLOtI3wC460QDnaoii207OrsdGip8F5K1sZ8ZV/Mc+gNmcCKAZJEGaoyowPvP4cYXMyos7j3MCZ0lpwi1E8jwyXYJZK17JKXmykEpBywPpYLSgBQR9C9yJ6oCfaVsxkOBVgbEiPKpA+hGngsiDmGBoy41VdgpA9ZCBTIH5W8B2z1xqCk+dHVkczJqYNpM+Dad0Xagq42rnK1kLff2bFsIKxly3ak1uFRYBCQbS74BEo2kbczOAn1XLfPYMLiEiMCdjXgILzVgqe4TVjue9Nv7Zak5iheUPqLVZfxTbuQUWIWrLakQWSVHfQ8c+/L98/5M9TsgYSIV5BS5JCABvbCVe4RHeq4ibECcsf41I2qPUeCSXxHuTvJ26L/T3ONpAEzXRw2+8tJNaK3Plczm5f9TfYPL3yuZGcplKsgPwogm0us=",
|
|
801
|
+
"sig": "eff2cc0225466dd6367fda5509d36fcb17e88b28039148b17870100ebe65b7658b005a18d0563ae01003ff88833d0716d01029b2e1b78456d63e19d27261cb24"
|
|
802
|
+
}
|
|
803
|
+
},
|
|
804
|
+
"output": {
|
|
805
|
+
"sealSender": null,
|
|
806
|
+
"renewedDevice": {
|
|
807
|
+
"epoch": 1
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
},
|
|
811
|
+
{
|
|
812
|
+
"name": "epoch-grant-one-time-sender",
|
|
813
|
+
"kind": "positive",
|
|
814
|
+
"note": "epoch-grant-seal's grant with `oneTimeSender`: tags d, p, then [\"from\", one-time pubkey], signed by the authority, content sealed from the one-time key to the renewed seal key. `randomHex`: the 48-byte seed for the one-time key (`senderSkHex`), body nonce, aux-rand. The renewed device opens it; the thief does not; the authority's key no longer opens it. A reader older than this cannot open it at all, which is why writers wait for readers.",
|
|
815
|
+
"input": {
|
|
816
|
+
"roomId": "58c0b5a427b80559f5b67f35e74c686f630259dcdafca2ab03e3b082f4a1740c",
|
|
817
|
+
"authority": "4fca1ba4b39e24ea0acea325af3e6a62315903952c277e197cccfe8f0143c533",
|
|
818
|
+
"authoritySkHex": "bb058b43370d37b7ed3f09e3a36a6ffcc88a8dad56c5ee204b1715240a1466e0",
|
|
819
|
+
"deviceSkHex": "678d943bfdab0df34f3945be66ca867a4c15b507d7a9c125651f9018a905f305",
|
|
820
|
+
"now": 1800000000,
|
|
821
|
+
"request": "ef8f52180b25300214a75780fc12a89cb1d4ee1a88a78b2f8afb92605769e87a",
|
|
822
|
+
"credential": {
|
|
823
|
+
"id": "3c3f64a769c826a89f2712ed9898e43afacc5f2784201999e8ca0ed5cdcf3d6c",
|
|
824
|
+
"pubkey": "3683caad0bf9bb5a653582155c0ba9192fd23d7d6c717a7b0d233c8356a7042f",
|
|
825
|
+
"created_at": 1800000000,
|
|
826
|
+
"kind": 20460,
|
|
827
|
+
"tags": [
|
|
828
|
+
[
|
|
829
|
+
"d",
|
|
830
|
+
"58c0b5a427b80559f5b67f35e74c686f630259dcdafca2ab03e3b082f4a1740c"
|
|
831
|
+
],
|
|
832
|
+
[
|
|
833
|
+
"device",
|
|
834
|
+
"f29e30f72c9aaaa5066aa366afd4f0970e13853d10922cf5ba2ece21dc18ee5b"
|
|
835
|
+
],
|
|
836
|
+
[
|
|
837
|
+
"expiration",
|
|
838
|
+
"1800043200"
|
|
839
|
+
],
|
|
840
|
+
[
|
|
841
|
+
"seal",
|
|
842
|
+
"63a40c9bc84c065f4aab7e63208ea37c3edcd3ab47a334b157cf000409c7eaaf"
|
|
843
|
+
]
|
|
844
|
+
],
|
|
845
|
+
"content": "",
|
|
846
|
+
"sig": "fdd0c5f23d2f1531594100470d6fe6c87d6c28d2185898b49844b664ffcb65f3c76ce0b9fcef3166708baca068d7f9d2ee74a827f56140b6cef11591ce942049"
|
|
847
|
+
},
|
|
848
|
+
"epoch": {
|
|
849
|
+
"epoch": 2,
|
|
850
|
+
"secretHex": "1173b8e8457a46fdb6ae949e37aa41ce259534d4bb4f15a5fdb74aefc83344f5"
|
|
851
|
+
},
|
|
852
|
+
"renewedSealSkHex": "7729de6b913094fc7fd121d12671448494184b9eff28633869a0c81567291035",
|
|
853
|
+
"copiedSealSkHex": "22b29538be86feaf0dbe57fcd046cc95ca39082e813d450d8d9d653a2434a374",
|
|
854
|
+
"event": {
|
|
855
|
+
"id": "38ad58597fa0c34a18e3975e755c27523b4c1cdf60d4a49aa89d2817f734dc76",
|
|
856
|
+
"pubkey": "4fca1ba4b39e24ea0acea325af3e6a62315903952c277e197cccfe8f0143c533",
|
|
857
|
+
"created_at": 1800000000,
|
|
858
|
+
"kind": 20469,
|
|
859
|
+
"tags": [
|
|
860
|
+
[
|
|
861
|
+
"d",
|
|
862
|
+
"58c0b5a427b80559f5b67f35e74c686f630259dcdafca2ab03e3b082f4a1740c"
|
|
863
|
+
],
|
|
864
|
+
[
|
|
865
|
+
"p",
|
|
866
|
+
"f29e30f72c9aaaa5066aa366afd4f0970e13853d10922cf5ba2ece21dc18ee5b"
|
|
867
|
+
],
|
|
868
|
+
[
|
|
869
|
+
"from",
|
|
870
|
+
"0f44811c4dab721de6ee8e8d740b264fe8e106453fca7fe2a4543656cf10bb98"
|
|
871
|
+
]
|
|
872
|
+
],
|
|
873
|
+
"content": "Ar+D/3yOjgZRgyeW88MD9C17M37/kTTSSXfT5gTHvH8ecKBRUai4cGo/MMEiMBPN7b/YLj+HKOab20VjFGh1b6+CwWjoruSUpgvxNvZUcKiOF2lmZp6Awd2R46k55IJ8ShyGHQ674wyHyBs7J2GbZeEZZlsAPOztW0iCXEB4RzdUP4HrmyYbhKxY9g+8juxhT91aMIH7bmFN+kOCrOO3W+4gaj73tGRyB6OBvMKledoK1ypL+COkXqj4GzFUcDsG88fFOD6+JMnVoUDNhyobovbxRsj6B5wApTkr6Bj6nAr5+er5RTTEBAwmUuIc/V7qFzK76NUmYGunr6r5bZHaxLld3Q==",
|
|
874
|
+
"sig": "7992b0b101abb281e45c6066132e571a30ef8b02fcd0eb25f8764cf967c960d89f8d6def75e7bf91cf31d6af1efaa7b9d0171eaeba7482521afe9bc07d1fde17"
|
|
875
|
+
},
|
|
876
|
+
"randomHex": [
|
|
877
|
+
"5ac80b90d9396022bf334b58968aec74e7c8c72a7f62635b91a13924910c605a62efa5fac4e4ce2c169755acc6d1b49c",
|
|
878
|
+
"bf83ff7c8e8e0651832796f3c303f42d7b337eff9134d24977d3e604c7bc7f1e",
|
|
879
|
+
"a100e717ef4874d688fb51f37339cc25d5eddb8a17f3c554f939a7d3098ad8fa"
|
|
880
|
+
],
|
|
881
|
+
"senderSkHex": "5b2583975fba6efa84bdfd6fa48d2422e1d31090140b71d4cadc19e4ed5ee25d"
|
|
882
|
+
},
|
|
883
|
+
"output": {
|
|
884
|
+
"sealSender": "0f44811c4dab721de6ee8e8d740b264fe8e106453fca7fe2a4543656cf10bb98",
|
|
885
|
+
"renewedDevice": {
|
|
886
|
+
"epoch": 2,
|
|
887
|
+
"secretHex": "1173b8e8457a46fdb6ae949e37aa41ce259534d4bb4f15a5fdb74aefc83344f5"
|
|
888
|
+
},
|
|
889
|
+
"thief": null,
|
|
890
|
+
"authorityOpens": false
|
|
891
|
+
}
|
|
892
|
+
},
|
|
893
|
+
{
|
|
894
|
+
"name": "epoch-grant-from-off-curve",
|
|
895
|
+
"kind": "negative",
|
|
896
|
+
"note": "epoch-grant-one-time-sender's content re-signed by the authority with a `from` tag off the curve: the grant is refused (null).",
|
|
897
|
+
"input": {
|
|
898
|
+
"roomId": "58c0b5a427b80559f5b67f35e74c686f630259dcdafca2ab03e3b082f4a1740c",
|
|
899
|
+
"authority": "4fca1ba4b39e24ea0acea325af3e6a62315903952c277e197cccfe8f0143c533",
|
|
900
|
+
"deviceSkHex": "678d943bfdab0df34f3945be66ca867a4c15b507d7a9c125651f9018a905f305",
|
|
901
|
+
"now": 1800000000,
|
|
902
|
+
"request": "ef8f52180b25300214a75780fc12a89cb1d4ee1a88a78b2f8afb92605769e87a",
|
|
903
|
+
"renewedSealSkHex": "7729de6b913094fc7fd121d12671448494184b9eff28633869a0c81567291035",
|
|
904
|
+
"event": {
|
|
905
|
+
"id": "3185002094d21dd5f585b6e718c8587a5ab494235e73a5351f51ba0648af5db9",
|
|
906
|
+
"pubkey": "4fca1ba4b39e24ea0acea325af3e6a62315903952c277e197cccfe8f0143c533",
|
|
907
|
+
"created_at": 1800000000,
|
|
908
|
+
"kind": 20469,
|
|
909
|
+
"tags": [
|
|
910
|
+
[
|
|
911
|
+
"d",
|
|
912
|
+
"58c0b5a427b80559f5b67f35e74c686f630259dcdafca2ab03e3b082f4a1740c"
|
|
913
|
+
],
|
|
914
|
+
[
|
|
915
|
+
"p",
|
|
916
|
+
"f29e30f72c9aaaa5066aa366afd4f0970e13853d10922cf5ba2ece21dc18ee5b"
|
|
917
|
+
],
|
|
918
|
+
[
|
|
919
|
+
"from",
|
|
920
|
+
"0000000000000000000000000000000000000000000000000000000000000005"
|
|
921
|
+
]
|
|
922
|
+
],
|
|
923
|
+
"content": "Ar+D/3yOjgZRgyeW88MD9C17M37/kTTSSXfT5gTHvH8ecKBRUai4cGo/MMEiMBPN7b/YLj+HKOab20VjFGh1b6+CwWjoruSUpgvxNvZUcKiOF2lmZp6Awd2R46k55IJ8ShyGHQ674wyHyBs7J2GbZeEZZlsAPOztW0iCXEB4RzdUP4HrmyYbhKxY9g+8juxhT91aMIH7bmFN+kOCrOO3W+4gaj73tGRyB6OBvMKledoK1ypL+COkXqj4GzFUcDsG88fFOD6+JMnVoUDNhyobovbxRsj6B5wApTkr6Bj6nAr5+er5RTTEBAwmUuIc/V7qFzK76NUmYGunr6r5bZHaxLld3Q==",
|
|
924
|
+
"sig": "ba59c2a57832972de1b73a75724f6636fd2cea73e6a9040e05bc3ea022a10a5260d866d94c49dd16408237ea62a65e9bfb39cf332099a23f560ff7ca463eed02"
|
|
925
|
+
}
|
|
926
|
+
},
|
|
927
|
+
"output": {
|
|
928
|
+
"renewedDevice": null
|
|
929
|
+
}
|
|
626
930
|
}
|
|
627
931
|
]
|
|
628
932
|
}
|