@the9ines/bolt-core 0.5.1
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 +53 -0
- package/dist/constants.d.ts +26 -0
- package/dist/constants.js +26 -0
- package/dist/crypto.d.ts +31 -0
- package/dist/crypto.js +54 -0
- package/dist/encoding.d.ts +4 -0
- package/dist/encoding.js +10 -0
- package/dist/errors.d.ts +26 -0
- package/dist/errors.js +67 -0
- package/dist/hash.d.ts +12 -0
- package/dist/hash.js +25 -0
- package/dist/identity.d.ts +24 -0
- package/dist/identity.js +26 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +18 -0
- package/dist/peer-code.d.ts +20 -0
- package/dist/peer-code.js +56 -0
- package/dist/sas.d.ts +13 -0
- package/dist/sas.js +53 -0
- package/package.json +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# @the9ines/bolt-core
|
|
2
|
+
|
|
3
|
+
Core crypto primitives and utilities for the Bolt Protocol.
|
|
4
|
+
|
|
5
|
+
## Scope
|
|
6
|
+
|
|
7
|
+
This package provides transport-agnostic building blocks consumed by all Bolt ecosystem products:
|
|
8
|
+
|
|
9
|
+
- **Crypto**: NaCl box seal/open, ephemeral keypair generation
|
|
10
|
+
- **Peer codes**: Generation and validation using unambiguous base32 alphabet
|
|
11
|
+
- **Hashing**: SHA-256, file hashing, hex encoding
|
|
12
|
+
- **SAS**: Short Authentication String computation per Bolt Protocol spec
|
|
13
|
+
- **Errors**: Core error types (BoltError, EncryptionError, ConnectionError, TransferError)
|
|
14
|
+
- **Constants**: Protocol constants (nonce length, chunk size, key sizes, etc.)
|
|
15
|
+
- **Encoding**: Base64 encode/decode helpers
|
|
16
|
+
|
|
17
|
+
## What this package does NOT include
|
|
18
|
+
|
|
19
|
+
- WebRTC, WebSocket, or any transport logic
|
|
20
|
+
- Signaling or discovery
|
|
21
|
+
- UI components
|
|
22
|
+
- Handshake state machines or HELLO message handling
|
|
23
|
+
- Identity persistence or TOFU pinning
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import {
|
|
29
|
+
sealBoxPayload,
|
|
30
|
+
openBoxPayload,
|
|
31
|
+
generateEphemeralKeyPair,
|
|
32
|
+
generateSecurePeerCode,
|
|
33
|
+
computeSas,
|
|
34
|
+
DEFAULT_CHUNK_SIZE,
|
|
35
|
+
} from '@the9ines/bolt-core';
|
|
36
|
+
|
|
37
|
+
// Generate ephemeral keys for a connection
|
|
38
|
+
const myKeys = generateEphemeralKeyPair();
|
|
39
|
+
|
|
40
|
+
// Encrypt a chunk
|
|
41
|
+
const sealed = sealBoxPayload(plaintext, remotePubKey, myKeys.secretKey);
|
|
42
|
+
|
|
43
|
+
// Decrypt a chunk
|
|
44
|
+
const opened = openBoxPayload(sealed, remotePubKey, myKeys.secretKey);
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Build
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npm install
|
|
51
|
+
npm run build # tsc -> dist/
|
|
52
|
+
npm run test # vitest
|
|
53
|
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** NaCl box nonce length in bytes */
|
|
2
|
+
export declare const NONCE_LENGTH = 24;
|
|
3
|
+
/** X25519 public key length in bytes */
|
|
4
|
+
export declare const PUBLIC_KEY_LENGTH = 32;
|
|
5
|
+
/** X25519 secret key length in bytes */
|
|
6
|
+
export declare const SECRET_KEY_LENGTH = 32;
|
|
7
|
+
/** Default plaintext chunk size in bytes (16KB) */
|
|
8
|
+
export declare const DEFAULT_CHUNK_SIZE = 16384;
|
|
9
|
+
/** Peer code length in characters */
|
|
10
|
+
export declare const PEER_CODE_LENGTH = 6;
|
|
11
|
+
/** Unambiguous base32 alphabet for peer codes (no 0/O, 1/I/L) */
|
|
12
|
+
export declare const PEER_CODE_ALPHABET = "ABCDEFGHJKMNPQRSTUVWXYZ23456789";
|
|
13
|
+
/** SAS display length in hex characters */
|
|
14
|
+
export declare const SAS_LENGTH = 6;
|
|
15
|
+
/** Current Bolt Protocol version */
|
|
16
|
+
export declare const BOLT_VERSION = 1;
|
|
17
|
+
/** Transfer ID length in bytes (§14) */
|
|
18
|
+
export declare const TRANSFER_ID_LENGTH = 16;
|
|
19
|
+
/** SAS entropy in bits (§14) */
|
|
20
|
+
export declare const SAS_ENTROPY = 24;
|
|
21
|
+
/** File hash algorithm identifier (§14) */
|
|
22
|
+
export declare const FILE_HASH_ALGORITHM = "SHA-256";
|
|
23
|
+
/** File hash length in bytes (§14) */
|
|
24
|
+
export declare const FILE_HASH_LENGTH = 32;
|
|
25
|
+
/** Capability namespace prefix (§14) — all capability strings start with this */
|
|
26
|
+
export declare const CAPABILITY_NAMESPACE = "bolt.";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** NaCl box nonce length in bytes */
|
|
2
|
+
export const NONCE_LENGTH = 24;
|
|
3
|
+
/** X25519 public key length in bytes */
|
|
4
|
+
export const PUBLIC_KEY_LENGTH = 32;
|
|
5
|
+
/** X25519 secret key length in bytes */
|
|
6
|
+
export const SECRET_KEY_LENGTH = 32;
|
|
7
|
+
/** Default plaintext chunk size in bytes (16KB) */
|
|
8
|
+
export const DEFAULT_CHUNK_SIZE = 16384;
|
|
9
|
+
/** Peer code length in characters */
|
|
10
|
+
export const PEER_CODE_LENGTH = 6;
|
|
11
|
+
/** Unambiguous base32 alphabet for peer codes (no 0/O, 1/I/L) */
|
|
12
|
+
export const PEER_CODE_ALPHABET = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789';
|
|
13
|
+
/** SAS display length in hex characters */
|
|
14
|
+
export const SAS_LENGTH = 6;
|
|
15
|
+
/** Current Bolt Protocol version */
|
|
16
|
+
export const BOLT_VERSION = 1;
|
|
17
|
+
/** Transfer ID length in bytes (§14) */
|
|
18
|
+
export const TRANSFER_ID_LENGTH = 16;
|
|
19
|
+
/** SAS entropy in bits (§14) */
|
|
20
|
+
export const SAS_ENTROPY = 24;
|
|
21
|
+
/** File hash algorithm identifier (§14) */
|
|
22
|
+
export const FILE_HASH_ALGORITHM = 'SHA-256';
|
|
23
|
+
/** File hash length in bytes (§14) */
|
|
24
|
+
export const FILE_HASH_LENGTH = 32;
|
|
25
|
+
/** Capability namespace prefix (§14) — all capability strings start with this */
|
|
26
|
+
export const CAPABILITY_NAMESPACE = 'bolt.';
|
package/dist/crypto.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate a fresh ephemeral X25519 keypair for a single connection.
|
|
3
|
+
* Discard after session ends.
|
|
4
|
+
*/
|
|
5
|
+
export declare function generateEphemeralKeyPair(): {
|
|
6
|
+
publicKey: Uint8Array;
|
|
7
|
+
secretKey: Uint8Array;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Seal a plaintext payload using NaCl box (XSalsa20-Poly1305).
|
|
11
|
+
*
|
|
12
|
+
* Wire format: base64(nonce || ciphertext)
|
|
13
|
+
* This matches the exact format used by all current product repos.
|
|
14
|
+
*
|
|
15
|
+
* @param plaintext - Raw bytes to encrypt
|
|
16
|
+
* @param remotePublicKey - Receiver's ephemeral public key (32 bytes)
|
|
17
|
+
* @param senderSecretKey - Sender's ephemeral secret key (32 bytes)
|
|
18
|
+
* @returns base64-encoded string of nonce + ciphertext
|
|
19
|
+
*/
|
|
20
|
+
export declare function sealBoxPayload(plaintext: Uint8Array, remotePublicKey: Uint8Array, senderSecretKey: Uint8Array): string;
|
|
21
|
+
/**
|
|
22
|
+
* Open a sealed payload using NaCl box.open.
|
|
23
|
+
*
|
|
24
|
+
* Expects wire format: base64(nonce || ciphertext)
|
|
25
|
+
*
|
|
26
|
+
* @param sealed - base64-encoded string from sealBoxPayload
|
|
27
|
+
* @param senderPublicKey - Sender's ephemeral public key (32 bytes)
|
|
28
|
+
* @param receiverSecretKey - Receiver's ephemeral secret key (32 bytes)
|
|
29
|
+
* @returns Decrypted plaintext bytes
|
|
30
|
+
*/
|
|
31
|
+
export declare function openBoxPayload(sealed: string, senderPublicKey: Uint8Array, receiverSecretKey: Uint8Array): Uint8Array;
|
package/dist/crypto.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import tweetnacl from 'tweetnacl';
|
|
2
|
+
const { box, randomBytes } = tweetnacl;
|
|
3
|
+
import { toBase64, fromBase64 } from './encoding.js';
|
|
4
|
+
import { EncryptionError } from './errors.js';
|
|
5
|
+
/**
|
|
6
|
+
* Generate a fresh ephemeral X25519 keypair for a single connection.
|
|
7
|
+
* Discard after session ends.
|
|
8
|
+
*/
|
|
9
|
+
export function generateEphemeralKeyPair() {
|
|
10
|
+
return box.keyPair();
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Seal a plaintext payload using NaCl box (XSalsa20-Poly1305).
|
|
14
|
+
*
|
|
15
|
+
* Wire format: base64(nonce || ciphertext)
|
|
16
|
+
* This matches the exact format used by all current product repos.
|
|
17
|
+
*
|
|
18
|
+
* @param plaintext - Raw bytes to encrypt
|
|
19
|
+
* @param remotePublicKey - Receiver's ephemeral public key (32 bytes)
|
|
20
|
+
* @param senderSecretKey - Sender's ephemeral secret key (32 bytes)
|
|
21
|
+
* @returns base64-encoded string of nonce + ciphertext
|
|
22
|
+
*/
|
|
23
|
+
export function sealBoxPayload(plaintext, remotePublicKey, senderSecretKey) {
|
|
24
|
+
const nonce = randomBytes(box.nonceLength);
|
|
25
|
+
const encrypted = box(plaintext, nonce, remotePublicKey, senderSecretKey);
|
|
26
|
+
if (!encrypted)
|
|
27
|
+
throw new EncryptionError('Encryption returned null');
|
|
28
|
+
const combined = new Uint8Array(nonce.length + encrypted.length);
|
|
29
|
+
combined.set(nonce);
|
|
30
|
+
combined.set(encrypted, nonce.length);
|
|
31
|
+
return toBase64(combined);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Open a sealed payload using NaCl box.open.
|
|
35
|
+
*
|
|
36
|
+
* Expects wire format: base64(nonce || ciphertext)
|
|
37
|
+
*
|
|
38
|
+
* @param sealed - base64-encoded string from sealBoxPayload
|
|
39
|
+
* @param senderPublicKey - Sender's ephemeral public key (32 bytes)
|
|
40
|
+
* @param receiverSecretKey - Receiver's ephemeral secret key (32 bytes)
|
|
41
|
+
* @returns Decrypted plaintext bytes
|
|
42
|
+
*/
|
|
43
|
+
export function openBoxPayload(sealed, senderPublicKey, receiverSecretKey) {
|
|
44
|
+
const data = fromBase64(sealed);
|
|
45
|
+
if (data.length < box.nonceLength) {
|
|
46
|
+
throw new EncryptionError('Sealed payload too short');
|
|
47
|
+
}
|
|
48
|
+
const nonce = data.slice(0, box.nonceLength);
|
|
49
|
+
const ciphertext = data.slice(box.nonceLength);
|
|
50
|
+
const decrypted = box.open(ciphertext, nonce, senderPublicKey, receiverSecretKey);
|
|
51
|
+
if (!decrypted)
|
|
52
|
+
throw new EncryptionError('Decryption failed');
|
|
53
|
+
return decrypted;
|
|
54
|
+
}
|
package/dist/encoding.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import tweetnacl_util from 'tweetnacl-util';
|
|
2
|
+
const { encodeBase64, decodeBase64 } = tweetnacl_util;
|
|
3
|
+
/** Encode a Uint8Array to a base64 string */
|
|
4
|
+
export function toBase64(data) {
|
|
5
|
+
return encodeBase64(data);
|
|
6
|
+
}
|
|
7
|
+
/** Decode a base64 string to a Uint8Array */
|
|
8
|
+
export function fromBase64(base64) {
|
|
9
|
+
return decodeBase64(base64);
|
|
10
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export declare class BoltError extends Error {
|
|
2
|
+
details?: unknown | undefined;
|
|
3
|
+
constructor(message: string, details?: unknown | undefined);
|
|
4
|
+
}
|
|
5
|
+
export declare class EncryptionError extends BoltError {
|
|
6
|
+
constructor(message: string, details?: unknown);
|
|
7
|
+
}
|
|
8
|
+
export declare class ConnectionError extends BoltError {
|
|
9
|
+
constructor(message: string, details?: unknown);
|
|
10
|
+
}
|
|
11
|
+
export declare class TransferError extends BoltError {
|
|
12
|
+
constructor(message: string, details?: unknown);
|
|
13
|
+
}
|
|
14
|
+
export declare class IntegrityError extends BoltError {
|
|
15
|
+
constructor(message?: string);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Canonical wire error code registry — 22 codes (11 PROTOCOL + 11 ENFORCEMENT).
|
|
19
|
+
* Every error frame sent on the wire MUST use a code from this array.
|
|
20
|
+
* Implementations MUST reject inbound error frames carrying codes not listed here.
|
|
21
|
+
*/
|
|
22
|
+
export declare const WIRE_ERROR_CODES: readonly ["VERSION_MISMATCH", "ENCRYPTION_FAILED", "INTEGRITY_FAILED", "REPLAY_DETECTED", "TRANSFER_FAILED", "LIMIT_EXCEEDED", "CONNECTION_LOST", "PEER_NOT_FOUND", "ALREADY_CONNECTED", "INVALID_STATE", "KEY_MISMATCH", "DUPLICATE_HELLO", "ENVELOPE_REQUIRED", "ENVELOPE_UNNEGOTIATED", "ENVELOPE_DECRYPT_FAIL", "ENVELOPE_INVALID", "HELLO_PARSE_ERROR", "HELLO_DECRYPT_FAIL", "HELLO_SCHEMA_ERROR", "INVALID_MESSAGE", "UNKNOWN_MESSAGE_TYPE", "PROTOCOL_VIOLATION"];
|
|
23
|
+
/** A valid wire error code string from PROTOCOL.md §10. */
|
|
24
|
+
export type WireErrorCode = (typeof WIRE_ERROR_CODES)[number];
|
|
25
|
+
/** Type guard: returns true if `x` is a canonical wire error code. */
|
|
26
|
+
export declare function isValidWireErrorCode(x: unknown): x is WireErrorCode;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export class BoltError extends Error {
|
|
2
|
+
constructor(message, details) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.details = details;
|
|
5
|
+
this.name = 'BoltError';
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export class EncryptionError extends BoltError {
|
|
9
|
+
constructor(message, details) {
|
|
10
|
+
super(message, details);
|
|
11
|
+
this.name = 'EncryptionError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export class ConnectionError extends BoltError {
|
|
15
|
+
constructor(message, details) {
|
|
16
|
+
super(message, details);
|
|
17
|
+
this.name = 'ConnectionError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export class TransferError extends BoltError {
|
|
21
|
+
constructor(message, details) {
|
|
22
|
+
super(message, details);
|
|
23
|
+
this.name = 'TransferError';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export class IntegrityError extends BoltError {
|
|
27
|
+
constructor(message = 'File integrity check failed') {
|
|
28
|
+
super(message);
|
|
29
|
+
this.name = 'IntegrityError';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// ── Wire Error Code Registry (PROTOCOL.md §10, v0.1.3-spec) ──────────
|
|
33
|
+
/**
|
|
34
|
+
* Canonical wire error code registry — 22 codes (11 PROTOCOL + 11 ENFORCEMENT).
|
|
35
|
+
* Every error frame sent on the wire MUST use a code from this array.
|
|
36
|
+
* Implementations MUST reject inbound error frames carrying codes not listed here.
|
|
37
|
+
*/
|
|
38
|
+
export const WIRE_ERROR_CODES = [
|
|
39
|
+
// PROTOCOL class (11)
|
|
40
|
+
'VERSION_MISMATCH',
|
|
41
|
+
'ENCRYPTION_FAILED',
|
|
42
|
+
'INTEGRITY_FAILED',
|
|
43
|
+
'REPLAY_DETECTED',
|
|
44
|
+
'TRANSFER_FAILED',
|
|
45
|
+
'LIMIT_EXCEEDED',
|
|
46
|
+
'CONNECTION_LOST',
|
|
47
|
+
'PEER_NOT_FOUND',
|
|
48
|
+
'ALREADY_CONNECTED',
|
|
49
|
+
'INVALID_STATE',
|
|
50
|
+
'KEY_MISMATCH',
|
|
51
|
+
// ENFORCEMENT class (11)
|
|
52
|
+
'DUPLICATE_HELLO',
|
|
53
|
+
'ENVELOPE_REQUIRED',
|
|
54
|
+
'ENVELOPE_UNNEGOTIATED',
|
|
55
|
+
'ENVELOPE_DECRYPT_FAIL',
|
|
56
|
+
'ENVELOPE_INVALID',
|
|
57
|
+
'HELLO_PARSE_ERROR',
|
|
58
|
+
'HELLO_DECRYPT_FAIL',
|
|
59
|
+
'HELLO_SCHEMA_ERROR',
|
|
60
|
+
'INVALID_MESSAGE',
|
|
61
|
+
'UNKNOWN_MESSAGE_TYPE',
|
|
62
|
+
'PROTOCOL_VIOLATION',
|
|
63
|
+
];
|
|
64
|
+
/** Type guard: returns true if `x` is a canonical wire error code. */
|
|
65
|
+
export function isValidWireErrorCode(x) {
|
|
66
|
+
return typeof x === 'string' && WIRE_ERROR_CODES.includes(x);
|
|
67
|
+
}
|
package/dist/hash.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compute SHA-256 hash of data.
|
|
3
|
+
*/
|
|
4
|
+
export declare function sha256(data: ArrayBuffer | Uint8Array): Promise<ArrayBuffer>;
|
|
5
|
+
/**
|
|
6
|
+
* Convert ArrayBuffer to hex string.
|
|
7
|
+
*/
|
|
8
|
+
export declare function bufferToHex(buffer: ArrayBuffer): string;
|
|
9
|
+
/**
|
|
10
|
+
* Compute SHA-256 hash of a File or Blob and return hex string.
|
|
11
|
+
*/
|
|
12
|
+
export declare function hashFile(file: Blob): Promise<string>;
|
package/dist/hash.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compute SHA-256 hash of data.
|
|
3
|
+
*/
|
|
4
|
+
export async function sha256(data) {
|
|
5
|
+
if (data instanceof Uint8Array) {
|
|
6
|
+
return await crypto.subtle.digest('SHA-256', data);
|
|
7
|
+
}
|
|
8
|
+
return await crypto.subtle.digest('SHA-256', data);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Convert ArrayBuffer to hex string.
|
|
12
|
+
*/
|
|
13
|
+
export function bufferToHex(buffer) {
|
|
14
|
+
return Array.from(new Uint8Array(buffer))
|
|
15
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
16
|
+
.join('');
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Compute SHA-256 hash of a File or Blob and return hex string.
|
|
20
|
+
*/
|
|
21
|
+
export async function hashFile(file) {
|
|
22
|
+
const buffer = await file.arrayBuffer();
|
|
23
|
+
const hash = await sha256(buffer);
|
|
24
|
+
return bufferToHex(hash);
|
|
25
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { BoltError } from './errors.js';
|
|
2
|
+
/** Long-lived X25519 identity keypair. Persisted by the transport layer. */
|
|
3
|
+
export interface IdentityKeyPair {
|
|
4
|
+
publicKey: Uint8Array;
|
|
5
|
+
secretKey: Uint8Array;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Generate a persistent identity keypair (X25519).
|
|
9
|
+
*
|
|
10
|
+
* Identity keys are long-lived and stored by the transport layer.
|
|
11
|
+
* They MUST NOT be sent through the signaling server — identity
|
|
12
|
+
* material travels only inside encrypted DataChannel messages (HELLO).
|
|
13
|
+
*/
|
|
14
|
+
export declare function generateIdentityKeyPair(): IdentityKeyPair;
|
|
15
|
+
/**
|
|
16
|
+
* Thrown when a peer's identity public key does not match a previously
|
|
17
|
+
* pinned value. This is a TOFU violation — the session MUST be aborted.
|
|
18
|
+
*/
|
|
19
|
+
export declare class KeyMismatchError extends BoltError {
|
|
20
|
+
readonly peerCode: string;
|
|
21
|
+
readonly expected: Uint8Array;
|
|
22
|
+
readonly received: Uint8Array;
|
|
23
|
+
constructor(peerCode: string, expected: Uint8Array, received: Uint8Array);
|
|
24
|
+
}
|
package/dist/identity.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import tweetnacl from 'tweetnacl';
|
|
2
|
+
const { box } = tweetnacl;
|
|
3
|
+
import { BoltError } from './errors.js';
|
|
4
|
+
/**
|
|
5
|
+
* Generate a persistent identity keypair (X25519).
|
|
6
|
+
*
|
|
7
|
+
* Identity keys are long-lived and stored by the transport layer.
|
|
8
|
+
* They MUST NOT be sent through the signaling server — identity
|
|
9
|
+
* material travels only inside encrypted DataChannel messages (HELLO).
|
|
10
|
+
*/
|
|
11
|
+
export function generateIdentityKeyPair() {
|
|
12
|
+
return box.keyPair();
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Thrown when a peer's identity public key does not match a previously
|
|
16
|
+
* pinned value. This is a TOFU violation — the session MUST be aborted.
|
|
17
|
+
*/
|
|
18
|
+
export class KeyMismatchError extends BoltError {
|
|
19
|
+
constructor(peerCode, expected, received) {
|
|
20
|
+
super(`Identity key mismatch for peer ${peerCode}`);
|
|
21
|
+
this.peerCode = peerCode;
|
|
22
|
+
this.expected = expected;
|
|
23
|
+
this.received = received;
|
|
24
|
+
this.name = 'KeyMismatchError';
|
|
25
|
+
}
|
|
26
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { NONCE_LENGTH, PUBLIC_KEY_LENGTH, SECRET_KEY_LENGTH, DEFAULT_CHUNK_SIZE, PEER_CODE_LENGTH, PEER_CODE_ALPHABET, SAS_LENGTH, BOLT_VERSION, TRANSFER_ID_LENGTH, SAS_ENTROPY, FILE_HASH_ALGORITHM, FILE_HASH_LENGTH, CAPABILITY_NAMESPACE, } from './constants.js';
|
|
2
|
+
export { toBase64, fromBase64 } from './encoding.js';
|
|
3
|
+
export { generateEphemeralKeyPair, sealBoxPayload, openBoxPayload } from './crypto.js';
|
|
4
|
+
export { generateSecurePeerCode, generateLongPeerCode, isValidPeerCode, normalizePeerCode, } from './peer-code.js';
|
|
5
|
+
export { sha256, bufferToHex, hashFile } from './hash.js';
|
|
6
|
+
export { computeSas } from './sas.js';
|
|
7
|
+
export { generateIdentityKeyPair, KeyMismatchError } from './identity.js';
|
|
8
|
+
export type { IdentityKeyPair } from './identity.js';
|
|
9
|
+
export { BoltError, EncryptionError, ConnectionError, TransferError, IntegrityError } from './errors.js';
|
|
10
|
+
export { WIRE_ERROR_CODES, isValidWireErrorCode } from './errors.js';
|
|
11
|
+
export type { WireErrorCode } from './errors.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Constants (§14)
|
|
2
|
+
export { NONCE_LENGTH, PUBLIC_KEY_LENGTH, SECRET_KEY_LENGTH, DEFAULT_CHUNK_SIZE, PEER_CODE_LENGTH, PEER_CODE_ALPHABET, SAS_LENGTH, BOLT_VERSION, TRANSFER_ID_LENGTH, SAS_ENTROPY, FILE_HASH_ALGORITHM, FILE_HASH_LENGTH, CAPABILITY_NAMESPACE, } from './constants.js';
|
|
3
|
+
// Encoding
|
|
4
|
+
export { toBase64, fromBase64 } from './encoding.js';
|
|
5
|
+
// Crypto primitives
|
|
6
|
+
export { generateEphemeralKeyPair, sealBoxPayload, openBoxPayload } from './crypto.js';
|
|
7
|
+
// Peer codes
|
|
8
|
+
export { generateSecurePeerCode, generateLongPeerCode, isValidPeerCode, normalizePeerCode, } from './peer-code.js';
|
|
9
|
+
// Hashing
|
|
10
|
+
export { sha256, bufferToHex, hashFile } from './hash.js';
|
|
11
|
+
// SAS
|
|
12
|
+
export { computeSas } from './sas.js';
|
|
13
|
+
// Identity
|
|
14
|
+
export { generateIdentityKeyPair, KeyMismatchError } from './identity.js';
|
|
15
|
+
// Errors
|
|
16
|
+
export { BoltError, EncryptionError, ConnectionError, TransferError, IntegrityError } from './errors.js';
|
|
17
|
+
// Wire error code registry (PROTOCOL.md §10)
|
|
18
|
+
export { WIRE_ERROR_CODES, isValidWireErrorCode } from './errors.js';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate a cryptographically secure 6-character peer code.
|
|
3
|
+
* Uses rejection sampling to eliminate modulo bias.
|
|
4
|
+
*/
|
|
5
|
+
export declare function generateSecurePeerCode(): string;
|
|
6
|
+
/**
|
|
7
|
+
* Generate a longer peer code with dash separator.
|
|
8
|
+
* Format: XXXX-XXXX (~40 bits of entropy)
|
|
9
|
+
* Uses rejection sampling to eliminate modulo bias.
|
|
10
|
+
*/
|
|
11
|
+
export declare function generateLongPeerCode(): string;
|
|
12
|
+
/**
|
|
13
|
+
* Validate peer code format.
|
|
14
|
+
* Accepts 6-char or 8-char (with optional dash) codes using the unambiguous alphabet.
|
|
15
|
+
*/
|
|
16
|
+
export declare function isValidPeerCode(code: string): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Normalize peer code for comparison (remove dashes, uppercase).
|
|
19
|
+
*/
|
|
20
|
+
export declare function normalizePeerCode(code: string): string;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { PEER_CODE_ALPHABET } from './constants.js';
|
|
2
|
+
// Rejection sampling threshold: largest multiple of N that fits in a byte.
|
|
3
|
+
// N = 31 (PEER_CODE_ALPHABET.length), MAX = floor(256/31) * 31 = 248.
|
|
4
|
+
// Bytes >= MAX are discarded to eliminate modulo bias.
|
|
5
|
+
const REJECTION_MAX = Math.floor(256 / PEER_CODE_ALPHABET.length) * PEER_CODE_ALPHABET.length;
|
|
6
|
+
/**
|
|
7
|
+
* Fill `out` with `count` unbiased alphabet indices via rejection sampling.
|
|
8
|
+
* Bytes >= REJECTION_MAX are discarded; survivors use byte % N.
|
|
9
|
+
*/
|
|
10
|
+
function fillUnbiased(count) {
|
|
11
|
+
const N = PEER_CODE_ALPHABET.length;
|
|
12
|
+
const result = [];
|
|
13
|
+
while (result.length < count) {
|
|
14
|
+
const batch = new Uint8Array(count - result.length + 4); // small over-request
|
|
15
|
+
crypto.getRandomValues(batch);
|
|
16
|
+
for (let i = 0; i < batch.length && result.length < count; i++) {
|
|
17
|
+
if (batch[i] < REJECTION_MAX) {
|
|
18
|
+
result.push(PEER_CODE_ALPHABET[batch[i] % N]);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Generate a cryptographically secure 6-character peer code.
|
|
26
|
+
* Uses rejection sampling to eliminate modulo bias.
|
|
27
|
+
*/
|
|
28
|
+
export function generateSecurePeerCode() {
|
|
29
|
+
return fillUnbiased(6).join('');
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Generate a longer peer code with dash separator.
|
|
33
|
+
* Format: XXXX-XXXX (~40 bits of entropy)
|
|
34
|
+
* Uses rejection sampling to eliminate modulo bias.
|
|
35
|
+
*/
|
|
36
|
+
export function generateLongPeerCode() {
|
|
37
|
+
const chars = fillUnbiased(8);
|
|
38
|
+
return chars.slice(0, 4).join('') + '-' + chars.slice(4).join('');
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Validate peer code format.
|
|
42
|
+
* Accepts 6-char or 8-char (with optional dash) codes using the unambiguous alphabet.
|
|
43
|
+
*/
|
|
44
|
+
export function isValidPeerCode(code) {
|
|
45
|
+
const normalized = code.replace(/-/g, '').toUpperCase();
|
|
46
|
+
if (normalized.length !== 6 && normalized.length !== 8) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
return normalized.split('').every(char => PEER_CODE_ALPHABET.includes(char));
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Normalize peer code for comparison (remove dashes, uppercase).
|
|
53
|
+
*/
|
|
54
|
+
export function normalizePeerCode(code) {
|
|
55
|
+
return code.replace(/-/g, '').toUpperCase();
|
|
56
|
+
}
|
package/dist/sas.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compute a 6-character SAS (Short Authentication String) per PROTOCOL.md.
|
|
3
|
+
*
|
|
4
|
+
* SAS_input = SHA-256( sort32(identityA, identityB) || sort32(ephemeralA, ephemeralB) )
|
|
5
|
+
* Display first 6 hex chars uppercase.
|
|
6
|
+
*
|
|
7
|
+
* @param identityA - Raw 32-byte identity public key of peer A
|
|
8
|
+
* @param identityB - Raw 32-byte identity public key of peer B
|
|
9
|
+
* @param ephemeralA - Raw 32-byte ephemeral public key of peer A
|
|
10
|
+
* @param ephemeralB - Raw 32-byte ephemeral public key of peer B
|
|
11
|
+
* @returns 6-character uppercase hex string (24 bits of entropy)
|
|
12
|
+
*/
|
|
13
|
+
export declare function computeSas(identityA: Uint8Array, identityB: Uint8Array, ephemeralA: Uint8Array, ephemeralB: Uint8Array): Promise<string>;
|
package/dist/sas.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { sha256, bufferToHex } from './hash.js';
|
|
2
|
+
import { PUBLIC_KEY_LENGTH, SAS_LENGTH } from './constants.js';
|
|
3
|
+
// CANONICAL: computeSas() is the ONLY SAS implementation in the Bolt ecosystem.
|
|
4
|
+
// SAS verification is not yet surfaced in products. No SAS logic may exist in
|
|
5
|
+
// transport or product packages. See scripts/verify-no-shadow-sas.sh.
|
|
6
|
+
/**
|
|
7
|
+
* Lexicographically sort two 32-byte values and concatenate them.
|
|
8
|
+
*/
|
|
9
|
+
function sort32(a, b) {
|
|
10
|
+
for (let i = 0; i < a.length; i++) {
|
|
11
|
+
if (a[i] !== b[i]) {
|
|
12
|
+
const first = a[i] < b[i] ? a : b;
|
|
13
|
+
const second = a[i] < b[i] ? b : a;
|
|
14
|
+
const result = new Uint8Array(first.length + second.length);
|
|
15
|
+
result.set(first);
|
|
16
|
+
result.set(second, first.length);
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
// Keys are identical — concatenate as-is
|
|
21
|
+
const result = new Uint8Array(a.length + b.length);
|
|
22
|
+
result.set(a);
|
|
23
|
+
result.set(b, a.length);
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Compute a 6-character SAS (Short Authentication String) per PROTOCOL.md.
|
|
28
|
+
*
|
|
29
|
+
* SAS_input = SHA-256( sort32(identityA, identityB) || sort32(ephemeralA, ephemeralB) )
|
|
30
|
+
* Display first 6 hex chars uppercase.
|
|
31
|
+
*
|
|
32
|
+
* @param identityA - Raw 32-byte identity public key of peer A
|
|
33
|
+
* @param identityB - Raw 32-byte identity public key of peer B
|
|
34
|
+
* @param ephemeralA - Raw 32-byte ephemeral public key of peer A
|
|
35
|
+
* @param ephemeralB - Raw 32-byte ephemeral public key of peer B
|
|
36
|
+
* @returns 6-character uppercase hex string (24 bits of entropy)
|
|
37
|
+
*/
|
|
38
|
+
export async function computeSas(identityA, identityB, ephemeralA, ephemeralB) {
|
|
39
|
+
if (identityA.length !== PUBLIC_KEY_LENGTH || identityB.length !== PUBLIC_KEY_LENGTH) {
|
|
40
|
+
throw new Error(`Identity keys must be ${PUBLIC_KEY_LENGTH} bytes`);
|
|
41
|
+
}
|
|
42
|
+
if (ephemeralA.length !== PUBLIC_KEY_LENGTH || ephemeralB.length !== PUBLIC_KEY_LENGTH) {
|
|
43
|
+
throw new Error(`Ephemeral keys must be ${PUBLIC_KEY_LENGTH} bytes`);
|
|
44
|
+
}
|
|
45
|
+
const sortedIdentity = sort32(identityA, identityB);
|
|
46
|
+
const sortedEphemeral = sort32(ephemeralA, ephemeralB);
|
|
47
|
+
const combined = new Uint8Array(sortedIdentity.length + sortedEphemeral.length);
|
|
48
|
+
combined.set(sortedIdentity);
|
|
49
|
+
combined.set(sortedEphemeral, sortedIdentity.length);
|
|
50
|
+
const hash = await sha256(combined);
|
|
51
|
+
const hex = bufferToHex(hash);
|
|
52
|
+
return hex.substring(0, SAS_LENGTH).toUpperCase();
|
|
53
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@the9ines/bolt-core",
|
|
3
|
+
"version": "0.5.1",
|
|
4
|
+
"description": "Bolt Protocol core crypto primitives and utilities",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"test": "vitest run",
|
|
17
|
+
"audit-exports": "node scripts/audit-exports.mjs",
|
|
18
|
+
"generate-vectors": "node scripts/print-test-vectors.mjs",
|
|
19
|
+
"check-vectors": "node scripts/print-test-vectors.mjs --check",
|
|
20
|
+
"prepublishOnly": "npm run build"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"tweetnacl": "^1.0.3",
|
|
24
|
+
"tweetnacl-util": "^0.15.1"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"typescript": "^5.5.3",
|
|
28
|
+
"vitest": "^4.0.18"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist"
|
|
32
|
+
],
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "https://github.com/the9ines/bolt-core-sdk.git"
|
|
39
|
+
},
|
|
40
|
+
"license": "MIT"
|
|
41
|
+
}
|