ftown 0.20.8 → 0.21.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/dist/centrifugo-client.d.ts +15 -11
- package/dist/centrifugo-client.js +63 -70
- package/dist/centrifugo-client.js.map +1 -1
- package/dist/cli-client.d.ts +11 -1
- package/dist/cli-client.js +23 -3
- package/dist/cli-client.js.map +1 -1
- package/dist/command-rpc.js +4 -8
- package/dist/command-rpc.js.map +1 -1
- package/dist/identity-reset.d.ts +11 -0
- package/dist/identity-reset.js +192 -0
- package/dist/identity-reset.js.map +1 -0
- package/dist/index.js +276 -279
- package/dist/index.js.map +1 -1
- package/dist/ledger-pairing-client.d.ts +28 -0
- package/dist/ledger-pairing-client.js +152 -0
- package/dist/ledger-pairing-client.js.map +1 -0
- package/dist/ledger-relay.d.ts +124 -0
- package/dist/ledger-relay.js +453 -0
- package/dist/ledger-relay.js.map +1 -0
- package/dist/ledger-startup.d.ts +151 -0
- package/dist/ledger-startup.js +203 -0
- package/dist/ledger-startup.js.map +1 -0
- package/dist/relay-token.d.ts +39 -0
- package/dist/relay-token.js +130 -0
- package/dist/relay-token.js.map +1 -0
- package/dist/revocation-gating.d.ts +15 -0
- package/dist/revocation-gating.js +16 -0
- package/dist/revocation-gating.js.map +1 -0
- package/dist/revoked-restart.d.ts +21 -0
- package/dist/revoked-restart.js +55 -0
- package/dist/revoked-restart.js.map +1 -0
- package/dist/security/bridge/identity.d.ts +2 -47
- package/dist/security/bridge/identity.js +3 -3
- package/dist/security/bridge/identity.js.map +1 -1
- package/dist/security/bridge/ledger-identity.d.ts +48 -0
- package/dist/security/bridge/ledger-identity.js +342 -0
- package/dist/security/bridge/ledger-identity.js.map +1 -0
- package/dist/security/bridge/ledger-store.d.ts +84 -0
- package/dist/security/bridge/ledger-store.js +485 -0
- package/dist/security/bridge/ledger-store.js.map +1 -0
- package/dist/security/bridge/metadata.d.ts +2 -2
- package/dist/security/bridge/permissions.js +2 -2
- package/dist/security/bridge/permissions.js.map +1 -1
- package/dist/security/bridge/policy.d.ts +42 -21
- package/dist/security/bridge/policy.js +217 -135
- package/dist/security/bridge/policy.js.map +1 -1
- package/dist/security/bridge/security.d.ts +91 -28
- package/dist/security/bridge/security.js +274 -117
- package/dist/security/bridge/security.js.map +1 -1
- package/dist/security/crypto.js +1 -1
- package/dist/security/crypto.js.map +1 -1
- package/dist/security/ledger/channels.d.ts +5 -0
- package/dist/security/ledger/channels.js +21 -0
- package/dist/security/ledger/channels.js.map +1 -0
- package/dist/security/ledger/ledger.d.ts +61 -0
- package/dist/security/ledger/ledger.js +597 -0
- package/dist/security/ledger/ledger.js.map +1 -0
- package/dist/security/ledger/pairing.d.ts +81 -0
- package/dist/security/ledger/pairing.js +414 -0
- package/dist/security/ledger/pairing.js.map +1 -0
- package/dist/security/ledger/proof.d.ts +24 -0
- package/dist/security/ledger/proof.js +161 -0
- package/dist/security/ledger/proof.js.map +1 -0
- package/dist/security/ledger/protocol.d.ts +100 -0
- package/dist/security/ledger/protocol.js +16 -0
- package/dist/security/ledger/protocol.js.map +1 -0
- package/dist/security/ledger/testing.d.ts +27 -0
- package/dist/security/ledger/testing.js +70 -0
- package/dist/security/ledger/testing.js.map +1 -0
- package/dist/security/ledger/webauthn.d.ts +12 -0
- package/dist/security/ledger/webauthn.js +147 -0
- package/dist/security/ledger/webauthn.js.map +1 -0
- package/dist/security/protocol.d.ts +5 -10
- package/dist/security-cli.d.ts +5 -0
- package/dist/security-cli.js +28 -16
- package/dist/security-cli.js.map +1 -1
- package/dist/token-provider.d.ts +39 -0
- package/dist/token-provider.js +101 -0
- package/dist/token-provider.js.map +1 -0
- package/dist/types.d.ts +1 -1
- package/package.json +2 -2
- package/skills/ftown/references/sessions.md +4 -0
- package/dist/bridge-auth.d.ts +0 -25
- package/dist/bridge-auth.js +0 -56
- package/dist/bridge-auth.js.map +0 -1
- package/dist/pairing-client.d.ts +0 -60
- package/dist/pairing-client.js +0 -460
- package/dist/pairing-client.js.map +0 -1
- package/dist/rotating-token-refresher.d.ts +0 -42
- package/dist/rotating-token-refresher.js +0 -91
- package/dist/rotating-token-refresher.js.map +0 -1
- package/dist/security/account-crypto.d.ts +0 -33
- package/dist/security/account-crypto.js +0 -649
- package/dist/security/account-crypto.js.map +0 -1
- package/dist/security/account-protocol.d.ts +0 -228
- package/dist/security/account-protocol.js +0 -2
- package/dist/security/account-protocol.js.map +0 -1
- package/dist/security/bridge/account-client.d.ts +0 -7
- package/dist/security/bridge/account-client.js +0 -87
- package/dist/security/bridge/account-client.js.map +0 -1
- package/dist/security/bridge/account-state.d.ts +0 -105
- package/dist/security/bridge/account-state.js +0 -79
- package/dist/security/bridge/account-state.js.map +0 -1
- package/dist/security/bridge/pending-approval.d.ts +0 -21
- package/dist/security/bridge/pending-approval.js +0 -80
- package/dist/security/bridge/pending-approval.js.map +0 -1
- package/dist/security/bridge/pending-enrollment.d.ts +0 -33
- package/dist/security/bridge/pending-enrollment.js +0 -177
- package/dist/security/bridge/pending-enrollment.js.map +0 -1
- package/dist/security/pending-approval-crypto.d.ts +0 -29
- package/dist/security/pending-approval-crypto.js +0 -716
- package/dist/security/pending-approval-crypto.js.map +0 -1
- package/dist/security/pending-approval-protocol.d.ts +0 -329
- package/dist/security/pending-approval-protocol.js +0 -2
- package/dist/security/pending-approval-protocol.js.map +0 -1
- package/dist/security/security-nudge.d.ts +0 -31
- package/dist/security/security-nudge.js +0 -98
- package/dist/security/security-nudge.js.map +0 -1
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { EndpointPublic } from '../protocol.js';
|
|
2
|
+
export interface LedgerBridgeIdentity {
|
|
3
|
+
/** ECDH P-256 endpoint key pair for HPKE; the private key is non-extractable. */
|
|
4
|
+
keyPair: CryptoKeyPair;
|
|
5
|
+
/** Raw uncompressed ECDH point, base64url: BridgePairRequest.endpointPublicKey. */
|
|
6
|
+
endpointPublicKey: string;
|
|
7
|
+
/** base64url SHA-256 of the raw endpoint point: EndpointPublic.id. */
|
|
8
|
+
endpointId: string;
|
|
9
|
+
/** Raw uncompressed ECDSA point, base64url: BridgePairRequest/LedgerBridge.authPublicKey. */
|
|
10
|
+
authPublicKey: string;
|
|
11
|
+
/** Non-extractable ECDSA P-256 signing key for signAuthProof. */
|
|
12
|
+
authPrivateKey: CryptoKey;
|
|
13
|
+
/** The bound endpoint, or null before pairing. With `keyPair` it is the bridge's EndpointIdentity. */
|
|
14
|
+
endpoint: EndpointPublic | null;
|
|
15
|
+
/** endpoint.ownerId once bound, else null. */
|
|
16
|
+
accountId: string | null;
|
|
17
|
+
}
|
|
18
|
+
/** What is on disk, without importing keys. A corrupted file throws BridgeSecurityError('key_unavailable'). */
|
|
19
|
+
export type LedgerIdentityFileInfo = {
|
|
20
|
+
state: 'absent';
|
|
21
|
+
accountId: null;
|
|
22
|
+
} | {
|
|
23
|
+
state: 'ready';
|
|
24
|
+
accountId: string | null;
|
|
25
|
+
};
|
|
26
|
+
/** Reports the v2 identity file's state without importing keys. Throws BridgeSecurityError('key_unavailable') for a corrupted file. */
|
|
27
|
+
export declare function inspectIdentityFile(dataDir: string): Promise<LedgerIdentityFileInfo>;
|
|
28
|
+
/**
|
|
29
|
+
* True iff the data dir holds v1 ACCOUNT state: `refresh-token`, `security/account-enrollment.json`,
|
|
30
|
+
* `security/pending-approval-status.json` or `security/pending-approval-confirmation.json` exists, or `security/bridge-identity.json`
|
|
31
|
+
* has a readable endpoint owner that is not `solo:` (Solo's identity is never reported). An unreadable bridge-identity.json is not
|
|
32
|
+
* reported: its owner cannot be told apart from Solo's. Reads only; never modifies anything.
|
|
33
|
+
*/
|
|
34
|
+
export declare function hasLegacyAccountState(dataDir: string): boolean;
|
|
35
|
+
/** Loads the identity without creating one: null when absent. Throws BridgeSecurityError('key_unavailable') for a corrupted file. */
|
|
36
|
+
export declare function readLedgerIdentity(dataDir: string): Promise<LedgerBridgeIdentity | null>;
|
|
37
|
+
/**
|
|
38
|
+
* Loads the identity, creating fresh keys only when the file is absent. Throws BridgeSecurityError: key_unavailable for an unreadable
|
|
39
|
+
* or corrupted file (never replaced), storage_unavailable when the new file cannot be written.
|
|
40
|
+
*/
|
|
41
|
+
export declare function loadOrCreateLedgerIdentity(dataDir: string): Promise<LedgerBridgeIdentity>;
|
|
42
|
+
/**
|
|
43
|
+
* Binds the identity to `accountId` once: sets endpoint {version:1, id, ownerId: accountId, kind:'bridge', publicKey}. Idempotent for
|
|
44
|
+
* the same account. Throws BridgeSecurityError: wrong_context for a different (or malformed) accountId, key_unavailable when there is
|
|
45
|
+
* no valid identity to bind, storage_unavailable when the file cannot be written. Binds are serialized within this process only: the
|
|
46
|
+
* bridge's single-instance guarantee keeps a second process from binding concurrently.
|
|
47
|
+
*/
|
|
48
|
+
export declare function bindLedgerIdentity(dataDir: string, accountId: string): Promise<LedgerBridgeIdentity>;
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* OS-local keys of a ledger (v2) bridge: `<dataDir>/security/ledger-identity.json`, version 2.
|
|
3
|
+
* - An ECDH P-256 endpoint key (HPKE) and an ECDSA P-256 auth key (AuthProof), both stored as JWK, file mode 0600.
|
|
4
|
+
* - Before pairing the file has keys but no endpoint; bindLedgerIdentity sets {version:1, id, ownerId: accountId, kind:'bridge',
|
|
5
|
+
* publicKey} exactly once and never rebinds to another account.
|
|
6
|
+
* - Absence alone creates keys. A corrupted or unreadable file is never replaced: it throws BridgeSecurityError('key_unavailable').
|
|
7
|
+
* - Solo and --local keep their v1 `security/bridge-identity.json` (identity.ts); this module never reads, writes or deletes it, except
|
|
8
|
+
* for hasLegacyAccountState, which only reads its owner to tell a v1 ACCOUNT identity from a Solo one.
|
|
9
|
+
* - Writers are serialized within this process; across processes the bridge's single-instance guarantee is relied on (creation is
|
|
10
|
+
* still exclusive via link(2), but two processes binding at once could each believe they won).
|
|
11
|
+
*/
|
|
12
|
+
import { randomUUID } from 'node:crypto';
|
|
13
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
14
|
+
import { link, mkdir, open, readdir, rename, rm } from 'node:fs/promises';
|
|
15
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
16
|
+
import { z } from 'zod';
|
|
17
|
+
import { exportPublicKey, importPublicKey } from '../crypto.js';
|
|
18
|
+
import { generateAuthKey } from '../ledger/proof.js';
|
|
19
|
+
import { BridgeSecurityError } from './errors.js';
|
|
20
|
+
import { endpointSchema } from './metadata.js';
|
|
21
|
+
const FILE_NAME = 'ledger-identity.json';
|
|
22
|
+
const MAX_FILE_BYTES = 64 * 1024;
|
|
23
|
+
const ECDH = { name: 'ECDH', namedCurve: 'P-256' };
|
|
24
|
+
const ECDSA = { name: 'ECDSA', namedCurve: 'P-256' };
|
|
25
|
+
const SIGN = { name: 'ECDSA', hash: 'SHA-256' };
|
|
26
|
+
/** v1 account-mode state (never Solo's): any of these means the data dir still holds a v1 account bridge. */
|
|
27
|
+
const LEGACY_ACCOUNT_FILES = [
|
|
28
|
+
['refresh-token'], ['security', 'account-enrollment.json'], ['security', 'pending-approval-status.json'],
|
|
29
|
+
['security', 'pending-approval-confirmation.json'],
|
|
30
|
+
];
|
|
31
|
+
const TEMPORARY = /^\.[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.tmp$/;
|
|
32
|
+
const jwkSchema = z.strictObject({
|
|
33
|
+
kty: z.literal('EC'), crv: z.literal('P-256'), x: z.string().max(64), y: z.string().max(64), d: z.string().max(64),
|
|
34
|
+
key_ops: z.array(z.string().max(32)).max(8).optional(), ext: z.boolean().optional(),
|
|
35
|
+
});
|
|
36
|
+
const pointSchema = z.string().regex(/^[A-Za-z0-9_-]{87}$/);
|
|
37
|
+
const recordSchema = z.strictObject({
|
|
38
|
+
version: z.literal(2),
|
|
39
|
+
endpoint: endpointSchema.optional(),
|
|
40
|
+
privateKey: jwkSchema,
|
|
41
|
+
endpointPublicKey: pointSchema,
|
|
42
|
+
authPublicKey: pointSchema,
|
|
43
|
+
authPrivateKey: jwkSchema,
|
|
44
|
+
}).superRefine((record, context) => {
|
|
45
|
+
if (record.endpointPublicKey === record.authPublicKey)
|
|
46
|
+
context.addIssue({ code: 'custom', message: 'Shared identity key' });
|
|
47
|
+
const endpoint = record.endpoint;
|
|
48
|
+
if (endpoint !== undefined && (endpoint.kind !== 'bridge' || endpoint.publicKey !== record.endpointPublicKey || !isAccountId(endpoint.ownerId))) {
|
|
49
|
+
context.addIssue({ code: 'custom', message: 'Invalid bound endpoint' });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
const queues = new Map();
|
|
53
|
+
const swept = new Set();
|
|
54
|
+
/** Canonical unpadded base64url of exactly 32 bytes: the accountId syntax. */
|
|
55
|
+
function isAccountId(value) {
|
|
56
|
+
if (typeof value !== 'string' || !/^[A-Za-z0-9_-]{43}$/.test(value))
|
|
57
|
+
return false;
|
|
58
|
+
const raw = Buffer.from(value, 'base64url');
|
|
59
|
+
return raw.length === 32 && raw.toString('base64url') === value;
|
|
60
|
+
}
|
|
61
|
+
function identityFile(dataDir) {
|
|
62
|
+
return resolve(join(dataDir, 'security', FILE_NAME));
|
|
63
|
+
}
|
|
64
|
+
function errorCode(error) {
|
|
65
|
+
return typeof error === 'object' && error !== null && 'code' in error && typeof error.code === 'string' ? error.code : undefined;
|
|
66
|
+
}
|
|
67
|
+
/** Operations on one file run one at a time within this process. */
|
|
68
|
+
function serialized(file, run) {
|
|
69
|
+
const previous = queues.get(file) ?? Promise.resolve();
|
|
70
|
+
const next = previous.then(run);
|
|
71
|
+
const settled = next.then(() => undefined, () => undefined);
|
|
72
|
+
queues.set(file, settled);
|
|
73
|
+
void settled.then(() => { if (queues.get(file) === settled)
|
|
74
|
+
queues.delete(file); });
|
|
75
|
+
return next;
|
|
76
|
+
}
|
|
77
|
+
/** Once per process: removes `<file>.<uuid>.tmp` left by a crash mid-write. Runs inside the file's queue, so no own write is in flight. */
|
|
78
|
+
async function sweepTemporaries(file) {
|
|
79
|
+
if (swept.has(file))
|
|
80
|
+
return;
|
|
81
|
+
swept.add(file);
|
|
82
|
+
const prefix = basename(file);
|
|
83
|
+
let names;
|
|
84
|
+
try {
|
|
85
|
+
names = await readdir(dirname(file));
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
for (const name of names) {
|
|
91
|
+
if (name.indexOf(prefix) === 0 && TEMPORARY.test(name.slice(prefix.length)))
|
|
92
|
+
await rm(join(dirname(file), name), { force: true }).catch(() => undefined);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** null when absent; every other read failure is key_unavailable. */
|
|
96
|
+
async function readText(file) {
|
|
97
|
+
let handle;
|
|
98
|
+
try {
|
|
99
|
+
handle = await open(file, 'r');
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (errorCode(error) === 'ENOENT')
|
|
103
|
+
return null;
|
|
104
|
+
throw new BridgeSecurityError('key_unavailable');
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
const info = await handle.stat();
|
|
108
|
+
if (!info.isFile() || info.size > MAX_FILE_BYTES)
|
|
109
|
+
throw new Error('Unexpected identity file');
|
|
110
|
+
return await handle.readFile('utf8');
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
throw new BridgeSecurityError('key_unavailable');
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
await handle.close();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function parseRecord(text) {
|
|
120
|
+
let value;
|
|
121
|
+
try {
|
|
122
|
+
value = JSON.parse(text);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
throw new BridgeSecurityError('key_unavailable');
|
|
126
|
+
}
|
|
127
|
+
const parsed = recordSchema.safeParse(value);
|
|
128
|
+
if (!parsed.success)
|
|
129
|
+
throw new BridgeSecurityError('key_unavailable');
|
|
130
|
+
return parsed.data;
|
|
131
|
+
}
|
|
132
|
+
async function readRecord(file) {
|
|
133
|
+
const text = await readText(file);
|
|
134
|
+
return text === null ? null : parseRecord(text);
|
|
135
|
+
}
|
|
136
|
+
async function syncDirectory(directory) {
|
|
137
|
+
let handle;
|
|
138
|
+
try {
|
|
139
|
+
handle = await open(directory, 'r');
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
await handle.sync();
|
|
146
|
+
}
|
|
147
|
+
catch { /* directory fsync is unsupported on some platforms */ }
|
|
148
|
+
finally {
|
|
149
|
+
await handle.close();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Writes a complete 0600 file next to the target, then publishes it atomically: `link` when `exclusive` (never replaces an existing
|
|
154
|
+
* identity; returns false if one appeared meanwhile), else `rename` (replace). Throws storage_unavailable on any failure.
|
|
155
|
+
*/
|
|
156
|
+
async function publish(file, record, exclusive) {
|
|
157
|
+
const directory = dirname(file);
|
|
158
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
159
|
+
try {
|
|
160
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
161
|
+
const handle = await open(temporary, 'wx', 0o600);
|
|
162
|
+
try {
|
|
163
|
+
await handle.writeFile(JSON.stringify(recordSchema.parse(record)));
|
|
164
|
+
await handle.sync();
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
await handle.close();
|
|
168
|
+
}
|
|
169
|
+
if (exclusive) {
|
|
170
|
+
try {
|
|
171
|
+
await link(temporary, file);
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
if (errorCode(error) === 'EEXIST')
|
|
175
|
+
return false;
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
await rename(temporary, file);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
throw new BridgeSecurityError('storage_unavailable');
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
await rm(temporary, { force: true }).catch(() => undefined);
|
|
188
|
+
}
|
|
189
|
+
await syncDirectory(directory);
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
function sameBytes(left, right) {
|
|
193
|
+
if (left.length !== right.length)
|
|
194
|
+
return false;
|
|
195
|
+
let difference = 0;
|
|
196
|
+
for (let index = 0; index < left.length; index += 1)
|
|
197
|
+
difference |= left[index] ^ right[index];
|
|
198
|
+
return difference === 0;
|
|
199
|
+
}
|
|
200
|
+
async function generateRecord() {
|
|
201
|
+
try {
|
|
202
|
+
const endpoint = await crypto.subtle.generateKey(ECDH, true, ['deriveBits']);
|
|
203
|
+
const auth = await generateAuthKey(true);
|
|
204
|
+
return recordSchema.parse({
|
|
205
|
+
version: 2,
|
|
206
|
+
privateKey: await crypto.subtle.exportKey('jwk', endpoint.privateKey),
|
|
207
|
+
endpointPublicKey: await exportPublicKey(endpoint.publicKey),
|
|
208
|
+
authPublicKey: auth.publicKey,
|
|
209
|
+
authPrivateKey: await crypto.subtle.exportKey('jwk', auth.privateKey),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
throw new BridgeSecurityError('key_unavailable');
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/** Imports both keys non-extractable and proves each private key belongs to its pinned public point. */
|
|
217
|
+
async function importRecord(record) {
|
|
218
|
+
try {
|
|
219
|
+
const endpointPublic = await importPublicKey(record.endpointPublicKey);
|
|
220
|
+
const endpointJwk = await crypto.subtle.exportKey('jwk', endpointPublic);
|
|
221
|
+
if (record.privateKey.x !== endpointJwk.x || record.privateKey.y !== endpointJwk.y)
|
|
222
|
+
throw new Error('Endpoint key mismatch');
|
|
223
|
+
const endpointPrivate = await crypto.subtle.importKey('jwk', record.privateKey, ECDH, false, ['deriveBits']);
|
|
224
|
+
// WebCrypto may trust the supplied public coordinates of a JWK: prove the private scalar by a fresh exchange.
|
|
225
|
+
const probe = await crypto.subtle.generateKey(ECDH, false, ['deriveBits']);
|
|
226
|
+
const left = new Uint8Array(await crypto.subtle.deriveBits({ name: 'ECDH', public: probe.publicKey }, endpointPrivate, 256));
|
|
227
|
+
const right = new Uint8Array(await crypto.subtle.deriveBits({ name: 'ECDH', public: endpointPublic }, probe.privateKey, 256));
|
|
228
|
+
if (!sameBytes(left, right))
|
|
229
|
+
throw new Error('Endpoint key mismatch');
|
|
230
|
+
const authRaw = Buffer.from(record.authPublicKey, 'base64url');
|
|
231
|
+
if (authRaw.length !== 65 || authRaw[0] !== 4 || authRaw.toString('base64url') !== record.authPublicKey)
|
|
232
|
+
throw new Error('Auth key');
|
|
233
|
+
const authPublic = await crypto.subtle.importKey('raw', new Uint8Array(authRaw), ECDSA, true, ['verify']);
|
|
234
|
+
const authJwk = await crypto.subtle.exportKey('jwk', authPublic);
|
|
235
|
+
if (record.authPrivateKey.x !== authJwk.x || record.authPrivateKey.y !== authJwk.y)
|
|
236
|
+
throw new Error('Auth key mismatch');
|
|
237
|
+
const authPrivateKey = await crypto.subtle.importKey('jwk', record.authPrivateKey, ECDSA, false, ['sign']);
|
|
238
|
+
const challenge = crypto.getRandomValues(new Uint8Array(32));
|
|
239
|
+
const signature = await crypto.subtle.sign(SIGN, authPrivateKey, challenge);
|
|
240
|
+
if (!await crypto.subtle.verify(SIGN, authPublic, signature, challenge))
|
|
241
|
+
throw new Error('Auth key mismatch');
|
|
242
|
+
const endpointRaw = new Uint8Array(Buffer.from(record.endpointPublicKey, 'base64url'));
|
|
243
|
+
const endpointId = Buffer.from(await crypto.subtle.digest('SHA-256', endpointRaw)).toString('base64url');
|
|
244
|
+
const endpoint = record.endpoint === undefined ? null : { ...record.endpoint };
|
|
245
|
+
if (endpoint !== null && endpoint.id !== endpointId)
|
|
246
|
+
throw new Error('Endpoint id mismatch');
|
|
247
|
+
return {
|
|
248
|
+
keyPair: { publicKey: endpointPublic, privateKey: endpointPrivate }, endpointPublicKey: record.endpointPublicKey, endpointId,
|
|
249
|
+
authPublicKey: record.authPublicKey, authPrivateKey, endpoint, accountId: endpoint === null ? null : endpoint.ownerId,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
throw new BridgeSecurityError('key_unavailable');
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/** Reports the v2 identity file's state without importing keys. Throws BridgeSecurityError('key_unavailable') for a corrupted file. */
|
|
257
|
+
export async function inspectIdentityFile(dataDir) {
|
|
258
|
+
const record = await readRecord(identityFile(dataDir));
|
|
259
|
+
if (record === null)
|
|
260
|
+
return { state: 'absent', accountId: null };
|
|
261
|
+
return { state: 'ready', accountId: record.endpoint === undefined ? null : record.endpoint.ownerId };
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* True iff the data dir holds v1 ACCOUNT state: `refresh-token`, `security/account-enrollment.json`,
|
|
265
|
+
* `security/pending-approval-status.json` or `security/pending-approval-confirmation.json` exists, or `security/bridge-identity.json`
|
|
266
|
+
* has a readable endpoint owner that is not `solo:` (Solo's identity is never reported). An unreadable bridge-identity.json is not
|
|
267
|
+
* reported: its owner cannot be told apart from Solo's. Reads only; never modifies anything.
|
|
268
|
+
*/
|
|
269
|
+
export function hasLegacyAccountState(dataDir) {
|
|
270
|
+
if (LEGACY_ACCOUNT_FILES.some(parts => existsSync(join(dataDir, ...parts))))
|
|
271
|
+
return true;
|
|
272
|
+
let value;
|
|
273
|
+
try {
|
|
274
|
+
value = JSON.parse(readFileSync(join(dataDir, 'security', 'bridge-identity.json'), 'utf8'));
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
if (typeof value !== 'object' || value === null || !('endpoint' in value))
|
|
280
|
+
return false;
|
|
281
|
+
const endpoint = value.endpoint;
|
|
282
|
+
if (typeof endpoint !== 'object' || endpoint === null || !('ownerId' in endpoint) || typeof endpoint.ownerId !== 'string')
|
|
283
|
+
return false;
|
|
284
|
+
return endpoint.ownerId.length > 0 && endpoint.ownerId.indexOf('solo:') !== 0;
|
|
285
|
+
}
|
|
286
|
+
/** Loads the identity without creating one: null when absent. Throws BridgeSecurityError('key_unavailable') for a corrupted file. */
|
|
287
|
+
export function readLedgerIdentity(dataDir) {
|
|
288
|
+
const file = identityFile(dataDir);
|
|
289
|
+
return serialized(file, async () => {
|
|
290
|
+
await sweepTemporaries(file);
|
|
291
|
+
const record = await readRecord(file);
|
|
292
|
+
return record === null ? null : importRecord(record);
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Loads the identity, creating fresh keys only when the file is absent. Throws BridgeSecurityError: key_unavailable for an unreadable
|
|
297
|
+
* or corrupted file (never replaced), storage_unavailable when the new file cannot be written.
|
|
298
|
+
*/
|
|
299
|
+
export function loadOrCreateLedgerIdentity(dataDir) {
|
|
300
|
+
const file = identityFile(dataDir);
|
|
301
|
+
return serialized(file, async () => {
|
|
302
|
+
await sweepTemporaries(file);
|
|
303
|
+
const existing = await readRecord(file);
|
|
304
|
+
if (existing !== null)
|
|
305
|
+
return importRecord(existing);
|
|
306
|
+
const created = await generateRecord();
|
|
307
|
+
if (await publish(file, created, true))
|
|
308
|
+
return importRecord(created);
|
|
309
|
+
// Another process created it between the read and the link: use theirs.
|
|
310
|
+
const raced = await readRecord(file);
|
|
311
|
+
if (raced === null)
|
|
312
|
+
throw new BridgeSecurityError('key_unavailable');
|
|
313
|
+
return importRecord(raced);
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Binds the identity to `accountId` once: sets endpoint {version:1, id, ownerId: accountId, kind:'bridge', publicKey}. Idempotent for
|
|
318
|
+
* the same account. Throws BridgeSecurityError: wrong_context for a different (or malformed) accountId, key_unavailable when there is
|
|
319
|
+
* no valid identity to bind, storage_unavailable when the file cannot be written. Binds are serialized within this process only: the
|
|
320
|
+
* bridge's single-instance guarantee keeps a second process from binding concurrently.
|
|
321
|
+
*/
|
|
322
|
+
export function bindLedgerIdentity(dataDir, accountId) {
|
|
323
|
+
if (!isAccountId(accountId))
|
|
324
|
+
return Promise.reject(new BridgeSecurityError('wrong_context'));
|
|
325
|
+
const file = identityFile(dataDir);
|
|
326
|
+
return serialized(file, async () => {
|
|
327
|
+
await sweepTemporaries(file);
|
|
328
|
+
const record = await readRecord(file);
|
|
329
|
+
if (record === null)
|
|
330
|
+
throw new BridgeSecurityError('key_unavailable');
|
|
331
|
+
const identity = await importRecord(record);
|
|
332
|
+
if (identity.accountId !== null) {
|
|
333
|
+
if (identity.accountId !== accountId)
|
|
334
|
+
throw new BridgeSecurityError('wrong_context');
|
|
335
|
+
return identity;
|
|
336
|
+
}
|
|
337
|
+
const endpoint = { version: 1, id: identity.endpointId, ownerId: accountId, kind: 'bridge', publicKey: identity.endpointPublicKey };
|
|
338
|
+
await publish(file, { ...record, endpoint }, false);
|
|
339
|
+
return { ...identity, endpoint: { ...endpoint }, accountId };
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
//# sourceMappingURL=ledger-identity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ledger-identity.js","sourceRoot":"","sources":["../../../src/security/bridge/ledger-identity.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { BridgePairLink, PairApproval } from '../ledger/pairing.js';
|
|
2
|
+
import type { Ledger, LedgerCheckpoint, LedgerTransition, Signed } from '../ledger/protocol.js';
|
|
3
|
+
export type LedgerSource = 'server' | 'relay';
|
|
4
|
+
export type OwnBridgeStatus = 'active' | 'revoked' | 'absent';
|
|
5
|
+
export type ReconcileResult = 'ok' | 'unpaired' | 'needs_ledger';
|
|
6
|
+
export interface StickyRevocations {
|
|
7
|
+
deviceIds: string[];
|
|
8
|
+
bridgeIds: string[];
|
|
9
|
+
}
|
|
10
|
+
export interface BridgeLedgerState {
|
|
11
|
+
ledger: Ledger;
|
|
12
|
+
/** The stored chain's own checkpoint (raw). Never use it for a trust decision. */
|
|
13
|
+
checkpoint: LedgerCheckpoint;
|
|
14
|
+
/** applyRevocations(checkpoint, sticky): the checkpoint for every trust decision. */
|
|
15
|
+
trusted: LedgerCheckpoint;
|
|
16
|
+
/** Union of the revocations of every chain this replica kept or adopted. */
|
|
17
|
+
sticky: StickyRevocations;
|
|
18
|
+
lastServerReadAt: number | null;
|
|
19
|
+
}
|
|
20
|
+
export interface LedgerOfferResult {
|
|
21
|
+
decision: 'adopt' | 'keep' | 'reject';
|
|
22
|
+
/** The stored state after the decision; null while the store is empty. */
|
|
23
|
+
state: BridgeLedgerState | null;
|
|
24
|
+
/** Local records discarded by a winning fork, for the owner to re-sign; empty otherwise. */
|
|
25
|
+
discarded: Signed<LedgerTransition>[];
|
|
26
|
+
}
|
|
27
|
+
export interface BridgeLedgerStoreOptions {
|
|
28
|
+
/** The account the bridge identity is bound to: ledgers of any other account are rejected even while the store is empty, and a
|
|
29
|
+
* stored ledger of another account throws LedgerStoreError('ledger_store_wrong_account'). */
|
|
30
|
+
accountId?: string | null;
|
|
31
|
+
now?: () => number;
|
|
32
|
+
}
|
|
33
|
+
export type LedgerStoreErrorCode = 'ledger_store_corrupt' | 'ledger_store_wrong_account' | 'ledger_store_pairing_rejected' | 'ledger_store_bound_rejected';
|
|
34
|
+
/** Not retryable; file contents and crypto errors never enter it. Read/write failures throw BridgeSecurityError('storage_unavailable'). */
|
|
35
|
+
export declare class LedgerStoreError extends Error {
|
|
36
|
+
readonly code: LedgerStoreErrorCode;
|
|
37
|
+
readonly retryable = false;
|
|
38
|
+
constructor(code: LedgerStoreErrorCode);
|
|
39
|
+
}
|
|
40
|
+
export declare class BridgeLedgerStore {
|
|
41
|
+
private readonly replica;
|
|
42
|
+
constructor(dataDir: string, options?: BridgeLedgerStoreOptions);
|
|
43
|
+
/** The stored state, or null while no ledger is stored. Throws LedgerStoreError for a corrupted or wrong-account file. */
|
|
44
|
+
read(): Promise<BridgeLedgerState | null>;
|
|
45
|
+
/** applyRevocations(checkpoint, sticky) of the stored chain, or null while no ledger is stored. */
|
|
46
|
+
trusted(): Promise<LedgerCheckpoint | null>;
|
|
47
|
+
/** 'revoked' iff bridgeId is revoked in trusted(); 'active' iff trusted() lists bridgeId with this auth key; else 'absent'. */
|
|
48
|
+
ownBridgeStatus(bridgeId: string, authPublicKey: string): Promise<OwnBridgeStatus>;
|
|
49
|
+
/** ownBridgeStatus(bridgeId, …) === 'revoked': by the stored chain or by any chain this replica ever kept or adopted. */
|
|
50
|
+
ownBridgeRevoked(bridgeId: string): Promise<boolean>;
|
|
51
|
+
/**
|
|
52
|
+
* Offers an untrusted candidate chain from the server or the relay. Never binds an empty store, and never throws for a bad candidate:
|
|
53
|
+
* it is rejected. Throws LedgerStoreError for a corrupted or wrong-account local file, and BridgeSecurityError('storage_unavailable')
|
|
54
|
+
* when the file cannot be read or written.
|
|
55
|
+
*/
|
|
56
|
+
offer(candidate: unknown, source: LedgerSource): Promise<LedgerOfferResult>;
|
|
57
|
+
/**
|
|
58
|
+
* The pairing path, after openPairApproval. Runs confirmPairedLedger(link, approval, ledger, now, sticky) with this replica's sticky
|
|
59
|
+
* set, then binds the empty store (or applies the replica rule to a bound store of the same account); the request's bridge must be in
|
|
60
|
+
* the resulting trusted roster. Returns trusted(). Throws LedgerStoreError('ledger_store_pairing_rejected') and writes nothing on any
|
|
61
|
+
* pairing failure.
|
|
62
|
+
*/
|
|
63
|
+
adoptPaired(link: BridgePairLink, approval: PairApproval, ledger: unknown, now?: number): Promise<LedgerCheckpoint>;
|
|
64
|
+
/**
|
|
65
|
+
* Recovery when the identity is bound but the store is empty (reconcileIdentityAndStore returned 'needs_ledger'): accepts a server
|
|
66
|
+
* ledger only if its account is the identity's bound account and its trusted roster lists this identity's bridge keys (endpoint id and
|
|
67
|
+
* authPublicKey). Returns trusted(). Throws LedgerStoreError('ledger_store_bound_rejected') and writes nothing otherwise.
|
|
68
|
+
*/
|
|
69
|
+
adoptBound(ledger: unknown, now?: number): Promise<LedgerCheckpoint>;
|
|
70
|
+
/** Records the time of the last successful server ledger read. No-op while no ledger is stored. */
|
|
71
|
+
markServerRead(now: number): Promise<void>;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Startup recovery between the identity file and the ledger store, under the store's queue:
|
|
75
|
+
* - identity bound, store holds its account's ledger -> 'ok'; store holds another account's -> throws ledger_store_wrong_account;
|
|
76
|
+
* - identity bound, store empty -> 'needs_ledger' (fetch the server ledger, then adoptBound); a bound identity is never unbound;
|
|
77
|
+
* - identity unbound (or absent), store empty -> 'unpaired';
|
|
78
|
+
* - identity unbound, store's trusted roster lists this identity's bridge keys -> binds the identity to that account -> 'ok';
|
|
79
|
+
* - otherwise (absent identity, or keys not listed) the stale ledger.json is deleted -> 'unpaired'.
|
|
80
|
+
* Corrupted files throw (LedgerStoreError / BridgeSecurityError('key_unavailable')) and are never reset.
|
|
81
|
+
*/
|
|
82
|
+
export declare function reconcileIdentityAndStore(dataDir: string, options?: {
|
|
83
|
+
now?: () => number;
|
|
84
|
+
}): Promise<ReconcileResult>;
|