@the9ines/bolt-core 0.5.1 → 0.6.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/btr/constants.d.ts +20 -0
- package/dist/btr/constants.js +25 -0
- package/dist/btr/encrypt.d.ts +26 -0
- package/dist/btr/encrypt.js +56 -0
- package/dist/btr/errors.d.ts +28 -0
- package/dist/btr/errors.js +38 -0
- package/dist/btr/index.d.ts +15 -0
- package/dist/btr/index.js +21 -0
- package/dist/btr/key-schedule.d.ts +33 -0
- package/dist/btr/key-schedule.js +38 -0
- package/dist/btr/negotiate.d.ts +34 -0
- package/dist/btr/negotiate.js +50 -0
- package/dist/btr/ratchet.d.ts +34 -0
- package/dist/btr/ratchet.js +44 -0
- package/dist/btr/replay.d.ts +30 -0
- package/dist/btr/replay.js +81 -0
- package/dist/btr/state.d.ts +81 -0
- package/dist/btr/state.js +146 -0
- package/dist/crypto.d.ts +4 -9
- package/dist/crypto.js +14 -9
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js +6 -1
- package/dist/identity.d.ts +1 -3
- package/dist/identity.js +5 -3
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -0
- package/dist/sas.d.ts +6 -0
- package/dist/sas.js +13 -0
- package/dist/wasm-crypto.d.ts +118 -0
- package/dist/wasm-crypto.js +119 -0
- package/package.json +4 -3
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BTR-specific constants — HKDF info strings and key lengths (§14).
|
|
3
|
+
*
|
|
4
|
+
* All values locked from PROTOCOL.md §14. No divergence permitted.
|
|
5
|
+
* Must match Rust bolt-btr/src/constants.rs exactly.
|
|
6
|
+
*/
|
|
7
|
+
/** HKDF info string for session root derivation (§16.3). */
|
|
8
|
+
export declare const BTR_SESSION_ROOT_INFO = "bolt-btr-session-root-v1";
|
|
9
|
+
/** HKDF info string for transfer root derivation (§16.3). */
|
|
10
|
+
export declare const BTR_TRANSFER_ROOT_INFO = "bolt-btr-transfer-root-v1";
|
|
11
|
+
/** HKDF info string for message key derivation (§16.3). */
|
|
12
|
+
export declare const BTR_MESSAGE_KEY_INFO = "bolt-btr-message-key-v1";
|
|
13
|
+
/** HKDF info string for chain key advancement (§16.3). */
|
|
14
|
+
export declare const BTR_CHAIN_ADVANCE_INFO = "bolt-btr-chain-advance-v1";
|
|
15
|
+
/** HKDF info string for DH ratchet step (§16.3). */
|
|
16
|
+
export declare const BTR_DH_RATCHET_INFO = "bolt-btr-dh-ratchet-v1";
|
|
17
|
+
/** BTR key length in bytes (all derived keys). */
|
|
18
|
+
export declare const BTR_KEY_LENGTH = 32;
|
|
19
|
+
/** BTR wire error codes (§16.7, extends PROTOCOL.md §10 registry). */
|
|
20
|
+
export declare const BTR_WIRE_ERROR_CODES: readonly ["RATCHET_STATE_ERROR", "RATCHET_CHAIN_ERROR", "RATCHET_DECRYPT_FAIL", "RATCHET_DOWNGRADE_REJECTED"];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BTR-specific constants — HKDF info strings and key lengths (§14).
|
|
3
|
+
*
|
|
4
|
+
* All values locked from PROTOCOL.md §14. No divergence permitted.
|
|
5
|
+
* Must match Rust bolt-btr/src/constants.rs exactly.
|
|
6
|
+
*/
|
|
7
|
+
/** HKDF info string for session root derivation (§16.3). */
|
|
8
|
+
export const BTR_SESSION_ROOT_INFO = 'bolt-btr-session-root-v1';
|
|
9
|
+
/** HKDF info string for transfer root derivation (§16.3). */
|
|
10
|
+
export const BTR_TRANSFER_ROOT_INFO = 'bolt-btr-transfer-root-v1';
|
|
11
|
+
/** HKDF info string for message key derivation (§16.3). */
|
|
12
|
+
export const BTR_MESSAGE_KEY_INFO = 'bolt-btr-message-key-v1';
|
|
13
|
+
/** HKDF info string for chain key advancement (§16.3). */
|
|
14
|
+
export const BTR_CHAIN_ADVANCE_INFO = 'bolt-btr-chain-advance-v1';
|
|
15
|
+
/** HKDF info string for DH ratchet step (§16.3). */
|
|
16
|
+
export const BTR_DH_RATCHET_INFO = 'bolt-btr-dh-ratchet-v1';
|
|
17
|
+
/** BTR key length in bytes (all derived keys). */
|
|
18
|
+
export const BTR_KEY_LENGTH = 32;
|
|
19
|
+
/** BTR wire error codes (§16.7, extends PROTOCOL.md §10 registry). */
|
|
20
|
+
export const BTR_WIRE_ERROR_CODES = [
|
|
21
|
+
'RATCHET_STATE_ERROR',
|
|
22
|
+
'RATCHET_CHAIN_ERROR',
|
|
23
|
+
'RATCHET_DECRYPT_FAIL',
|
|
24
|
+
'RATCHET_DOWNGRADE_REJECTED',
|
|
25
|
+
];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BTR encryption — NaCl secretbox keyed by BTR message_key (§16.4).
|
|
3
|
+
*
|
|
4
|
+
* Uses symmetric NaCl secretbox (XSalsa20-Poly1305), NOT asymmetric box.
|
|
5
|
+
* Both peers derive identical message_key deterministically via HKDF.
|
|
6
|
+
* Fresh 24-byte CSPRNG nonce per envelope (production) or fixed nonce (test vectors).
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Encrypt a chunk using NaCl secretbox with a BTR-derived message_key.
|
|
10
|
+
*
|
|
11
|
+
* Returns nonce || ciphertext (24 + plaintext.length + 16 bytes).
|
|
12
|
+
* Fresh 24-byte CSPRNG nonce generated internally.
|
|
13
|
+
*/
|
|
14
|
+
export declare function btrSeal(messageKey: Uint8Array, plaintext: Uint8Array): Uint8Array;
|
|
15
|
+
/**
|
|
16
|
+
* Encrypt with a caller-provided nonce (deterministic vectors only).
|
|
17
|
+
* NOT for production — nonce reuse with the same key is catastrophic.
|
|
18
|
+
*/
|
|
19
|
+
export declare function btrSealDeterministic(messageKey: Uint8Array, plaintext: Uint8Array, nonce: Uint8Array): Uint8Array;
|
|
20
|
+
/**
|
|
21
|
+
* Decrypt a chunk using NaCl secretbox with a BTR-derived message_key.
|
|
22
|
+
*
|
|
23
|
+
* Expects nonce || ciphertext format (first 24 bytes are nonce).
|
|
24
|
+
* Throws BtrError with RATCHET_DECRYPT_FAIL on MAC failure or truncation.
|
|
25
|
+
*/
|
|
26
|
+
export declare function btrOpen(messageKey: Uint8Array, sealed: Uint8Array): Uint8Array;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BTR encryption — NaCl secretbox keyed by BTR message_key (§16.4).
|
|
3
|
+
*
|
|
4
|
+
* Uses symmetric NaCl secretbox (XSalsa20-Poly1305), NOT asymmetric box.
|
|
5
|
+
* Both peers derive identical message_key deterministically via HKDF.
|
|
6
|
+
* Fresh 24-byte CSPRNG nonce per envelope (production) or fixed nonce (test vectors).
|
|
7
|
+
*/
|
|
8
|
+
import tweetnacl from 'tweetnacl';
|
|
9
|
+
import { ratchetDecryptFail } from './errors.js';
|
|
10
|
+
/** NaCl secretbox nonce length (24 bytes). */
|
|
11
|
+
const SECRETBOX_NONCE_LENGTH = 24;
|
|
12
|
+
/** NaCl secretbox MAC overhead (Poly1305, 16 bytes). */
|
|
13
|
+
const SECRETBOX_OVERHEAD = 16;
|
|
14
|
+
/**
|
|
15
|
+
* Encrypt a chunk using NaCl secretbox with a BTR-derived message_key.
|
|
16
|
+
*
|
|
17
|
+
* Returns nonce || ciphertext (24 + plaintext.length + 16 bytes).
|
|
18
|
+
* Fresh 24-byte CSPRNG nonce generated internally.
|
|
19
|
+
*/
|
|
20
|
+
export function btrSeal(messageKey, plaintext) {
|
|
21
|
+
const nonce = tweetnacl.randomBytes(SECRETBOX_NONCE_LENGTH);
|
|
22
|
+
const ciphertext = tweetnacl.secretbox(plaintext, nonce, messageKey);
|
|
23
|
+
const combined = new Uint8Array(SECRETBOX_NONCE_LENGTH + ciphertext.length);
|
|
24
|
+
combined.set(nonce);
|
|
25
|
+
combined.set(ciphertext, SECRETBOX_NONCE_LENGTH);
|
|
26
|
+
return combined;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Encrypt with a caller-provided nonce (deterministic vectors only).
|
|
30
|
+
* NOT for production — nonce reuse with the same key is catastrophic.
|
|
31
|
+
*/
|
|
32
|
+
export function btrSealDeterministic(messageKey, plaintext, nonce) {
|
|
33
|
+
const ciphertext = tweetnacl.secretbox(plaintext, nonce, messageKey);
|
|
34
|
+
const combined = new Uint8Array(SECRETBOX_NONCE_LENGTH + ciphertext.length);
|
|
35
|
+
combined.set(nonce);
|
|
36
|
+
combined.set(ciphertext, SECRETBOX_NONCE_LENGTH);
|
|
37
|
+
return combined;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Decrypt a chunk using NaCl secretbox with a BTR-derived message_key.
|
|
41
|
+
*
|
|
42
|
+
* Expects nonce || ciphertext format (first 24 bytes are nonce).
|
|
43
|
+
* Throws BtrError with RATCHET_DECRYPT_FAIL on MAC failure or truncation.
|
|
44
|
+
*/
|
|
45
|
+
export function btrOpen(messageKey, sealed) {
|
|
46
|
+
if (sealed.length < SECRETBOX_NONCE_LENGTH + SECRETBOX_OVERHEAD) {
|
|
47
|
+
throw ratchetDecryptFail('sealed payload too short');
|
|
48
|
+
}
|
|
49
|
+
const nonce = sealed.slice(0, SECRETBOX_NONCE_LENGTH);
|
|
50
|
+
const ciphertext = sealed.slice(SECRETBOX_NONCE_LENGTH);
|
|
51
|
+
const plaintext = tweetnacl.secretbox.open(ciphertext, nonce, messageKey);
|
|
52
|
+
if (plaintext === null) {
|
|
53
|
+
throw ratchetDecryptFail('secretbox open failed');
|
|
54
|
+
}
|
|
55
|
+
return plaintext;
|
|
56
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BTR error types — §16.7 error behavior.
|
|
3
|
+
*
|
|
4
|
+
* Four error codes with deterministic behavior mapping.
|
|
5
|
+
* Must match Rust bolt-btr/src/errors.rs semantics exactly.
|
|
6
|
+
*/
|
|
7
|
+
import { BTR_WIRE_ERROR_CODES } from './constants.js';
|
|
8
|
+
type BtrWireErrorCode = (typeof BTR_WIRE_ERROR_CODES)[number];
|
|
9
|
+
/** BTR-specific error — carries a wire error code and disconnect semantics. */
|
|
10
|
+
export declare class BtrError extends Error {
|
|
11
|
+
/** Canonical wire error code from §16.7. */
|
|
12
|
+
readonly wireCode: BtrWireErrorCode;
|
|
13
|
+
constructor(wireCode: BtrWireErrorCode, message: string);
|
|
14
|
+
/**
|
|
15
|
+
* Returns true if the required action is disconnect (vs cancel transfer).
|
|
16
|
+
* Matches Rust BtrError::requires_disconnect().
|
|
17
|
+
*/
|
|
18
|
+
requiresDisconnect(): boolean;
|
|
19
|
+
}
|
|
20
|
+
/** Ratchet generation mismatch, unexpected DH key, or missing BTR fields. Action: disconnect. */
|
|
21
|
+
export declare function ratchetStateError(detail: string): BtrError;
|
|
22
|
+
/** chain_index != expected next, chain index gap, or replay. Action: cancel transfer. */
|
|
23
|
+
export declare function ratchetChainError(detail: string): BtrError;
|
|
24
|
+
/** NaCl secretbox open fails with BTR message key. Action: cancel transfer. */
|
|
25
|
+
export declare function ratchetDecryptFail(detail: string): BtrError;
|
|
26
|
+
/** Peer advertised BTR but sends invalid envelopes. Action: disconnect. */
|
|
27
|
+
export declare function ratchetDowngradeRejected(detail: string): BtrError;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BTR error types — §16.7 error behavior.
|
|
3
|
+
*
|
|
4
|
+
* Four error codes with deterministic behavior mapping.
|
|
5
|
+
* Must match Rust bolt-btr/src/errors.rs semantics exactly.
|
|
6
|
+
*/
|
|
7
|
+
/** BTR-specific error — carries a wire error code and disconnect semantics. */
|
|
8
|
+
export class BtrError extends Error {
|
|
9
|
+
constructor(wireCode, message) {
|
|
10
|
+
super(`${wireCode}: ${message}`);
|
|
11
|
+
this.name = 'BtrError';
|
|
12
|
+
this.wireCode = wireCode;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Returns true if the required action is disconnect (vs cancel transfer).
|
|
16
|
+
* Matches Rust BtrError::requires_disconnect().
|
|
17
|
+
*/
|
|
18
|
+
requiresDisconnect() {
|
|
19
|
+
return (this.wireCode === 'RATCHET_STATE_ERROR' ||
|
|
20
|
+
this.wireCode === 'RATCHET_DOWNGRADE_REJECTED');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Ratchet generation mismatch, unexpected DH key, or missing BTR fields. Action: disconnect. */
|
|
24
|
+
export function ratchetStateError(detail) {
|
|
25
|
+
return new BtrError('RATCHET_STATE_ERROR', detail);
|
|
26
|
+
}
|
|
27
|
+
/** chain_index != expected next, chain index gap, or replay. Action: cancel transfer. */
|
|
28
|
+
export function ratchetChainError(detail) {
|
|
29
|
+
return new BtrError('RATCHET_CHAIN_ERROR', detail);
|
|
30
|
+
}
|
|
31
|
+
/** NaCl secretbox open fails with BTR message key. Action: cancel transfer. */
|
|
32
|
+
export function ratchetDecryptFail(detail) {
|
|
33
|
+
return new BtrError('RATCHET_DECRYPT_FAIL', detail);
|
|
34
|
+
}
|
|
35
|
+
/** Peer advertised BTR but sends invalid envelopes. Action: disconnect. */
|
|
36
|
+
export function ratchetDowngradeRejected(detail) {
|
|
37
|
+
return new BtrError('RATCHET_DOWNGRADE_REJECTED', detail);
|
|
38
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bolt Transfer Ratchet (BTR) — TypeScript parity implementation.
|
|
3
|
+
*
|
|
4
|
+
* Barrel re-export for all BTR modules.
|
|
5
|
+
*/
|
|
6
|
+
export { BTR_SESSION_ROOT_INFO, BTR_TRANSFER_ROOT_INFO, BTR_MESSAGE_KEY_INFO, BTR_CHAIN_ADVANCE_INFO, BTR_DH_RATCHET_INFO, BTR_KEY_LENGTH, BTR_WIRE_ERROR_CODES, } from './constants.js';
|
|
7
|
+
export { BtrError, ratchetStateError, ratchetChainError, ratchetDecryptFail, ratchetDowngradeRejected, } from './errors.js';
|
|
8
|
+
export { deriveSessionRoot, deriveTransferRoot, chainAdvance, } from './key-schedule.js';
|
|
9
|
+
export type { ChainAdvanceOutput } from './key-schedule.js';
|
|
10
|
+
export { generateRatchetKeypair, scalarMult, deriveRatchetedSessionRoot, } from './ratchet.js';
|
|
11
|
+
export { btrSeal, btrSealDeterministic, btrOpen } from './encrypt.js';
|
|
12
|
+
export { ReplayGuard } from './replay.js';
|
|
13
|
+
export { BtrMode, negotiateBtr, btrLogToken } from './negotiate.js';
|
|
14
|
+
export type { BtrModeValue } from './negotiate.js';
|
|
15
|
+
export { BtrEngine, BtrTransferContext } from './state.js';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bolt Transfer Ratchet (BTR) — TypeScript parity implementation.
|
|
3
|
+
*
|
|
4
|
+
* Barrel re-export for all BTR modules.
|
|
5
|
+
*/
|
|
6
|
+
// Constants
|
|
7
|
+
export { BTR_SESSION_ROOT_INFO, BTR_TRANSFER_ROOT_INFO, BTR_MESSAGE_KEY_INFO, BTR_CHAIN_ADVANCE_INFO, BTR_DH_RATCHET_INFO, BTR_KEY_LENGTH, BTR_WIRE_ERROR_CODES, } from './constants.js';
|
|
8
|
+
// Errors
|
|
9
|
+
export { BtrError, ratchetStateError, ratchetChainError, ratchetDecryptFail, ratchetDowngradeRejected, } from './errors.js';
|
|
10
|
+
// Key schedule
|
|
11
|
+
export { deriveSessionRoot, deriveTransferRoot, chainAdvance, } from './key-schedule.js';
|
|
12
|
+
// DH ratchet
|
|
13
|
+
export { generateRatchetKeypair, scalarMult, deriveRatchetedSessionRoot, } from './ratchet.js';
|
|
14
|
+
// Encrypt/decrypt
|
|
15
|
+
export { btrSeal, btrSealDeterministic, btrOpen } from './encrypt.js';
|
|
16
|
+
// Replay guard
|
|
17
|
+
export { ReplayGuard } from './replay.js';
|
|
18
|
+
// Negotiate
|
|
19
|
+
export { BtrMode, negotiateBtr, btrLogToken } from './negotiate.js';
|
|
20
|
+
// State engine
|
|
21
|
+
export { BtrEngine, BtrTransferContext } from './state.js';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Key schedule — HKDF-SHA256 derivation chain (§16.3).
|
|
3
|
+
*
|
|
4
|
+
* All derivations use HKDF-SHA256 with info strings from §14.
|
|
5
|
+
* Output length is always 32 bytes (BTR_KEY_LENGTH).
|
|
6
|
+
* Must match Rust bolt-btr/src/key_schedule.rs exactly.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Derive session root key from ephemeral shared secret (§16.3).
|
|
10
|
+
*
|
|
11
|
+
* session_root_key = HKDF-SHA256(salt=empty, ikm=shared_secret, info="bolt-btr-session-root-v1", len=32)
|
|
12
|
+
*/
|
|
13
|
+
export declare function deriveSessionRoot(ephemeralSharedSecret: Uint8Array): Uint8Array;
|
|
14
|
+
/**
|
|
15
|
+
* Derive transfer root key from session root key and transfer_id (§16.3).
|
|
16
|
+
*
|
|
17
|
+
* transfer_root_key = HKDF-SHA256(salt=transfer_id, ikm=session_root_key, info="bolt-btr-transfer-root-v1", len=32)
|
|
18
|
+
*/
|
|
19
|
+
export declare function deriveTransferRoot(sessionRootKey: Uint8Array, transferId: Uint8Array): Uint8Array;
|
|
20
|
+
/** Output of a single chain advance step. */
|
|
21
|
+
export interface ChainAdvanceOutput {
|
|
22
|
+
/** Key for encrypting/decrypting one chunk. Single-use. */
|
|
23
|
+
messageKey: Uint8Array;
|
|
24
|
+
/** Replacement chain key for the next advance step. */
|
|
25
|
+
nextChainKey: Uint8Array;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Advance the symmetric chain: derive message_key and next_chain_key (§16.3).
|
|
29
|
+
*
|
|
30
|
+
* message_key = HKDF-SHA256(salt=empty, ikm=chain_key, info="bolt-btr-message-key-v1", len=32)
|
|
31
|
+
* next_chain_key = HKDF-SHA256(salt=empty, ikm=chain_key, info="bolt-btr-chain-advance-v1", len=32)
|
|
32
|
+
*/
|
|
33
|
+
export declare function chainAdvance(chainKey: Uint8Array): ChainAdvanceOutput;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Key schedule — HKDF-SHA256 derivation chain (§16.3).
|
|
3
|
+
*
|
|
4
|
+
* All derivations use HKDF-SHA256 with info strings from §14.
|
|
5
|
+
* Output length is always 32 bytes (BTR_KEY_LENGTH).
|
|
6
|
+
* Must match Rust bolt-btr/src/key_schedule.rs exactly.
|
|
7
|
+
*/
|
|
8
|
+
import { hkdf } from '@noble/hashes/hkdf.js';
|
|
9
|
+
import { sha256 } from '@noble/hashes/sha2.js';
|
|
10
|
+
import { BTR_KEY_LENGTH, BTR_SESSION_ROOT_INFO, BTR_TRANSFER_ROOT_INFO, BTR_MESSAGE_KEY_INFO, BTR_CHAIN_ADVANCE_INFO, } from './constants.js';
|
|
11
|
+
const encoder = new TextEncoder();
|
|
12
|
+
/**
|
|
13
|
+
* Derive session root key from ephemeral shared secret (§16.3).
|
|
14
|
+
*
|
|
15
|
+
* session_root_key = HKDF-SHA256(salt=empty, ikm=shared_secret, info="bolt-btr-session-root-v1", len=32)
|
|
16
|
+
*/
|
|
17
|
+
export function deriveSessionRoot(ephemeralSharedSecret) {
|
|
18
|
+
return hkdf(sha256, ephemeralSharedSecret, undefined, encoder.encode(BTR_SESSION_ROOT_INFO), BTR_KEY_LENGTH);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Derive transfer root key from session root key and transfer_id (§16.3).
|
|
22
|
+
*
|
|
23
|
+
* transfer_root_key = HKDF-SHA256(salt=transfer_id, ikm=session_root_key, info="bolt-btr-transfer-root-v1", len=32)
|
|
24
|
+
*/
|
|
25
|
+
export function deriveTransferRoot(sessionRootKey, transferId) {
|
|
26
|
+
return hkdf(sha256, sessionRootKey, transferId, encoder.encode(BTR_TRANSFER_ROOT_INFO), BTR_KEY_LENGTH);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Advance the symmetric chain: derive message_key and next_chain_key (§16.3).
|
|
30
|
+
*
|
|
31
|
+
* message_key = HKDF-SHA256(salt=empty, ikm=chain_key, info="bolt-btr-message-key-v1", len=32)
|
|
32
|
+
* next_chain_key = HKDF-SHA256(salt=empty, ikm=chain_key, info="bolt-btr-chain-advance-v1", len=32)
|
|
33
|
+
*/
|
|
34
|
+
export function chainAdvance(chainKey) {
|
|
35
|
+
const messageKey = hkdf(sha256, chainKey, undefined, encoder.encode(BTR_MESSAGE_KEY_INFO), BTR_KEY_LENGTH);
|
|
36
|
+
const nextChainKey = hkdf(sha256, chainKey, undefined, encoder.encode(BTR_CHAIN_ADVANCE_INFO), BTR_KEY_LENGTH);
|
|
37
|
+
return { messageKey, nextChainKey };
|
|
38
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability negotiation — BTR mode decision matrix (§4).
|
|
3
|
+
*
|
|
4
|
+
* Maps the 6-cell negotiation matrix from the BTR-0 spec.
|
|
5
|
+
* Must match Rust bolt-btr/src/negotiate.rs exactly.
|
|
6
|
+
*/
|
|
7
|
+
/** BTR negotiation result. */
|
|
8
|
+
export declare const BtrMode: {
|
|
9
|
+
/** Both peers support BTR. Full per-transfer DH ratchet + per-chunk chain. */
|
|
10
|
+
readonly FullBtr: "FULL_BTR";
|
|
11
|
+
/** One peer supports BTR, the other does not. Fall back to static ephemeral. */
|
|
12
|
+
readonly Downgrade: "DOWNGRADE";
|
|
13
|
+
/** Neither peer supports BTR. Current v1 static ephemeral. */
|
|
14
|
+
readonly StaticEphemeral: "STATIC_EPHEMERAL";
|
|
15
|
+
/** Malformed BTR metadata detected. RATCHET_DOWNGRADE_REJECTED + disconnect. */
|
|
16
|
+
readonly Reject: "REJECT";
|
|
17
|
+
};
|
|
18
|
+
export type BtrModeValue = (typeof BtrMode)[keyof typeof BtrMode];
|
|
19
|
+
/**
|
|
20
|
+
* Negotiate BTR mode from capability advertisement.
|
|
21
|
+
*
|
|
22
|
+
* Implements the 6-cell matrix from §4:
|
|
23
|
+
*
|
|
24
|
+
* | Local | Remote | Well-formed | Result |
|
|
25
|
+
* |-------|--------|-------------|-----------------|
|
|
26
|
+
* | YES | YES | YES | FullBtr |
|
|
27
|
+
* | YES | NO | - | Downgrade |
|
|
28
|
+
* | NO | YES | - | Downgrade |
|
|
29
|
+
* | NO | NO | - | StaticEphemeral |
|
|
30
|
+
* | YES | YES | NO | Reject |
|
|
31
|
+
*/
|
|
32
|
+
export declare function negotiateBtr(localSupports: boolean, remoteSupports: boolean, remoteWellFormed: boolean): BtrModeValue;
|
|
33
|
+
/** Returns the log token for a given BTR mode, or null if none. */
|
|
34
|
+
export declare function btrLogToken(mode: BtrModeValue): string | null;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability negotiation — BTR mode decision matrix (§4).
|
|
3
|
+
*
|
|
4
|
+
* Maps the 6-cell negotiation matrix from the BTR-0 spec.
|
|
5
|
+
* Must match Rust bolt-btr/src/negotiate.rs exactly.
|
|
6
|
+
*/
|
|
7
|
+
/** BTR negotiation result. */
|
|
8
|
+
export const BtrMode = {
|
|
9
|
+
/** Both peers support BTR. Full per-transfer DH ratchet + per-chunk chain. */
|
|
10
|
+
FullBtr: 'FULL_BTR',
|
|
11
|
+
/** One peer supports BTR, the other does not. Fall back to static ephemeral. */
|
|
12
|
+
Downgrade: 'DOWNGRADE',
|
|
13
|
+
/** Neither peer supports BTR. Current v1 static ephemeral. */
|
|
14
|
+
StaticEphemeral: 'STATIC_EPHEMERAL',
|
|
15
|
+
/** Malformed BTR metadata detected. RATCHET_DOWNGRADE_REJECTED + disconnect. */
|
|
16
|
+
Reject: 'REJECT',
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Negotiate BTR mode from capability advertisement.
|
|
20
|
+
*
|
|
21
|
+
* Implements the 6-cell matrix from §4:
|
|
22
|
+
*
|
|
23
|
+
* | Local | Remote | Well-formed | Result |
|
|
24
|
+
* |-------|--------|-------------|-----------------|
|
|
25
|
+
* | YES | YES | YES | FullBtr |
|
|
26
|
+
* | YES | NO | - | Downgrade |
|
|
27
|
+
* | NO | YES | - | Downgrade |
|
|
28
|
+
* | NO | NO | - | StaticEphemeral |
|
|
29
|
+
* | YES | YES | NO | Reject |
|
|
30
|
+
*/
|
|
31
|
+
export function negotiateBtr(localSupports, remoteSupports, remoteWellFormed) {
|
|
32
|
+
if (localSupports && remoteSupports) {
|
|
33
|
+
return remoteWellFormed ? BtrMode.FullBtr : BtrMode.Reject;
|
|
34
|
+
}
|
|
35
|
+
if (localSupports || remoteSupports) {
|
|
36
|
+
return BtrMode.Downgrade;
|
|
37
|
+
}
|
|
38
|
+
return BtrMode.StaticEphemeral;
|
|
39
|
+
}
|
|
40
|
+
/** Returns the log token for a given BTR mode, or null if none. */
|
|
41
|
+
export function btrLogToken(mode) {
|
|
42
|
+
switch (mode) {
|
|
43
|
+
case BtrMode.Downgrade:
|
|
44
|
+
return '[BTR_DOWNGRADE]';
|
|
45
|
+
case BtrMode.Reject:
|
|
46
|
+
return '[BTR_DOWNGRADE_REJECTED]';
|
|
47
|
+
default:
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inter-transfer DH ratchet — §16.3 DH ratchet step.
|
|
3
|
+
*
|
|
4
|
+
* Uses tweetnacl.scalarMult for X25519 DH (parity with Rust x25519-dalek).
|
|
5
|
+
* Uses HKDF-SHA256 for session root key derivation after DH.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Generate a fresh X25519 keypair for the DH ratchet step.
|
|
9
|
+
*
|
|
10
|
+
* Returns { publicKey, secretKey } both 32 bytes.
|
|
11
|
+
* The secretKey should be consumed once for DH then discarded.
|
|
12
|
+
*/
|
|
13
|
+
export declare function generateRatchetKeypair(): {
|
|
14
|
+
publicKey: Uint8Array;
|
|
15
|
+
secretKey: Uint8Array;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Perform X25519 DH with a remote peer's ratchet public key.
|
|
19
|
+
*
|
|
20
|
+
* Returns the raw 32-byte DH output.
|
|
21
|
+
* Matches Rust: x25519-dalek EphemeralSecret::diffie_hellman().
|
|
22
|
+
*/
|
|
23
|
+
export declare function scalarMult(localSecretKey: Uint8Array, remotePublicKey: Uint8Array): Uint8Array;
|
|
24
|
+
/**
|
|
25
|
+
* Derive new session root key from DH ratchet step output (§16.3).
|
|
26
|
+
*
|
|
27
|
+
* new_session_root_key = HKDF-SHA256(
|
|
28
|
+
* salt = current_session_root_key,
|
|
29
|
+
* ikm = dh_output,
|
|
30
|
+
* info = "bolt-btr-dh-ratchet-v1",
|
|
31
|
+
* len = 32
|
|
32
|
+
* )
|
|
33
|
+
*/
|
|
34
|
+
export declare function deriveRatchetedSessionRoot(currentSessionRootKey: Uint8Array, dhOutput: Uint8Array): Uint8Array;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inter-transfer DH ratchet — §16.3 DH ratchet step.
|
|
3
|
+
*
|
|
4
|
+
* Uses tweetnacl.scalarMult for X25519 DH (parity with Rust x25519-dalek).
|
|
5
|
+
* Uses HKDF-SHA256 for session root key derivation after DH.
|
|
6
|
+
*/
|
|
7
|
+
import tweetnacl from 'tweetnacl';
|
|
8
|
+
import { hkdf } from '@noble/hashes/hkdf.js';
|
|
9
|
+
import { sha256 } from '@noble/hashes/sha2.js';
|
|
10
|
+
import { BTR_DH_RATCHET_INFO, BTR_KEY_LENGTH } from './constants.js';
|
|
11
|
+
const encoder = new TextEncoder();
|
|
12
|
+
/**
|
|
13
|
+
* Generate a fresh X25519 keypair for the DH ratchet step.
|
|
14
|
+
*
|
|
15
|
+
* Returns { publicKey, secretKey } both 32 bytes.
|
|
16
|
+
* The secretKey should be consumed once for DH then discarded.
|
|
17
|
+
*/
|
|
18
|
+
export function generateRatchetKeypair() {
|
|
19
|
+
// tweetnacl.box.keyPair() generates X25519 keypairs
|
|
20
|
+
const kp = tweetnacl.box.keyPair();
|
|
21
|
+
return { publicKey: kp.publicKey, secretKey: kp.secretKey };
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Perform X25519 DH with a remote peer's ratchet public key.
|
|
25
|
+
*
|
|
26
|
+
* Returns the raw 32-byte DH output.
|
|
27
|
+
* Matches Rust: x25519-dalek EphemeralSecret::diffie_hellman().
|
|
28
|
+
*/
|
|
29
|
+
export function scalarMult(localSecretKey, remotePublicKey) {
|
|
30
|
+
return tweetnacl.scalarMult(localSecretKey, remotePublicKey);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Derive new session root key from DH ratchet step output (§16.3).
|
|
34
|
+
*
|
|
35
|
+
* new_session_root_key = HKDF-SHA256(
|
|
36
|
+
* salt = current_session_root_key,
|
|
37
|
+
* ikm = dh_output,
|
|
38
|
+
* info = "bolt-btr-dh-ratchet-v1",
|
|
39
|
+
* len = 32
|
|
40
|
+
* )
|
|
41
|
+
*/
|
|
42
|
+
export function deriveRatchetedSessionRoot(currentSessionRootKey, dhOutput) {
|
|
43
|
+
return hkdf(sha256, dhOutput, currentSessionRootKey, encoder.encode(BTR_DH_RATCHET_INFO), BTR_KEY_LENGTH);
|
|
44
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replay rejection — (transfer_id, generation, chain_index) guard (§11).
|
|
3
|
+
*
|
|
4
|
+
* ORDER-BTR: chain_index must equal expected_next_index (no gaps).
|
|
5
|
+
* REPLAY-BTR: (transfer_id, ratchet_generation, chain_index) triple
|
|
6
|
+
* prevents cross-generation replay.
|
|
7
|
+
*
|
|
8
|
+
* Must match Rust bolt-btr/src/replay.rs semantics exactly.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Replay guard tracking seen (transfer_id, generation, chain_index) triples.
|
|
12
|
+
*
|
|
13
|
+
* Enforces ORDER-BTR: chain_index must be strictly monotonic per transfer
|
|
14
|
+
* (no skipped-key buffer). Also rejects cross-generation replay.
|
|
15
|
+
*/
|
|
16
|
+
export declare class ReplayGuard {
|
|
17
|
+
private seen;
|
|
18
|
+
private expected;
|
|
19
|
+
/** Begin tracking a new transfer at the given generation. Resets expected chain_index to 0. */
|
|
20
|
+
beginTransfer(transferId: Uint8Array, generation: number): void;
|
|
21
|
+
/**
|
|
22
|
+
* Check and record a (transfer_id, generation, chain_index) triple.
|
|
23
|
+
* Throws BtrError on violation.
|
|
24
|
+
*/
|
|
25
|
+
check(transferId: Uint8Array, generation: number, chainIndex: number): void;
|
|
26
|
+
/** End tracking for the current transfer. Retains seen set for cross-transfer replay detection. */
|
|
27
|
+
endTransfer(): void;
|
|
28
|
+
/** Full reset — clears all state. Used on disconnect. */
|
|
29
|
+
reset(): void;
|
|
30
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replay rejection — (transfer_id, generation, chain_index) guard (§11).
|
|
3
|
+
*
|
|
4
|
+
* ORDER-BTR: chain_index must equal expected_next_index (no gaps).
|
|
5
|
+
* REPLAY-BTR: (transfer_id, ratchet_generation, chain_index) triple
|
|
6
|
+
* prevents cross-generation replay.
|
|
7
|
+
*
|
|
8
|
+
* Must match Rust bolt-btr/src/replay.rs semantics exactly.
|
|
9
|
+
*/
|
|
10
|
+
import { ratchetStateError, ratchetChainError } from './errors.js';
|
|
11
|
+
function tripleKey(tid, generation, chainIndex) {
|
|
12
|
+
// Encode triple as a string key for Set lookup
|
|
13
|
+
const hexTid = Array.from(tid, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
14
|
+
return `${hexTid}:${generation}:${chainIndex}`;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Replay guard tracking seen (transfer_id, generation, chain_index) triples.
|
|
18
|
+
*
|
|
19
|
+
* Enforces ORDER-BTR: chain_index must be strictly monotonic per transfer
|
|
20
|
+
* (no skipped-key buffer). Also rejects cross-generation replay.
|
|
21
|
+
*/
|
|
22
|
+
export class ReplayGuard {
|
|
23
|
+
constructor() {
|
|
24
|
+
this.seen = new Set();
|
|
25
|
+
this.expected = null;
|
|
26
|
+
}
|
|
27
|
+
/** Begin tracking a new transfer at the given generation. Resets expected chain_index to 0. */
|
|
28
|
+
beginTransfer(transferId, generation) {
|
|
29
|
+
this.expected = {
|
|
30
|
+
transferId: new Uint8Array(transferId),
|
|
31
|
+
generation,
|
|
32
|
+
nextIndex: 0,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Check and record a (transfer_id, generation, chain_index) triple.
|
|
37
|
+
* Throws BtrError on violation.
|
|
38
|
+
*/
|
|
39
|
+
check(transferId, generation, chainIndex) {
|
|
40
|
+
if (this.expected === null) {
|
|
41
|
+
throw ratchetStateError('no active transfer in replay guard');
|
|
42
|
+
}
|
|
43
|
+
// Check transfer_id matches
|
|
44
|
+
if (!uint8ArrayEqual(transferId, this.expected.transferId)) {
|
|
45
|
+
throw ratchetStateError('transfer_id mismatch');
|
|
46
|
+
}
|
|
47
|
+
// Check generation matches
|
|
48
|
+
if (generation !== this.expected.generation) {
|
|
49
|
+
throw ratchetStateError(`generation mismatch: expected ${this.expected.generation}, got ${generation}`);
|
|
50
|
+
}
|
|
51
|
+
// ORDER-BTR: chain_index must equal expected next (no gaps)
|
|
52
|
+
if (chainIndex !== this.expected.nextIndex) {
|
|
53
|
+
throw ratchetChainError(`chain_index out of order: expected ${this.expected.nextIndex}, got ${chainIndex}`);
|
|
54
|
+
}
|
|
55
|
+
// REPLAY-BTR: check for duplicate triple
|
|
56
|
+
const key = tripleKey(transferId, generation, chainIndex);
|
|
57
|
+
if (this.seen.has(key)) {
|
|
58
|
+
throw ratchetChainError(`replay detected: generation=${generation}, chain_index=${chainIndex}`);
|
|
59
|
+
}
|
|
60
|
+
this.seen.add(key);
|
|
61
|
+
this.expected.nextIndex = chainIndex + 1;
|
|
62
|
+
}
|
|
63
|
+
/** End tracking for the current transfer. Retains seen set for cross-transfer replay detection. */
|
|
64
|
+
endTransfer() {
|
|
65
|
+
this.expected = null;
|
|
66
|
+
}
|
|
67
|
+
/** Full reset — clears all state. Used on disconnect. */
|
|
68
|
+
reset() {
|
|
69
|
+
this.seen.clear();
|
|
70
|
+
this.expected = null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function uint8ArrayEqual(a, b) {
|
|
74
|
+
if (a.length !== b.length)
|
|
75
|
+
return false;
|
|
76
|
+
for (let i = 0; i < a.length; i++) {
|
|
77
|
+
if (a[i] !== b[i])
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
return true;
|
|
81
|
+
}
|