@serfab/cadre-core 1.16.0 → 1.17.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/README.md +1 -1
- package/dist/bootstrap-peer-store-file.js +1 -1
- package/dist/bootstrap-peer-store.d.ts +1 -1
- package/dist/bootstrap-peer-store.js +4 -4
- package/dist/cadre-invite-protocol.d.ts +55 -138
- package/dist/cadre-invite-protocol.js +94 -449
- package/dist/cadre-invite-protocol.js.map +1 -1
- package/dist/cadre-join-request.d.ts +15 -2
- package/dist/cadre-join-request.js +22 -3
- package/dist/cadre-join-request.js.map +1 -1
- package/dist/cadre-node.d.ts +530 -247
- package/dist/cadre-node.js +1046 -525
- package/dist/cadre-node.js.map +1 -1
- package/dist/control-authorization.d.ts +36 -14
- package/dist/control-authorization.js +20 -3
- package/dist/control-authorization.js.map +1 -1
- package/dist/control-cohort.d.ts +22 -23
- package/dist/control-cohort.js +24 -24
- package/dist/control-cohort.js.map +1 -1
- package/dist/control-database.d.ts +416 -136
- package/dist/control-database.js +757 -378
- package/dist/control-database.js.map +1 -1
- package/dist/control-protocol-guard.d.ts +5 -4
- package/dist/control-protocol-guard.js +6 -3
- package/dist/control-protocol-guard.js.map +1 -1
- package/dist/control-schema-version.d.ts +80 -0
- package/dist/control-schema-version.js +203 -0
- package/dist/control-schema-version.js.map +1 -0
- package/dist/control-schema.d.ts +45 -5
- package/dist/control-schema.js +254 -70
- package/dist/control-schema.js.map +1 -1
- package/dist/control-write-retry.d.ts +5 -5
- package/dist/control-write-retry.js +10 -8
- package/dist/control-write-retry.js.map +1 -1
- package/dist/delegate-admission.d.ts +13 -0
- package/dist/delegate-admission.js +27 -0
- package/dist/delegate-admission.js.map +1 -1
- package/dist/ed25519-key.d.ts +10 -7
- package/dist/ed25519-key.js +23 -20
- package/dist/ed25519-key.js.map +1 -1
- package/dist/edge-node/config.d.ts +122 -0
- package/dist/edge-node/config.js +199 -0
- package/dist/edge-node/config.js.map +1 -0
- package/dist/edge-node/dissolve.d.ts +95 -0
- package/dist/edge-node/dissolve.js +184 -0
- package/dist/edge-node/dissolve.js.map +1 -0
- package/dist/edge-node/edge-join.d.ts +44 -0
- package/dist/edge-node/edge-join.js +227 -0
- package/dist/edge-node/edge-join.js.map +1 -0
- package/dist/edge-node/edge-node.d.ts +295 -0
- package/dist/edge-node/edge-node.js +704 -0
- package/dist/edge-node/edge-node.js.map +1 -0
- package/dist/edge-node/index.d.ts +18 -0
- package/dist/edge-node/index.js +18 -0
- package/dist/edge-node/index.js.map +1 -0
- package/dist/edge-node/join-credential.d.ts +11 -0
- package/dist/edge-node/join-credential.js +11 -0
- package/dist/edge-node/join-credential.js.map +1 -0
- package/dist/edge-node/lazy-raw-storage.d.ts +40 -0
- package/dist/edge-node/lazy-raw-storage.js +134 -0
- package/dist/edge-node/lazy-raw-storage.js.map +1 -0
- package/dist/edge-node/node-local.d.ts +60 -0
- package/dist/edge-node/node-local.js +58 -0
- package/dist/edge-node/node-local.js.map +1 -0
- package/dist/edge-node/options.d.ts +83 -0
- package/dist/edge-node/options.js +110 -0
- package/dist/edge-node/options.js.map +1 -0
- package/dist/edge-node/owner-grant.d.ts +41 -0
- package/dist/edge-node/owner-grant.js +38 -0
- package/dist/edge-node/owner-grant.js.map +1 -0
- package/dist/edge-node/platform.d.ts +140 -0
- package/dist/edge-node/platform.js +12 -0
- package/dist/edge-node/platform.js.map +1 -0
- package/dist/edge-node/strand-move.d.ts +115 -0
- package/dist/edge-node/strand-move.js +223 -0
- package/dist/edge-node/strand-move.js.map +1 -0
- package/dist/edge-node/strands.d.ts +28 -0
- package/dist/edge-node/strands.js +60 -0
- package/dist/edge-node/strands.js.map +1 -0
- package/dist/index.d.ts +16 -7
- package/dist/index.js +26 -7
- package/dist/index.js.map +1 -1
- package/dist/joined-strand-store.d.ts +3 -3
- package/dist/joined-strand-store.js +3 -3
- package/dist/joined-strand-store.js.map +1 -1
- package/dist/key-store.d.ts +9 -4
- package/dist/key-store.js +9 -4
- package/dist/key-store.js.map +1 -1
- package/dist/machine-removal.d.ts +19 -39
- package/dist/machine-removal.js +24 -98
- package/dist/machine-removal.js.map +1 -1
- package/dist/member-redemption.d.ts +250 -0
- package/dist/member-redemption.js +485 -0
- package/dist/member-redemption.js.map +1 -0
- package/dist/owner-anchor-sync.d.ts +4 -4
- package/dist/owner-anchor-sync.js +16 -15
- package/dist/owner-anchor-sync.js.map +1 -1
- package/dist/owner-grant-protocol.d.ts +247 -0
- package/dist/owner-grant-protocol.js +330 -0
- package/dist/owner-grant-protocol.js.map +1 -0
- package/dist/owner-key-export.d.ts +70 -0
- package/dist/owner-key-export.js +279 -0
- package/dist/owner-key-export.js.map +1 -0
- package/dist/owner-key-flows.d.ts +55 -0
- package/dist/owner-key-flows.js +60 -0
- package/dist/owner-key-flows.js.map +1 -0
- package/dist/owner-key-store.d.ts +31 -0
- package/dist/owner-key-store.js +81 -0
- package/dist/owner-key-store.js.map +1 -0
- package/dist/owner-keys.d.ts +78 -0
- package/dist/owner-keys.js +133 -0
- package/dist/owner-keys.js.map +1 -0
- package/dist/owner-signer.d.ts +54 -0
- package/dist/owner-signer.js +51 -0
- package/dist/owner-signer.js.map +1 -0
- package/dist/peer-authorization.d.ts +83 -18
- package/dist/peer-authorization.js +123 -53
- package/dist/peer-authorization.js.map +1 -1
- package/dist/provider/rent-node.js +1 -1
- package/dist/provider/rent-node.js.map +1 -1
- package/dist/seed-bootstrap.d.ts +131 -109
- package/dist/seed-bootstrap.js +260 -248
- package/dist/seed-bootstrap.js.map +1 -1
- package/dist/seed-trust-policy.js +4 -4
- package/dist/strand-addr-protocol.d.ts +4 -1
- package/dist/strand-addr-protocol.js +8 -2
- package/dist/strand-addr-protocol.js.map +1 -1
- package/dist/strand-cohort.d.ts +3 -0
- package/dist/strand-cohort.js.map +1 -1
- package/dist/strand-database.d.ts +1 -0
- package/dist/strand-database.js +3 -0
- package/dist/strand-database.js.map +1 -1
- package/dist/strand-first-sync-gate.d.ts +4 -3
- package/dist/strand-first-sync-gate.js +4 -3
- package/dist/strand-first-sync-gate.js.map +1 -1
- package/dist/strand-instance-manager.d.ts +1 -1
- package/dist/strand-membership-writer.d.ts +13 -2
- package/dist/strand-membership-writer.js +52 -16
- package/dist/strand-membership-writer.js.map +1 -1
- package/dist/strand-move.d.ts +6 -5
- package/dist/strand-move.js +31 -30
- package/dist/strand-move.js.map +1 -1
- package/dist/strand-peer-redial.js +2 -3
- package/dist/strand-peer-redial.js.map +1 -1
- package/dist/types.d.ts +286 -103
- package/dist/types.js.map +1 -1
- package/package.json +11 -2
- package/src/bootstrap-peer-store-file.ts +44 -44
- package/src/bootstrap-peer-store.ts +214 -214
- package/src/cadre-invite-protocol.ts +652 -1052
- package/src/cadre-join-request.ts +24 -3
- package/src/cadre-node.ts +1166 -541
- package/src/control-authorization.ts +47 -14
- package/src/control-cohort.ts +127 -128
- package/src/control-database.ts +940 -456
- package/src/control-protocol-guard.ts +7 -4
- package/src/control-schema-version.ts +251 -0
- package/src/control-schema.ts +265 -70
- package/src/control-write-retry.ts +569 -567
- package/src/delegate-admission.ts +37 -0
- package/src/ed25519-key.ts +174 -171
- package/src/edge-node/config.ts +277 -0
- package/src/edge-node/dissolve.ts +251 -0
- package/src/edge-node/edge-join.ts +319 -0
- package/src/edge-node/edge-node.ts +1045 -0
- package/src/edge-node/index.ts +59 -0
- package/src/edge-node/join-credential.ts +16 -0
- package/src/edge-node/lazy-raw-storage.ts +178 -0
- package/src/edge-node/node-local.ts +84 -0
- package/src/edge-node/options.ts +179 -0
- package/src/edge-node/owner-grant.ts +63 -0
- package/src/edge-node/platform.ts +151 -0
- package/src/edge-node/strand-move.ts +270 -0
- package/src/edge-node/strands.ts +72 -0
- package/src/index.ts +886 -789
- package/src/joined-strand-store.ts +4 -4
- package/src/key-store.ts +10 -4
- package/src/machine-removal.ts +26 -111
- package/src/member-redemption.ts +642 -0
- package/src/owner-anchor-sync.ts +19 -18
- package/src/owner-grant-protocol.ts +532 -0
- package/src/owner-key-export.ts +348 -0
- package/src/owner-key-flows.ts +109 -0
- package/src/owner-key-store.ts +85 -0
- package/src/owner-keys.ts +178 -0
- package/src/owner-signer.ts +94 -0
- package/src/peer-authorization.ts +558 -488
- package/src/provider/rent-node.ts +1 -1
- package/src/seed-bootstrap.ts +301 -312
- package/src/seed-trust-policy.ts +4 -4
- package/src/strand-addr-protocol.ts +547 -538
- package/src/strand-cohort.ts +4 -0
- package/src/strand-database.ts +3 -0
- package/src/strand-first-sync-gate.ts +4 -3
- package/src/strand-instance-manager.ts +1 -1
- package/src/strand-membership-writer.ts +61 -16
- package/src/strand-move.ts +45 -36
- package/src/strand-peer-redial.ts +2 -3
- package/src/types.ts +304 -103
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/** Version 1 of the plain export. */
|
|
2
|
+
export declare const OWNER_KEY_EXPORT_PREFIX = "sereus-owner-key:1.";
|
|
3
|
+
/** Version 1 of the passphrase-locked export. */
|
|
4
|
+
export declare const LOCKED_OWNER_KEY_EXPORT_PREFIX = "sereus-owner-key-locked:1.";
|
|
5
|
+
/** Longest label an owner key carries, the user's name for it ("phone", "paper backup"): in an export and on its `OwnerKey` row (the control schema's `LabelLength` check). */
|
|
6
|
+
export declare const OWNER_KEY_MAX_LABEL = 64;
|
|
7
|
+
export type OwnerKeyExportErrorCode =
|
|
8
|
+
/** Owner-key text whose body does not decode, or a version this build does not read. */
|
|
9
|
+
'malformed'
|
|
10
|
+
/** The passphrase does not open a locked export, or the locked text was altered. */
|
|
11
|
+
| 'wrong-passphrase'
|
|
12
|
+
/** A locked export given with no passphrase (`importOwnerKeyInto` in `owner-key-flows.ts`). */
|
|
13
|
+
| 'passphrase-required'
|
|
14
|
+
/** Not owner-key text; the message names an invitation, an owner grant, a join request or a join code. */
|
|
15
|
+
| 'not-an-owner-key'
|
|
16
|
+
/** A locked export asks for more scrypt work than {@link MAX_SCRYPT_N} and {@link MAX_SCRYPT_R_TIMES_P} allow. */
|
|
17
|
+
| 'cost-too-high';
|
|
18
|
+
/** Every refusal from this module. The message is for the user and never includes key material. */
|
|
19
|
+
export declare class OwnerKeyExportError extends Error {
|
|
20
|
+
readonly code: OwnerKeyExportErrorCode;
|
|
21
|
+
constructor(code: OwnerKeyExportErrorCode, message: string, options?: {
|
|
22
|
+
cause?: unknown;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
/** What an export holds once read. */
|
|
26
|
+
export interface OwnerKeyExportContents {
|
|
27
|
+
/** The 32-byte Ed25519 seed, base64url: the input to `importOwnerKey`. */
|
|
28
|
+
privateKeyB64: string;
|
|
29
|
+
/** The matching public key, base64url: the `OwnerKey.Key` it signs for. */
|
|
30
|
+
publicKey: string;
|
|
31
|
+
label?: string;
|
|
32
|
+
}
|
|
33
|
+
/** {@link decodeOwnerKeyExport}'s answer: the key, or word that a passphrase is needed ({@link unlockOwnerKeyExport}). */
|
|
34
|
+
export type DecodedOwnerKeyExport = ({
|
|
35
|
+
locked: false;
|
|
36
|
+
} & OwnerKeyExportContents) | {
|
|
37
|
+
locked: true;
|
|
38
|
+
};
|
|
39
|
+
export interface OwnerKeyExportInput {
|
|
40
|
+
/** The 32-byte Ed25519 seed, base64url (`readOwnerPrivateKey`). */
|
|
41
|
+
privateKeyB64: string;
|
|
42
|
+
/** Trimmed; at most {@link OWNER_KEY_MAX_LABEL} characters; a blank one is left out. */
|
|
43
|
+
label?: string;
|
|
44
|
+
/** Locks the export when present; must not be empty. */
|
|
45
|
+
passphrase?: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Encode an owner key as text, locked under `passphrase` when one is given.
|
|
49
|
+
*
|
|
50
|
+
* @throws {OwnerKeyExportError} `malformed` for a key that is not a 32-byte seed, an over-long
|
|
51
|
+
* label or an empty passphrase.
|
|
52
|
+
*/
|
|
53
|
+
export declare function encodeOwnerKeyExport(input: OwnerKeyExportInput): Promise<string>;
|
|
54
|
+
/**
|
|
55
|
+
* Read owner-key text without a passphrase: a plain export's key, or `{ locked: true }` for a
|
|
56
|
+
* locked one whose envelope is well formed and whose scrypt cost is within the cap, so an app
|
|
57
|
+
* refuses a hostile file before asking for the passphrase.
|
|
58
|
+
*
|
|
59
|
+
* @throws {OwnerKeyExportError} `not-an-owner-key` (naming an invitation, an owner grant, a join
|
|
60
|
+
* request or a join code), `malformed` or `cost-too-high`.
|
|
61
|
+
*/
|
|
62
|
+
export declare function decodeOwnerKeyExport(text: string): DecodedOwnerKeyExport;
|
|
63
|
+
/**
|
|
64
|
+
* Read owner-key text with its passphrase. A plain export is read as {@link decodeOwnerKeyExport}
|
|
65
|
+
* reads it, the passphrase unused.
|
|
66
|
+
*
|
|
67
|
+
* @throws {OwnerKeyExportError} as {@link decodeOwnerKeyExport}, and `wrong-passphrase` when the
|
|
68
|
+
* passphrase does not open it or the sealed part, nonce or scrypt parameters were altered.
|
|
69
|
+
*/
|
|
70
|
+
export declare function unlockOwnerKeyExport(text: string, passphrase: string): Promise<OwnerKeyExportContents>;
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owner-key export and import: the text (or QR code) a user keeps as a backup of an owner key or
|
|
3
|
+
* carries to another device (`docs/architecture.md` → Owner Keys → Exporting and importing an
|
|
4
|
+
* owner key). Whoever holds a plain export is an owner.
|
|
5
|
+
*
|
|
6
|
+
* - Plain: {@link OWNER_KEY_EXPORT_PREFIX}, then base64url of the canonical JSON
|
|
7
|
+
* `{ key, label?, v: 1 }`, `key` being the 32-byte Ed25519 seed in base64url.
|
|
8
|
+
* - Locked: {@link LOCKED_OWNER_KEY_EXPORT_PREFIX}, then base64url of the canonical JSON
|
|
9
|
+
* `{ kdf: { N, name: 'scrypt', p, r, salt }, nonce, sealed, v: 1 }`, `sealed` being the plain
|
|
10
|
+
* JSON under XChaCha20-Poly1305 with the 32-byte key scrypt derives from the passphrase. The
|
|
11
|
+
* scrypt parameters ride in the text, so a later default still reads an old export.
|
|
12
|
+
*
|
|
13
|
+
* No error carries key material. `@noble/hashes` and `@noble/ciphers` are pure JS and random
|
|
14
|
+
* bytes come from `crypto.getRandomValues`, so this loads on React Native and in the browser.
|
|
15
|
+
*/
|
|
16
|
+
import { xchacha20poly1305 } from '@noble/ciphers/chacha.js';
|
|
17
|
+
import { scryptAsync } from '@noble/hashes/scrypt.js';
|
|
18
|
+
import { randomBytes } from '@noble/hashes/utils.js';
|
|
19
|
+
import { fromString as uint8ArrayFromString, toString as uint8ArrayToString } from 'uint8arrays';
|
|
20
|
+
import { canonicalJson } from './canonical-json.js';
|
|
21
|
+
import { CADRE_INVITATION_PREFIX, CADRE_JOIN_REQUEST_PREFIX, OWNER_GRANT_PREFIX, OWNER_KEY_EXPORT_STEM } from './cadre-join-request.js';
|
|
22
|
+
import { NODE_CLAIM_PAYLOAD_PREFIX } from './node-claim-payload.js';
|
|
23
|
+
import { ed25519PublicKeyFromPrivate, requireEd25519PrivateKeyB64 } from './ed25519-key.js';
|
|
24
|
+
/** Version 1 of the plain export. */
|
|
25
|
+
export const OWNER_KEY_EXPORT_PREFIX = `${OWNER_KEY_EXPORT_STEM}:1.`;
|
|
26
|
+
/** Version 1 of the passphrase-locked export. */
|
|
27
|
+
export const LOCKED_OWNER_KEY_EXPORT_PREFIX = `${OWNER_KEY_EXPORT_STEM}-locked:1.`;
|
|
28
|
+
/** Longest label an owner key carries, the user's name for it ("phone", "paper backup"): in an export and on its `OwnerKey` row (the control schema's `LabelLength` check). */
|
|
29
|
+
export const OWNER_KEY_MAX_LABEL = 64;
|
|
30
|
+
/**
|
|
31
|
+
* The encoder's scrypt cost: about 140 ms and 64 MiB on Node 24 (x64, measured with
|
|
32
|
+
* `scryptAsync`).
|
|
33
|
+
* NOTE: unmeasured on Hermes; if a device run shows locking or unlocking is slow, lower N for the
|
|
34
|
+
* locked form or move the KDF to native.
|
|
35
|
+
*/
|
|
36
|
+
const DEFAULT_SCRYPT_COST = { N: 2 ** 16, r: 8, p: 1 };
|
|
37
|
+
/**
|
|
38
|
+
* The most scrypt work the decoder runs, checked before deriving so a hostile file cannot keep a
|
|
39
|
+
* device busy for minutes: N at most 2^20 and r·p at most 8, sixteen times the default's work.
|
|
40
|
+
* At the cap scrypt takes about 2.5 s and 1 GiB on Node 24.
|
|
41
|
+
* NOTE: unmeasured on Hermes; if a device run shows the cap runs for minutes or exhausts memory,
|
|
42
|
+
* lower these two.
|
|
43
|
+
*/
|
|
44
|
+
const MAX_SCRYPT_N = 2 ** 20;
|
|
45
|
+
const MAX_SCRYPT_R_TIMES_P = 8;
|
|
46
|
+
/** noble's `maxmem` for {@link MAX_SCRYPT_N}: its default (1 GiB + 1 KiB) is below what the cap itself needs, `128·r·(N + p + 1)` bytes. */
|
|
47
|
+
const SCRYPT_MAXMEM = 128 * MAX_SCRYPT_R_TIMES_P * (MAX_SCRYPT_N + MAX_SCRYPT_R_TIMES_P + 1);
|
|
48
|
+
const SCRYPT_SALT_BYTES = 16;
|
|
49
|
+
const MAX_SCRYPT_SALT_BYTES = 64;
|
|
50
|
+
const XCHACHA_NONCE_BYTES = 24;
|
|
51
|
+
const POLY1305_TAG_BYTES = 16;
|
|
52
|
+
/** A sealed plain export is under 200 bytes; this bounds what a hostile file makes the decoder decrypt. */
|
|
53
|
+
const MAX_SEALED_BYTES = 1024;
|
|
54
|
+
/** Every refusal from this module. The message is for the user and never includes key material. */
|
|
55
|
+
export class OwnerKeyExportError extends Error {
|
|
56
|
+
constructor(code, message, options) {
|
|
57
|
+
super(message, options);
|
|
58
|
+
this.name = 'OwnerKeyExportError';
|
|
59
|
+
this.code = code;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Encode an owner key as text, locked under `passphrase` when one is given.
|
|
64
|
+
*
|
|
65
|
+
* @throws {OwnerKeyExportError} `malformed` for a key that is not a 32-byte seed, an over-long
|
|
66
|
+
* label or an empty passphrase.
|
|
67
|
+
*/
|
|
68
|
+
export async function encodeOwnerKeyExport(input) {
|
|
69
|
+
const plainJson = plainExportJson(input.privateKeyB64, input.label);
|
|
70
|
+
if (input.passphrase === undefined) {
|
|
71
|
+
return withPrefix(OWNER_KEY_EXPORT_PREFIX, plainJson);
|
|
72
|
+
}
|
|
73
|
+
return withPrefix(LOCKED_OWNER_KEY_EXPORT_PREFIX, await lockedExportJson(plainJson, input.passphrase));
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Read owner-key text without a passphrase: a plain export's key, or `{ locked: true }` for a
|
|
77
|
+
* locked one whose envelope is well formed and whose scrypt cost is within the cap, so an app
|
|
78
|
+
* refuses a hostile file before asking for the passphrase.
|
|
79
|
+
*
|
|
80
|
+
* @throws {OwnerKeyExportError} `not-an-owner-key` (naming an invitation, an owner grant, a join
|
|
81
|
+
* request or a join code), `malformed` or `cost-too-high`.
|
|
82
|
+
*/
|
|
83
|
+
export function decodeOwnerKeyExport(text) {
|
|
84
|
+
const trimmed = text.trim();
|
|
85
|
+
if (exportPrefix(trimmed) === LOCKED_OWNER_KEY_EXPORT_PREFIX) {
|
|
86
|
+
lockedEnvelope(decodeBody(trimmed, LOCKED_OWNER_KEY_EXPORT_PREFIX));
|
|
87
|
+
return { locked: true };
|
|
88
|
+
}
|
|
89
|
+
return { locked: false, ...exportContents(decodeBody(trimmed, OWNER_KEY_EXPORT_PREFIX)) };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Read owner-key text with its passphrase. A plain export is read as {@link decodeOwnerKeyExport}
|
|
93
|
+
* reads it, the passphrase unused.
|
|
94
|
+
*
|
|
95
|
+
* @throws {OwnerKeyExportError} as {@link decodeOwnerKeyExport}, and `wrong-passphrase` when the
|
|
96
|
+
* passphrase does not open it or the sealed part, nonce or scrypt parameters were altered.
|
|
97
|
+
*/
|
|
98
|
+
export async function unlockOwnerKeyExport(text, passphrase) {
|
|
99
|
+
const trimmed = text.trim();
|
|
100
|
+
const prefix = exportPrefix(trimmed);
|
|
101
|
+
if (prefix === OWNER_KEY_EXPORT_PREFIX) {
|
|
102
|
+
return exportContents(decodeBody(trimmed, prefix));
|
|
103
|
+
}
|
|
104
|
+
const envelope = lockedEnvelope(decodeBody(trimmed, prefix));
|
|
105
|
+
const key = await deriveKey(passphrase, envelope, envelope.salt);
|
|
106
|
+
let plain;
|
|
107
|
+
try {
|
|
108
|
+
plain = xchacha20poly1305(key, envelope.nonce).decrypt(envelope.sealed);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
throw new OwnerKeyExportError('wrong-passphrase', 'The passphrase does not unlock this owner key, or the text was altered', { cause: error });
|
|
112
|
+
}
|
|
113
|
+
return exportContents(parseJsonObject(new TextDecoder().decode(plain)));
|
|
114
|
+
}
|
|
115
|
+
// ── Encoding ─────────────────────────────────────────────────────────────────
|
|
116
|
+
function plainExportJson(privateKeyB64, label) {
|
|
117
|
+
const key = requireKey(privateKeyB64);
|
|
118
|
+
const trimmedLabel = label?.trim();
|
|
119
|
+
return canonicalJson({ key, label: trimmedLabel ? requireLabel(trimmedLabel) : undefined, v: 1 });
|
|
120
|
+
}
|
|
121
|
+
async function lockedExportJson(plainJson, passphrase) {
|
|
122
|
+
if (passphrase.length === 0) {
|
|
123
|
+
throw new OwnerKeyExportError('malformed', 'A passphrase must not be empty; leave it out for an unlocked export');
|
|
124
|
+
}
|
|
125
|
+
const salt = randomBytes(SCRYPT_SALT_BYTES);
|
|
126
|
+
const nonce = randomBytes(XCHACHA_NONCE_BYTES);
|
|
127
|
+
const key = await deriveKey(passphrase, DEFAULT_SCRYPT_COST, salt);
|
|
128
|
+
const sealed = xchacha20poly1305(key, nonce).encrypt(new TextEncoder().encode(plainJson));
|
|
129
|
+
return canonicalJson({
|
|
130
|
+
kdf: { ...DEFAULT_SCRYPT_COST, name: 'scrypt', salt: toBase64url(salt) },
|
|
131
|
+
nonce: toBase64url(nonce),
|
|
132
|
+
sealed: toBase64url(sealed),
|
|
133
|
+
v: 1,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
function withPrefix(prefix, json) {
|
|
137
|
+
return `${prefix}${toBase64url(new TextEncoder().encode(json))}`;
|
|
138
|
+
}
|
|
139
|
+
/** scrypt's async form yields to the event loop every few milliseconds, so a phone's UI keeps drawing while it runs. */
|
|
140
|
+
function deriveKey(passphrase, cost, salt) {
|
|
141
|
+
// NFKC, so one passphrase typed on keyboards that compose its characters differently derives one key.
|
|
142
|
+
const password = new TextEncoder().encode(passphrase.normalize('NFKC'));
|
|
143
|
+
return scryptAsync(password, salt, { N: cost.N, r: cost.r, p: cost.p, dkLen: 32, maxmem: SCRYPT_MAXMEM });
|
|
144
|
+
}
|
|
145
|
+
// ── Decoding ─────────────────────────────────────────────────────────────────
|
|
146
|
+
/** Which of the two prefixes `text` starts with; refuses, by name where it can, anything else. */
|
|
147
|
+
function exportPrefix(text) {
|
|
148
|
+
if (text.startsWith(LOCKED_OWNER_KEY_EXPORT_PREFIX))
|
|
149
|
+
return LOCKED_OWNER_KEY_EXPORT_PREFIX;
|
|
150
|
+
if (text.startsWith(OWNER_KEY_EXPORT_PREFIX))
|
|
151
|
+
return OWNER_KEY_EXPORT_PREFIX;
|
|
152
|
+
if (text.startsWith(OWNER_KEY_EXPORT_STEM)) {
|
|
153
|
+
throw new OwnerKeyExportError('malformed', 'This owner key was exported by a newer version of the app; update this app to import it');
|
|
154
|
+
}
|
|
155
|
+
throw new OwnerKeyExportError('not-an-owner-key', notAnOwnerKeyMessage(text));
|
|
156
|
+
}
|
|
157
|
+
/** No prefix of the text is echoed: whatever was pasted may be a key in some other form. */
|
|
158
|
+
function notAnOwnerKeyMessage(text) {
|
|
159
|
+
if (text.startsWith(CADRE_INVITATION_PREFIX)) {
|
|
160
|
+
return 'This is a cadre invitation, not an owner key; paste it where the app asks for an invitation';
|
|
161
|
+
}
|
|
162
|
+
if (text.startsWith(OWNER_GRANT_PREFIX)) {
|
|
163
|
+
return 'This is an owner grant, not an owner key; paste it where the app asks for an owner grant';
|
|
164
|
+
}
|
|
165
|
+
if (text.startsWith(CADRE_JOIN_REQUEST_PREFIX)) {
|
|
166
|
+
return 'This is a join request, not an owner key; paste it into the app that manages your cadre';
|
|
167
|
+
}
|
|
168
|
+
if (text.startsWith(NODE_CLAIM_PAYLOAD_PREFIX)) {
|
|
169
|
+
return 'This is a machine\'s join code, not an owner key; scan it where the app adds a machine';
|
|
170
|
+
}
|
|
171
|
+
return `An owner key must start with "${OWNER_KEY_EXPORT_PREFIX}" or "${LOCKED_OWNER_KEY_EXPORT_PREFIX}"`;
|
|
172
|
+
}
|
|
173
|
+
function decodeBody(text, prefix) {
|
|
174
|
+
let json;
|
|
175
|
+
try {
|
|
176
|
+
json = new TextDecoder().decode(uint8ArrayFromString(text.slice(prefix.length), 'base64url'));
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
throw new OwnerKeyExportError('malformed', 'An owner key export must carry base64url after its prefix', { cause: error });
|
|
180
|
+
}
|
|
181
|
+
return parseJsonObject(json);
|
|
182
|
+
}
|
|
183
|
+
/** A version-1 JSON object, the shape of both the export body and a locked export's sealed plaintext. */
|
|
184
|
+
function parseJsonObject(json) {
|
|
185
|
+
let parsed;
|
|
186
|
+
try {
|
|
187
|
+
parsed = JSON.parse(json);
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
// No cause: JSON.parse's message can quote the input, which may hold the key.
|
|
191
|
+
throw new OwnerKeyExportError('malformed', 'An owner key export must carry JSON after its prefix');
|
|
192
|
+
}
|
|
193
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
194
|
+
throw new OwnerKeyExportError('malformed', 'An owner key export must decode to a JSON object');
|
|
195
|
+
}
|
|
196
|
+
const record = parsed;
|
|
197
|
+
if (record.v !== 1) {
|
|
198
|
+
throw new OwnerKeyExportError('malformed', `An owner key export must be version 1 (this one says ${JSON.stringify(record.v)})`);
|
|
199
|
+
}
|
|
200
|
+
return record;
|
|
201
|
+
}
|
|
202
|
+
function exportContents(record) {
|
|
203
|
+
const { key, label } = record;
|
|
204
|
+
if (typeof key !== 'string') {
|
|
205
|
+
throw new OwnerKeyExportError('malformed', 'An owner key export must carry its key');
|
|
206
|
+
}
|
|
207
|
+
if (label !== undefined && (typeof label !== 'string' || label.trim().length === 0)) {
|
|
208
|
+
throw new OwnerKeyExportError('malformed', 'An owner key export\'s label must be non-empty text');
|
|
209
|
+
}
|
|
210
|
+
const privateKeyB64 = requireKey(key);
|
|
211
|
+
return {
|
|
212
|
+
privateKeyB64,
|
|
213
|
+
publicKey: ed25519PublicKeyFromPrivate(privateKeyB64),
|
|
214
|
+
...(label === undefined ? {} : { label: requireLabel(label.trim()) }),
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function lockedEnvelope(record) {
|
|
218
|
+
const { kdf, nonce, sealed } = record;
|
|
219
|
+
if (typeof kdf !== 'object' || kdf === null || Array.isArray(kdf)) {
|
|
220
|
+
throw new OwnerKeyExportError('malformed', 'A locked owner key must name its key derivation');
|
|
221
|
+
}
|
|
222
|
+
const { name, N, r, p, salt } = kdf;
|
|
223
|
+
if (name !== 'scrypt') {
|
|
224
|
+
throw new OwnerKeyExportError('malformed', `A locked owner key must use scrypt (this one names ${JSON.stringify(name)})`);
|
|
225
|
+
}
|
|
226
|
+
const cost = requireScryptCost(N, r, p);
|
|
227
|
+
return {
|
|
228
|
+
...cost,
|
|
229
|
+
salt: requireBytes(salt, 'salt', 1, MAX_SCRYPT_SALT_BYTES),
|
|
230
|
+
nonce: requireBytes(nonce, 'nonce', XCHACHA_NONCE_BYTES, XCHACHA_NONCE_BYTES),
|
|
231
|
+
sealed: requireBytes(sealed, 'sealed key', POLY1305_TAG_BYTES, MAX_SEALED_BYTES),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
function requireScryptCost(N, r, p) {
|
|
235
|
+
if (!isPositiveInteger(N) || N < 2 || (N & (N - 1)) !== 0 || !isPositiveInteger(r) || !isPositiveInteger(p)) {
|
|
236
|
+
throw new OwnerKeyExportError('malformed', 'A locked owner key\'s scrypt parameters must be positive integers, N a power of two');
|
|
237
|
+
}
|
|
238
|
+
if (N > MAX_SCRYPT_N || r * p > MAX_SCRYPT_R_TIMES_P) {
|
|
239
|
+
throw new OwnerKeyExportError('cost-too-high', `This locked owner key asks for more work to unlock than this app allows (scrypt N ${N}, r ${r}, p ${p})`);
|
|
240
|
+
}
|
|
241
|
+
return { N, r, p };
|
|
242
|
+
}
|
|
243
|
+
function isPositiveInteger(value) {
|
|
244
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value > 0;
|
|
245
|
+
}
|
|
246
|
+
function requireBytes(value, field, minBytes, maxBytes) {
|
|
247
|
+
let bytes;
|
|
248
|
+
if (typeof value === 'string') {
|
|
249
|
+
try {
|
|
250
|
+
bytes = uint8ArrayFromString(value, 'base64url');
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
throw new OwnerKeyExportError('malformed', `A locked owner key's ${field} must be base64url`, { cause: error });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (bytes === undefined || bytes.length < minBytes || bytes.length > maxBytes) {
|
|
257
|
+
throw new OwnerKeyExportError('malformed', `A locked owner key's ${field} must be ${minBytes === maxBytes ? minBytes : `${minBytes} to ${maxBytes}`} bytes of base64url`);
|
|
258
|
+
}
|
|
259
|
+
return bytes;
|
|
260
|
+
}
|
|
261
|
+
// ── Shared field rules ───────────────────────────────────────────────────────
|
|
262
|
+
function requireKey(privateKeyB64) {
|
|
263
|
+
try {
|
|
264
|
+
return requireEd25519PrivateKeyB64(privateKeyB64, 'owner key');
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
throw new OwnerKeyExportError('malformed', error instanceof Error ? error.message : 'The owner key must be a 32-byte Ed25519 private key', { cause: error });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function requireLabel(label) {
|
|
271
|
+
if (label.length > OWNER_KEY_MAX_LABEL) {
|
|
272
|
+
throw new OwnerKeyExportError('malformed', `An owner key's label must be at most ${OWNER_KEY_MAX_LABEL} characters (this one has ${label.length})`);
|
|
273
|
+
}
|
|
274
|
+
return label;
|
|
275
|
+
}
|
|
276
|
+
function toBase64url(bytes) {
|
|
277
|
+
return uint8ArrayToString(bytes, 'base64url');
|
|
278
|
+
}
|
|
279
|
+
//# sourceMappingURL=owner-key-export.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"owner-key-export.js","sourceRoot":"","sources":["../src/owner-key-export.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The owner-key actions an app's owner-key screen offers, built from the owner-key slot
|
|
3
|
+
* (`owner-key-store.ts`) and the export text (`owner-key-export.ts`): export the device's own key,
|
|
4
|
+
* and import an exported key into the device's owner slot (`docs/architecture.md` → Owner Keys →
|
|
5
|
+
* Exporting and importing an owner key). One copy for the phone kit and the web app.
|
|
6
|
+
*
|
|
7
|
+
* Over `CadreNode`'s public surface only; pure-JS crypto, so it loads on React Native and in the
|
|
8
|
+
* browser.
|
|
9
|
+
*/
|
|
10
|
+
import type { CadreNode } from './cadre-node.js';
|
|
11
|
+
import type { KeyStore } from './key-store.js';
|
|
12
|
+
/** How an owner key is exported. */
|
|
13
|
+
export interface OwnerKeyExportOptions {
|
|
14
|
+
/** The user's name for the key, carried in the export. Trimmed, at most 64 characters, blank meaning none. */
|
|
15
|
+
label?: string;
|
|
16
|
+
/** Locks the export; must not be empty. Absent: a plain export. */
|
|
17
|
+
passphrase?: string;
|
|
18
|
+
}
|
|
19
|
+
/** An owner key's export and the key it holds: {@link exportOwnerKey}'s answer. */
|
|
20
|
+
export interface ExportedOwnerKey {
|
|
21
|
+
/** The exported key (base64url). */
|
|
22
|
+
publicKey: string;
|
|
23
|
+
/** The private key as export text. */
|
|
24
|
+
exportText: string;
|
|
25
|
+
}
|
|
26
|
+
/** {@link importOwnerKeyInto}'s answer. */
|
|
27
|
+
export interface ImportedOwnerKey {
|
|
28
|
+
/** The imported key (base64url), now in the owner slot. */
|
|
29
|
+
publicKey: string;
|
|
30
|
+
/** The node now signs with it: the key is a live owner of the node's party. */
|
|
31
|
+
wired: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Export the key in `keyStore`'s owner slot, the device's own owner key, which is the key the
|
|
35
|
+
* kits' nodes sign with. The answer names the key, for a caller that shows the export under it.
|
|
36
|
+
*
|
|
37
|
+
* @throws when the slot is empty: a member device holds no owner key of its own to export.
|
|
38
|
+
* @throws {OwnerKeyExportError} `malformed` for an over-long label or an empty passphrase.
|
|
39
|
+
*/
|
|
40
|
+
export declare function exportOwnerKey(keyStore: KeyStore, options?: OwnerKeyExportOptions): Promise<ExportedOwnerKey>;
|
|
41
|
+
/**
|
|
42
|
+
* Import owner-key text into `keyStore`'s owner slot and, given a running node, wire the key when
|
|
43
|
+
* it is a live owner of that node's party. Importing the key the slot already holds changes
|
|
44
|
+
* nothing; a different one is refused.
|
|
45
|
+
*
|
|
46
|
+
* A node that already signs with another key keeps it, since a node takes one owner signer; the
|
|
47
|
+
* imported key is stored, `wired` is false, and the next start wires it. The kits' nodes sign
|
|
48
|
+
* only with the slot's key, so there the case arises only for an embedder that wired another.
|
|
49
|
+
*
|
|
50
|
+
* @throws {OwnerKeyExportError} `passphrase-required` for locked text with no passphrase,
|
|
51
|
+
* `wrong-passphrase`, or the decoder's refusals (`not-an-owner-key` names an invitation, join
|
|
52
|
+
* request or join code).
|
|
53
|
+
* @throws {OwnerKeySlotOccupiedError} when the slot holds a different key.
|
|
54
|
+
*/
|
|
55
|
+
export declare function importOwnerKeyInto(keyStore: KeyStore, node: CadreNode | null, text: string, passphrase?: string): Promise<ImportedOwnerKey>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { ed25519PublicKeyFromPrivate } from './ed25519-key.js';
|
|
2
|
+
import { decodeOwnerKeyExport, encodeOwnerKeyExport, OwnerKeyExportError, unlockOwnerKeyExport, } from './owner-key-export.js';
|
|
3
|
+
import { importOwnerKey, readOwnerPrivateKey } from './owner-key-store.js';
|
|
4
|
+
/**
|
|
5
|
+
* Export the key in `keyStore`'s owner slot, the device's own owner key, which is the key the
|
|
6
|
+
* kits' nodes sign with. The answer names the key, for a caller that shows the export under it.
|
|
7
|
+
*
|
|
8
|
+
* @throws when the slot is empty: a member device holds no owner key of its own to export.
|
|
9
|
+
* @throws {OwnerKeyExportError} `malformed` for an over-long label or an empty passphrase.
|
|
10
|
+
*/
|
|
11
|
+
export async function exportOwnerKey(keyStore, options = {}) {
|
|
12
|
+
const privateKeyB64 = await readOwnerPrivateKey(keyStore);
|
|
13
|
+
if (privateKeyB64 === undefined) {
|
|
14
|
+
throw new Error('This device holds no owner key of its own to export');
|
|
15
|
+
}
|
|
16
|
+
const exportText = await encodeOwnerKeyExport({ privateKeyB64, ...exportFields(options) });
|
|
17
|
+
return { publicKey: ed25519PublicKeyFromPrivate(privateKeyB64), exportText };
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Import owner-key text into `keyStore`'s owner slot and, given a running node, wire the key when
|
|
21
|
+
* it is a live owner of that node's party. Importing the key the slot already holds changes
|
|
22
|
+
* nothing; a different one is refused.
|
|
23
|
+
*
|
|
24
|
+
* A node that already signs with another key keeps it, since a node takes one owner signer; the
|
|
25
|
+
* imported key is stored, `wired` is false, and the next start wires it. The kits' nodes sign
|
|
26
|
+
* only with the slot's key, so there the case arises only for an embedder that wired another.
|
|
27
|
+
*
|
|
28
|
+
* @throws {OwnerKeyExportError} `passphrase-required` for locked text with no passphrase,
|
|
29
|
+
* `wrong-passphrase`, or the decoder's refusals (`not-an-owner-key` names an invitation, join
|
|
30
|
+
* request or join code).
|
|
31
|
+
* @throws {OwnerKeySlotOccupiedError} when the slot holds a different key.
|
|
32
|
+
*/
|
|
33
|
+
export async function importOwnerKeyInto(keyStore, node, text, passphrase) {
|
|
34
|
+
const contents = await readOwnerKeyExport(text, passphrase);
|
|
35
|
+
const signer = await importOwnerKey(keyStore, contents.privateKeyB64);
|
|
36
|
+
return { publicKey: signer.publicKey, wired: node !== null && (await wireIfOwner(node, signer)) };
|
|
37
|
+
}
|
|
38
|
+
async function readOwnerKeyExport(text, passphrase) {
|
|
39
|
+
const decoded = decodeOwnerKeyExport(text);
|
|
40
|
+
if (!decoded.locked)
|
|
41
|
+
return decoded;
|
|
42
|
+
if (passphrase === undefined) {
|
|
43
|
+
throw new OwnerKeyExportError('passphrase-required', 'This owner key is locked with a passphrase; enter it to import the key');
|
|
44
|
+
}
|
|
45
|
+
return await unlockOwnerKeyExport(text, passphrase);
|
|
46
|
+
}
|
|
47
|
+
/** `CadreNode.wireOwnKeyIfOwner(signer)`, unless the node already signs with another key, which it keeps. */
|
|
48
|
+
async function wireIfOwner(node, signer) {
|
|
49
|
+
const wired = node.getOwnerSigner();
|
|
50
|
+
if (wired !== null && wired.publicKey !== signer.publicKey)
|
|
51
|
+
return false;
|
|
52
|
+
return await node.wireOwnKeyIfOwner(signer);
|
|
53
|
+
}
|
|
54
|
+
function exportFields(options) {
|
|
55
|
+
return {
|
|
56
|
+
...(options.label !== undefined && { label: options.label }),
|
|
57
|
+
...(options.passphrase !== undefined && { passphrase: options.passphrase }),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=owner-key-flows.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"owner-key-flows.js","sourceRoot":"","sources":["../src/owner-key-flows.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,2BAA2B,EAAE,MAAM,kBAAkB,CAAC;AAE/D,OAAO,EACN,oBAAoB,EACpB,oBAAoB,EACpB,mBAAmB,EACnB,oBAAoB,GAEpB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AA2B3E;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,QAAkB,EAAE,UAAiC,EAAE;IAC3F,MAAM,aAAa,GAAG,MAAM,mBAAmB,CAAC,QAAQ,CAAC,CAAC;IAC1D,IAAI,aAAa,KAAK,SAAS,EAAE,CAAC;QACjC,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAC;IACxE,CAAC;IACD,MAAM,UAAU,GAAG,MAAM,oBAAoB,CAAC,EAAE,aAAa,EAAE,GAAG,YAAY,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAC3F,OAAO,EAAE,SAAS,EAAE,2BAA2B,CAAC,aAAa,CAAC,EAAE,UAAU,EAAE,CAAC;AAC9E,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACvC,QAAkB,EAClB,IAAsB,EACtB,IAAY,EACZ,UAAmB;IAEnB,MAAM,QAAQ,GAAG,MAAM,kBAAkB,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;IAC5D,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,QAAQ,EAAE,QAAQ,CAAC,aAAa,CAAC,CAAC;IACtE,OAAO,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,KAAK,EAAE,IAAI,KAAK,IAAI,IAAI,CAAC,MAAM,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,EAAE,CAAC;AACnG,CAAC;AAED,KAAK,UAAU,kBAAkB,CAAC,IAAY,EAAE,UAA8B;IAC7E,MAAM,OAAO,GAAG,oBAAoB,CAAC,IAAI,CAAC,CAAC;IAC3C,IAAI,CAAC,OAAO,CAAC,MAAM;QAAE,OAAO,OAAO,CAAC;IACpC,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;QAC9B,MAAM,IAAI,mBAAmB,CAAC,qBAAqB,EAAE,wEAAwE,CAAC,CAAC;IAChI,CAAC;IACD,OAAO,MAAM,oBAAoB,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;AACrD,CAAC;AAED,6GAA6G;AAC7G,KAAK,UAAU,WAAW,CAAC,IAAe,EAAE,MAAmB;IAC9D,MAAM,KAAK,GAAG,IAAI,CAAC,cAAc,EAAE,CAAC;IACpC,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,SAAS,KAAK,MAAM,CAAC,SAAS;QAAE,OAAO,KAAK,CAAC;IACzE,OAAO,MAAM,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC,CAAC;AAC7C,CAAC;AAED,SAAS,YAAY,CAAC,OAA8B;IACnD,OAAO;QACN,GAAG,CAAC,OAAO,CAAC,KAAK,KAAK,SAAS,IAAI,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC;QAC5D,GAAG,CAAC,OAAO,CAAC,UAAU,KAAK,SAAS,IAAI,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,CAAC;KAC3E,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type KeyId, type KeyStore } from './key-store.js';
|
|
2
|
+
import { type OwnerSigner } from './owner-signer.js';
|
|
3
|
+
/** A create or import found the owner slot already holding a key (a different one, for an import). Carries no key material. */
|
|
4
|
+
export declare class OwnerKeySlotOccupiedError extends Error {
|
|
5
|
+
readonly keyId: KeyId;
|
|
6
|
+
constructor(keyId: KeyId, message: string);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Generate a fresh owner key and keep it in the slot.
|
|
10
|
+
*
|
|
11
|
+
* @throws {OwnerKeySlotOccupiedError} when the slot holds anything, unreadable bytes included.
|
|
12
|
+
*/
|
|
13
|
+
export declare function createOwnerKey(keyStore: KeyStore, keyId?: KeyId): Promise<OwnerSigner>;
|
|
14
|
+
/**
|
|
15
|
+
* A signer over the key in the slot, or `undefined` when the slot is empty. Never generates one.
|
|
16
|
+
* A rejected `get` (e.g. `KeyStoreAccessError`) propagates, and bytes that are not an Ed25519
|
|
17
|
+
* private key throw: neither means the slot is empty.
|
|
18
|
+
*/
|
|
19
|
+
export declare function loadOwnerSigner(keyStore: KeyStore, keyId?: KeyId): Promise<OwnerSigner | undefined>;
|
|
20
|
+
/**
|
|
21
|
+
* Keep an imported owner key (its 32-byte Ed25519 seed, base64url) in the slot. Importing the key
|
|
22
|
+
* the slot already holds changes nothing.
|
|
23
|
+
*
|
|
24
|
+
* @throws {OwnerKeySlotOccupiedError} when the slot holds anything else, unreadable bytes included.
|
|
25
|
+
*/
|
|
26
|
+
export declare function importOwnerKey(keyStore: KeyStore, privateKeyB64: string, keyId?: KeyId): Promise<OwnerSigner>;
|
|
27
|
+
/**
|
|
28
|
+
* The slot's private key as a base64url Ed25519 seed, or `undefined` when the slot is empty. For
|
|
29
|
+
* exporting the key (`owner-key-export.ts`); everything else signs through {@link loadOwnerSigner}.
|
|
30
|
+
*/
|
|
31
|
+
export declare function readOwnerPrivateKey(keyStore: KeyStore, keyId?: KeyId): Promise<string | undefined>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The owner-key slot: an owner key kept in a {@link KeyStore} slot of its own, apart from the
|
|
3
|
+
* node's identity slot (`docs/architecture.md` → Owner Keys).
|
|
4
|
+
*
|
|
5
|
+
* Unlike the identity slot (`identity-key.ts`), an empty owner slot is never filled on read: an
|
|
6
|
+
* owner key exists because the user made or imported one. Nothing here overwrites a slot that
|
|
7
|
+
* holds anything, since the key in it is what the user's cadres list as an owner.
|
|
8
|
+
*
|
|
9
|
+
* The stored form is the identity slot's: the libp2p protobuf bytes of an Ed25519 private key.
|
|
10
|
+
* Dependency-free beyond `@libp2p/crypto`, so this module is safe in every entry graph.
|
|
11
|
+
*
|
|
12
|
+
* NOTE: each write checks the slot and then sets it, which a `KeyStore` cannot do atomically; two
|
|
13
|
+
* concurrent creates or imports into one slot can both pass the check and the later write wins.
|
|
14
|
+
* Callers run one owner-key action at a time; if one ever can race another, serialize them here.
|
|
15
|
+
*/
|
|
16
|
+
import { generateKeyPair, generateKeyPairFromSeed, privateKeyFromProtobuf, privateKeyToProtobuf } from '@libp2p/crypto/keys';
|
|
17
|
+
import { equals as uint8ArrayEquals, fromString as uint8ArrayFromString } from 'uint8arrays';
|
|
18
|
+
import { DEFAULT_OWNER_KEY_ID } from './key-store.js';
|
|
19
|
+
import { ed25519KeyPairFromLibp2p, requireEd25519PrivateKeyB64 } from './ed25519-key.js';
|
|
20
|
+
import { softwareOwnerSigner } from './owner-signer.js';
|
|
21
|
+
/** A create or import found the owner slot already holding a key (a different one, for an import). Carries no key material. */
|
|
22
|
+
export class OwnerKeySlotOccupiedError extends Error {
|
|
23
|
+
constructor(keyId, message) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = 'OwnerKeySlotOccupiedError';
|
|
26
|
+
this.keyId = keyId;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Generate a fresh owner key and keep it in the slot.
|
|
31
|
+
*
|
|
32
|
+
* @throws {OwnerKeySlotOccupiedError} when the slot holds anything, unreadable bytes included.
|
|
33
|
+
*/
|
|
34
|
+
export async function createOwnerKey(keyStore, keyId = DEFAULT_OWNER_KEY_ID) {
|
|
35
|
+
if (await keyStore.get(keyId) !== undefined) {
|
|
36
|
+
throw new OwnerKeySlotOccupiedError(keyId, `The owner-key slot "${keyId}" already holds a key`);
|
|
37
|
+
}
|
|
38
|
+
const generated = await generateKeyPair('Ed25519');
|
|
39
|
+
await keyStore.set(keyId, privateKeyToProtobuf(generated));
|
|
40
|
+
return softwareOwnerSigner(ed25519KeyPairFromLibp2p(generated).privateKeyB64);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A signer over the key in the slot, or `undefined` when the slot is empty. Never generates one.
|
|
44
|
+
* A rejected `get` (e.g. `KeyStoreAccessError`) propagates, and bytes that are not an Ed25519
|
|
45
|
+
* private key throw: neither means the slot is empty.
|
|
46
|
+
*/
|
|
47
|
+
export async function loadOwnerSigner(keyStore, keyId = DEFAULT_OWNER_KEY_ID) {
|
|
48
|
+
const privateKeyB64 = await readOwnerPrivateKey(keyStore, keyId);
|
|
49
|
+
return privateKeyB64 === undefined ? undefined : softwareOwnerSigner(privateKeyB64);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Keep an imported owner key (its 32-byte Ed25519 seed, base64url) in the slot. Importing the key
|
|
53
|
+
* the slot already holds changes nothing.
|
|
54
|
+
*
|
|
55
|
+
* @throws {OwnerKeySlotOccupiedError} when the slot holds anything else, unreadable bytes included.
|
|
56
|
+
*/
|
|
57
|
+
export async function importOwnerKey(keyStore, privateKeyB64, keyId = DEFAULT_OWNER_KEY_ID) {
|
|
58
|
+
const imported = requireEd25519PrivateKeyB64(privateKeyB64, 'owner private key');
|
|
59
|
+
// The stored form is deterministic for a given seed, so equal bytes mean the same key.
|
|
60
|
+
const importedBytes = privateKeyToProtobuf(await generateKeyPairFromSeed('Ed25519', uint8ArrayFromString(imported, 'base64url')));
|
|
61
|
+
const stored = await keyStore.get(keyId);
|
|
62
|
+
if (stored === undefined) {
|
|
63
|
+
await keyStore.set(keyId, importedBytes);
|
|
64
|
+
}
|
|
65
|
+
else if (!uint8ArrayEquals(stored, importedBytes)) {
|
|
66
|
+
throw new OwnerKeySlotOccupiedError(keyId, `The owner-key slot "${keyId}" already holds a different key`);
|
|
67
|
+
}
|
|
68
|
+
return softwareOwnerSigner(imported);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The slot's private key as a base64url Ed25519 seed, or `undefined` when the slot is empty. For
|
|
72
|
+
* exporting the key (`owner-key-export.ts`); everything else signs through {@link loadOwnerSigner}.
|
|
73
|
+
*/
|
|
74
|
+
export async function readOwnerPrivateKey(keyStore, keyId = DEFAULT_OWNER_KEY_ID) {
|
|
75
|
+
const bytes = await keyStore.get(keyId);
|
|
76
|
+
if (bytes === undefined)
|
|
77
|
+
return undefined;
|
|
78
|
+
// A format conversion of the owner key's own stored bytes, not a derivation from the node identity.
|
|
79
|
+
return ed25519KeyPairFromLibp2p(privateKeyFromProtobuf(bytes)).privateKeyB64;
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=owner-key-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"owner-key-store.js","sourceRoot":"","sources":["../src/owner-key-store.ts"],"names":[],"mappings":"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"}
|