@blerpc/protocol-rn 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/LICENSE +13 -0
- package/dist/commandPacket.d.ts +20 -0
- package/dist/commandPacket.js +80 -0
- package/dist/commandPacket.js.map +1 -0
- package/dist/container.d.ts +26 -0
- package/dist/container.js +104 -0
- package/dist/container.js.map +1 -0
- package/dist/containerAssembler.d.ts +11 -0
- package/dist/containerAssembler.js +64 -0
- package/dist/containerAssembler.js.map +1 -0
- package/dist/containerSplitter.d.ts +17 -0
- package/dist/containerSplitter.js +70 -0
- package/dist/containerSplitter.js.map +1 -0
- package/dist/containerTypes.d.ts +28 -0
- package/dist/containerTypes.js +71 -0
- package/dist/containerTypes.js.map +1 -0
- package/dist/controlContainers.d.ts +35 -0
- package/dist/controlContainers.js +112 -0
- package/dist/controlContainers.js.map +1 -0
- package/dist/crypto.d.ts +149 -0
- package/dist/crypto.js +392 -0
- package/dist/crypto.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +52 -0
- package/dist/index.js.map +1 -0
- package/package.json +46 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Factory functions for control containers.
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.makeTimeoutRequest = makeTimeoutRequest;
|
|
5
|
+
exports.makeTimeoutResponse = makeTimeoutResponse;
|
|
6
|
+
exports.makeStreamEndC2P = makeStreamEndC2P;
|
|
7
|
+
exports.makeStreamEndP2C = makeStreamEndP2C;
|
|
8
|
+
exports.makeCapabilitiesRequest = makeCapabilitiesRequest;
|
|
9
|
+
exports.makeCapabilitiesResponse = makeCapabilitiesResponse;
|
|
10
|
+
exports.makeErrorResponse = makeErrorResponse;
|
|
11
|
+
exports.makeKeyExchange = makeKeyExchange;
|
|
12
|
+
const container_1 = require("./container");
|
|
13
|
+
const containerTypes_1 = require("./containerTypes");
|
|
14
|
+
/** Create a timeout request control container (Central -> Peripheral). */
|
|
15
|
+
function makeTimeoutRequest(transactionId, sequenceNumber = 0) {
|
|
16
|
+
return new container_1.Container({
|
|
17
|
+
transactionId,
|
|
18
|
+
sequenceNumber,
|
|
19
|
+
containerType: containerTypes_1.ContainerType.CONTROL,
|
|
20
|
+
controlCmd: containerTypes_1.ControlCmd.TIMEOUT,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/** Create a timeout response control container (Peripheral -> Central). */
|
|
24
|
+
function makeTimeoutResponse(transactionId, timeoutMs, sequenceNumber = 0) {
|
|
25
|
+
const payload = new Uint8Array(2);
|
|
26
|
+
const view = new DataView(payload.buffer);
|
|
27
|
+
view.setUint16(0, timeoutMs, true);
|
|
28
|
+
return new container_1.Container({
|
|
29
|
+
transactionId,
|
|
30
|
+
sequenceNumber,
|
|
31
|
+
containerType: containerTypes_1.ContainerType.CONTROL,
|
|
32
|
+
controlCmd: containerTypes_1.ControlCmd.TIMEOUT,
|
|
33
|
+
payload,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/** Create stream end container (Central -> Peripheral). */
|
|
37
|
+
function makeStreamEndC2P(transactionId, sequenceNumber = 0) {
|
|
38
|
+
return new container_1.Container({
|
|
39
|
+
transactionId,
|
|
40
|
+
sequenceNumber,
|
|
41
|
+
containerType: containerTypes_1.ContainerType.CONTROL,
|
|
42
|
+
controlCmd: containerTypes_1.ControlCmd.STREAM_END_C2P,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/** Create stream end container (Peripheral -> Central). */
|
|
46
|
+
function makeStreamEndP2C(transactionId, sequenceNumber = 0) {
|
|
47
|
+
return new container_1.Container({
|
|
48
|
+
transactionId,
|
|
49
|
+
sequenceNumber,
|
|
50
|
+
containerType: containerTypes_1.ContainerType.CONTROL,
|
|
51
|
+
controlCmd: containerTypes_1.ControlCmd.STREAM_END_P2C,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Create a capabilities request control container (Central -> Peripheral).
|
|
56
|
+
*
|
|
57
|
+
* 6-byte payload: [max_req:u16LE][max_resp:u16LE][flags:u16LE]
|
|
58
|
+
*/
|
|
59
|
+
function makeCapabilitiesRequest(transactionId, options = {}) {
|
|
60
|
+
const payload = new Uint8Array(6);
|
|
61
|
+
const view = new DataView(payload.buffer);
|
|
62
|
+
view.setUint16(0, options.maxRequestPayloadSize ?? 0, true);
|
|
63
|
+
view.setUint16(2, options.maxResponsePayloadSize ?? 0, true);
|
|
64
|
+
view.setUint16(4, options.flags ?? 0, true);
|
|
65
|
+
return new container_1.Container({
|
|
66
|
+
transactionId,
|
|
67
|
+
sequenceNumber: options.sequenceNumber ?? 0,
|
|
68
|
+
containerType: containerTypes_1.ContainerType.CONTROL,
|
|
69
|
+
controlCmd: containerTypes_1.ControlCmd.CAPABILITIES,
|
|
70
|
+
payload,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Create a capabilities response control container (Peripheral -> Central).
|
|
75
|
+
*
|
|
76
|
+
* 6-byte payload: [max_req:u16LE][max_resp:u16LE][flags:u16LE]
|
|
77
|
+
*/
|
|
78
|
+
function makeCapabilitiesResponse(transactionId, options) {
|
|
79
|
+
const payload = new Uint8Array(6);
|
|
80
|
+
const view = new DataView(payload.buffer);
|
|
81
|
+
view.setUint16(0, options.maxRequestPayloadSize, true);
|
|
82
|
+
view.setUint16(2, options.maxResponsePayloadSize, true);
|
|
83
|
+
view.setUint16(4, options.flags ?? 0, true);
|
|
84
|
+
return new container_1.Container({
|
|
85
|
+
transactionId,
|
|
86
|
+
sequenceNumber: options.sequenceNumber ?? 0,
|
|
87
|
+
containerType: containerTypes_1.ContainerType.CONTROL,
|
|
88
|
+
controlCmd: containerTypes_1.ControlCmd.CAPABILITIES,
|
|
89
|
+
payload,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/** Create an error control container (Peripheral -> Central). */
|
|
93
|
+
function makeErrorResponse(transactionId, errorCode, sequenceNumber = 0) {
|
|
94
|
+
return new container_1.Container({
|
|
95
|
+
transactionId,
|
|
96
|
+
sequenceNumber,
|
|
97
|
+
containerType: containerTypes_1.ContainerType.CONTROL,
|
|
98
|
+
controlCmd: containerTypes_1.ControlCmd.ERROR,
|
|
99
|
+
payload: new Uint8Array([errorCode]),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
/** Create a key exchange control container. */
|
|
103
|
+
function makeKeyExchange(transactionId, payload, sequenceNumber = 0) {
|
|
104
|
+
return new container_1.Container({
|
|
105
|
+
transactionId,
|
|
106
|
+
sequenceNumber,
|
|
107
|
+
containerType: containerTypes_1.ContainerType.CONTROL,
|
|
108
|
+
controlCmd: containerTypes_1.ControlCmd.KEY_EXCHANGE,
|
|
109
|
+
payload,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=controlContainers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"controlContainers.js","sourceRoot":"","sources":["../src/controlContainers.ts"],"names":[],"mappings":"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"}
|
package/dist/crypto.d.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
export declare const DIRECTION_C2P = 0;
|
|
2
|
+
export declare const DIRECTION_P2C = 1;
|
|
3
|
+
export declare const CONFIRM_CENTRAL: NodeJS.NonSharedUint8Array;
|
|
4
|
+
export declare const CONFIRM_PERIPHERAL: NodeJS.NonSharedUint8Array;
|
|
5
|
+
export declare const KEY_EXCHANGE_STEP1 = 1;
|
|
6
|
+
export declare const KEY_EXCHANGE_STEP2 = 2;
|
|
7
|
+
export declare const KEY_EXCHANGE_STEP3 = 3;
|
|
8
|
+
export declare const KEY_EXCHANGE_STEP4 = 4;
|
|
9
|
+
/** Cryptographic operations for blerpc E2E encryption. */
|
|
10
|
+
export declare class BlerpcCrypto {
|
|
11
|
+
/** Generate an X25519 key pair. Returns [privateKey(32), publicKey(32)]. */
|
|
12
|
+
static generateX25519KeyPair(): [Uint8Array, Uint8Array];
|
|
13
|
+
/** Get the raw 32-byte public key from a private key. */
|
|
14
|
+
static x25519PublicKey(privateKey: Uint8Array): Uint8Array;
|
|
15
|
+
/** Compute X25519 shared secret (32 bytes). */
|
|
16
|
+
static x25519SharedSecret(privateKey: Uint8Array, peerPublicKey: Uint8Array): Uint8Array;
|
|
17
|
+
/**
|
|
18
|
+
* Derive 16-byte AES-128 session key using HKDF-SHA256.
|
|
19
|
+
*
|
|
20
|
+
* salt = centralPubkey || peripheralPubkey (64 bytes)
|
|
21
|
+
* info = "blerpc-session-key"
|
|
22
|
+
*/
|
|
23
|
+
static deriveSessionKey(sharedSecret: Uint8Array, centralPubkey: Uint8Array, peripheralPubkey: Uint8Array): Uint8Array;
|
|
24
|
+
/** Generate an Ed25519 key pair. Returns [privateKey(32), publicKey(32)]. */
|
|
25
|
+
static generateEd25519KeyPair(): [Uint8Array, Uint8Array];
|
|
26
|
+
/** Get the raw 32-byte public key from an Ed25519 private key. */
|
|
27
|
+
static ed25519PublicKey(privateKey: Uint8Array): Uint8Array;
|
|
28
|
+
/** Sign a message with Ed25519. Returns 64-byte signature. */
|
|
29
|
+
static ed25519Sign(privateKey: Uint8Array, message: Uint8Array): Uint8Array;
|
|
30
|
+
/** Verify an Ed25519 signature. Returns true if valid. */
|
|
31
|
+
static ed25519Verify(publicKey: Uint8Array, message: Uint8Array, signature: Uint8Array): boolean;
|
|
32
|
+
/** Build 12-byte AES-GCM nonce: counter(4B LE) || direction(1B) || zeros(7B). */
|
|
33
|
+
private static _buildNonce;
|
|
34
|
+
/**
|
|
35
|
+
* Encrypt a command payload.
|
|
36
|
+
*
|
|
37
|
+
* Returns: [counter:4BLE][ciphertext:NB][tag:16B]
|
|
38
|
+
*/
|
|
39
|
+
static encryptCommand(sessionKey: Uint8Array, counter: number, direction: number, plaintext: Uint8Array): Uint8Array;
|
|
40
|
+
/**
|
|
41
|
+
* Decrypt a command payload.
|
|
42
|
+
*
|
|
43
|
+
* Input: [counter:4BLE][ciphertext:NB][tag:16B]
|
|
44
|
+
* Returns: [counter, plaintext]
|
|
45
|
+
*/
|
|
46
|
+
static decryptCommand(sessionKey: Uint8Array, direction: number, data: Uint8Array): [number, Uint8Array];
|
|
47
|
+
/**
|
|
48
|
+
* Encrypt a confirmation message for key exchange step 3/4.
|
|
49
|
+
*
|
|
50
|
+
* Returns: [nonce:12B][ciphertext:16B][tag:16B] = 44 bytes
|
|
51
|
+
*/
|
|
52
|
+
static encryptConfirmation(sessionKey: Uint8Array, message: Uint8Array): Uint8Array;
|
|
53
|
+
/**
|
|
54
|
+
* Decrypt a confirmation message from key exchange step 3/4.
|
|
55
|
+
*
|
|
56
|
+
* Input: [nonce:12B][ciphertext:16B][tag:16B] = 44 bytes
|
|
57
|
+
* Returns: plaintext (16 bytes)
|
|
58
|
+
*/
|
|
59
|
+
static decryptConfirmation(sessionKey: Uint8Array, data: Uint8Array): Uint8Array;
|
|
60
|
+
/** Build KEY_EXCHANGE step 1 payload (33 bytes). [step:u8=0x01][central_x25519_pubkey:32B] */
|
|
61
|
+
static buildStep1Payload(centralX25519Pubkey: Uint8Array): Uint8Array;
|
|
62
|
+
/** Parse KEY_EXCHANGE step 1 payload. Returns central_x25519_pubkey (32 bytes). */
|
|
63
|
+
static parseStep1Payload(data: Uint8Array): Uint8Array;
|
|
64
|
+
/**
|
|
65
|
+
* Build KEY_EXCHANGE step 2 payload (129 bytes).
|
|
66
|
+
* [step:u8=0x02][peripheral_x25519_pubkey:32B][ed25519_signature:64B][peripheral_ed25519_pubkey:32B]
|
|
67
|
+
*/
|
|
68
|
+
static buildStep2Payload(peripheralX25519Pubkey: Uint8Array, ed25519Signature: Uint8Array, peripheralEd25519Pubkey: Uint8Array): Uint8Array;
|
|
69
|
+
/** Parse KEY_EXCHANGE step 2 payload. Returns [peripheral_x25519_pubkey, signature, peripheral_ed25519_pubkey]. */
|
|
70
|
+
static parseStep2Payload(data: Uint8Array): [Uint8Array, Uint8Array, Uint8Array];
|
|
71
|
+
/** Build KEY_EXCHANGE step 3 payload (45 bytes). [step:u8=0x03][nonce:12B][ciphertext:16B][tag:16B] */
|
|
72
|
+
static buildStep3Payload(confirmationEncrypted: Uint8Array): Uint8Array;
|
|
73
|
+
/** Parse KEY_EXCHANGE step 3 payload. Returns the encrypted confirmation (44 bytes). */
|
|
74
|
+
static parseStep3Payload(data: Uint8Array): Uint8Array;
|
|
75
|
+
/** Build KEY_EXCHANGE step 4 payload (45 bytes). [step:u8=0x04][nonce:12B][ciphertext:16B][tag:16B] */
|
|
76
|
+
static buildStep4Payload(confirmationEncrypted: Uint8Array): Uint8Array;
|
|
77
|
+
/** Parse KEY_EXCHANGE step 4 payload. Returns the encrypted confirmation (44 bytes). */
|
|
78
|
+
static parseStep4Payload(data: Uint8Array): Uint8Array;
|
|
79
|
+
}
|
|
80
|
+
/** Encrypt/decrypt with counter management and replay detection. */
|
|
81
|
+
export declare class BlerpcCryptoSession {
|
|
82
|
+
private readonly _sessionKey;
|
|
83
|
+
/** TX counter, visible for testing. */
|
|
84
|
+
txCounter: number;
|
|
85
|
+
private _rxCounter;
|
|
86
|
+
private _rxFirstDone;
|
|
87
|
+
private readonly _txDirection;
|
|
88
|
+
private readonly _rxDirection;
|
|
89
|
+
constructor(sessionKey: Uint8Array, isCentral: boolean);
|
|
90
|
+
/** Encrypt plaintext with auto-incrementing TX counter. */
|
|
91
|
+
encrypt(plaintext: Uint8Array): Uint8Array;
|
|
92
|
+
/** Decrypt data with replay detection on RX counter. */
|
|
93
|
+
decrypt(data: Uint8Array): Uint8Array;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Central-side key exchange state machine.
|
|
97
|
+
*
|
|
98
|
+
* Usage:
|
|
99
|
+
* const kx = new CentralKeyExchange();
|
|
100
|
+
* const step1 = kx.start(); // send to peripheral
|
|
101
|
+
* const step3 = kx.processStep2(s2); // send to peripheral
|
|
102
|
+
* const session = kx.finish(s4); // BlerpcCryptoSession
|
|
103
|
+
*/
|
|
104
|
+
export declare class CentralKeyExchange {
|
|
105
|
+
private _x25519PrivKey;
|
|
106
|
+
private _x25519Pubkey;
|
|
107
|
+
private _sessionKey;
|
|
108
|
+
private _state;
|
|
109
|
+
/** Generate ephemeral X25519 keypair and return step 1 payload. */
|
|
110
|
+
start(): Uint8Array;
|
|
111
|
+
/** Parse step 2, verify signature, derive session key, return step 3 payload. */
|
|
112
|
+
processStep2(step2Payload: Uint8Array, verifyKeyCb?: (key: Uint8Array) => boolean): Uint8Array;
|
|
113
|
+
/** Parse step 4, verify peripheral confirmation, return session. */
|
|
114
|
+
finish(step4Payload: Uint8Array): BlerpcCryptoSession;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Peripheral-side key exchange state machine.
|
|
118
|
+
*
|
|
119
|
+
* Usage:
|
|
120
|
+
* const kx = new PeripheralKeyExchange(ed25519PrivKey, ed25519PubKey);
|
|
121
|
+
* const step2 = kx.processStep1(s1); // send to central
|
|
122
|
+
* const [step4, session] = kx.processStep3(s3); // send + session
|
|
123
|
+
*/
|
|
124
|
+
export declare class PeripheralKeyExchange {
|
|
125
|
+
private readonly _ed25519PrivKey;
|
|
126
|
+
private readonly _ed25519PubKey;
|
|
127
|
+
private _sessionKey;
|
|
128
|
+
private _state;
|
|
129
|
+
constructor(ed25519PrivKey: Uint8Array, ed25519PubKey: Uint8Array);
|
|
130
|
+
/** Visible for testing. */
|
|
131
|
+
get sessionKey(): Uint8Array | null;
|
|
132
|
+
/** Parse step 1, generate ephemeral X25519 keypair, sign, derive session key, return step 2 payload. */
|
|
133
|
+
processStep1(step1Payload: Uint8Array): Uint8Array;
|
|
134
|
+
/** Parse step 3, verify confirmation, return [step4Payload, session]. */
|
|
135
|
+
processStep3(step3Payload: Uint8Array): [Uint8Array, BlerpcCryptoSession];
|
|
136
|
+
/**
|
|
137
|
+
* Dispatch a key exchange payload by step byte.
|
|
138
|
+
* Returns [responsePayload, sessionOrNull].
|
|
139
|
+
*/
|
|
140
|
+
handleStep(payload: Uint8Array): [Uint8Array, BlerpcCryptoSession | null];
|
|
141
|
+
/** Reset key exchange state for new connection. */
|
|
142
|
+
reset(): void;
|
|
143
|
+
}
|
|
144
|
+
/** Perform the 4-step central key exchange using send/receive callbacks. */
|
|
145
|
+
export declare function centralPerformKeyExchange(options: {
|
|
146
|
+
send: (payload: Uint8Array) => Promise<void>;
|
|
147
|
+
receive: () => Promise<Uint8Array>;
|
|
148
|
+
verifyKeyCb?: (key: Uint8Array) => boolean;
|
|
149
|
+
}): Promise<BlerpcCryptoSession>;
|
package/dist/crypto.js
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// E2E encryption for blerpc using X25519, Ed25519, AES-128-GCM, HKDF-SHA256.
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.PeripheralKeyExchange = exports.CentralKeyExchange = exports.BlerpcCryptoSession = exports.BlerpcCrypto = exports.KEY_EXCHANGE_STEP4 = exports.KEY_EXCHANGE_STEP3 = exports.KEY_EXCHANGE_STEP2 = exports.KEY_EXCHANGE_STEP1 = exports.CONFIRM_PERIPHERAL = exports.CONFIRM_CENTRAL = exports.DIRECTION_P2C = exports.DIRECTION_C2P = void 0;
|
|
5
|
+
exports.centralPerformKeyExchange = centralPerformKeyExchange;
|
|
6
|
+
const ed25519_1 = require("@noble/curves/ed25519");
|
|
7
|
+
const ed25519_2 = require("@noble/curves/ed25519");
|
|
8
|
+
const aes_1 = require("@noble/ciphers/aes");
|
|
9
|
+
const webcrypto_1 = require("@noble/ciphers/webcrypto");
|
|
10
|
+
const hkdf_1 = require("@noble/hashes/hkdf");
|
|
11
|
+
const sha256_1 = require("@noble/hashes/sha256");
|
|
12
|
+
// Direction bytes for nonce construction
|
|
13
|
+
exports.DIRECTION_C2P = 0x00;
|
|
14
|
+
exports.DIRECTION_P2C = 0x01;
|
|
15
|
+
// Confirmation plaintexts
|
|
16
|
+
const encoder = new TextEncoder();
|
|
17
|
+
exports.CONFIRM_CENTRAL = encoder.encode("BLERPC_CONFIRM_C");
|
|
18
|
+
exports.CONFIRM_PERIPHERAL = encoder.encode("BLERPC_CONFIRM_P");
|
|
19
|
+
// Key exchange step constants
|
|
20
|
+
exports.KEY_EXCHANGE_STEP1 = 0x01;
|
|
21
|
+
exports.KEY_EXCHANGE_STEP2 = 0x02;
|
|
22
|
+
exports.KEY_EXCHANGE_STEP3 = 0x03;
|
|
23
|
+
exports.KEY_EXCHANGE_STEP4 = 0x04;
|
|
24
|
+
function concatBytes(...arrays) {
|
|
25
|
+
const totalLen = arrays.reduce((sum, a) => sum + a.length, 0);
|
|
26
|
+
const result = new Uint8Array(totalLen);
|
|
27
|
+
let offset = 0;
|
|
28
|
+
for (const a of arrays) {
|
|
29
|
+
result.set(a, offset);
|
|
30
|
+
offset += a.length;
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
function uint8ArrayEquals(a, b) {
|
|
35
|
+
if (a.length !== b.length)
|
|
36
|
+
return false;
|
|
37
|
+
for (let i = 0; i < a.length; i++) {
|
|
38
|
+
if (a[i] !== b[i])
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
/** Cryptographic operations for blerpc E2E encryption. */
|
|
44
|
+
class BlerpcCrypto {
|
|
45
|
+
/** Generate an X25519 key pair. Returns [privateKey(32), publicKey(32)]. */
|
|
46
|
+
static generateX25519KeyPair() {
|
|
47
|
+
const privateKey = (0, webcrypto_1.randomBytes)(32);
|
|
48
|
+
const publicKey = ed25519_1.x25519.getPublicKey(privateKey);
|
|
49
|
+
return [privateKey, publicKey];
|
|
50
|
+
}
|
|
51
|
+
/** Get the raw 32-byte public key from a private key. */
|
|
52
|
+
static x25519PublicKey(privateKey) {
|
|
53
|
+
return ed25519_1.x25519.getPublicKey(privateKey);
|
|
54
|
+
}
|
|
55
|
+
/** Compute X25519 shared secret (32 bytes). */
|
|
56
|
+
static x25519SharedSecret(privateKey, peerPublicKey) {
|
|
57
|
+
return ed25519_1.x25519.getSharedSecret(privateKey, peerPublicKey);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Derive 16-byte AES-128 session key using HKDF-SHA256.
|
|
61
|
+
*
|
|
62
|
+
* salt = centralPubkey || peripheralPubkey (64 bytes)
|
|
63
|
+
* info = "blerpc-session-key"
|
|
64
|
+
*/
|
|
65
|
+
static deriveSessionKey(sharedSecret, centralPubkey, peripheralPubkey) {
|
|
66
|
+
const salt = concatBytes(centralPubkey, peripheralPubkey);
|
|
67
|
+
const info = encoder.encode("blerpc-session-key");
|
|
68
|
+
return (0, hkdf_1.hkdf)(sha256_1.sha256, sharedSecret, salt, info, 16);
|
|
69
|
+
}
|
|
70
|
+
/** Generate an Ed25519 key pair. Returns [privateKey(32), publicKey(32)]. */
|
|
71
|
+
static generateEd25519KeyPair() {
|
|
72
|
+
const privateKey = (0, webcrypto_1.randomBytes)(32);
|
|
73
|
+
const publicKey = ed25519_2.ed25519.getPublicKey(privateKey);
|
|
74
|
+
return [privateKey, publicKey];
|
|
75
|
+
}
|
|
76
|
+
/** Get the raw 32-byte public key from an Ed25519 private key. */
|
|
77
|
+
static ed25519PublicKey(privateKey) {
|
|
78
|
+
return ed25519_2.ed25519.getPublicKey(privateKey);
|
|
79
|
+
}
|
|
80
|
+
/** Sign a message with Ed25519. Returns 64-byte signature. */
|
|
81
|
+
static ed25519Sign(privateKey, message) {
|
|
82
|
+
return ed25519_2.ed25519.sign(message, privateKey);
|
|
83
|
+
}
|
|
84
|
+
/** Verify an Ed25519 signature. Returns true if valid. */
|
|
85
|
+
static ed25519Verify(publicKey, message, signature) {
|
|
86
|
+
try {
|
|
87
|
+
return ed25519_2.ed25519.verify(signature, message, publicKey);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** Build 12-byte AES-GCM nonce: counter(4B LE) || direction(1B) || zeros(7B). */
|
|
94
|
+
static _buildNonce(counter, direction) {
|
|
95
|
+
const buf = new ArrayBuffer(12);
|
|
96
|
+
const view = new DataView(buf);
|
|
97
|
+
view.setUint32(0, counter, true);
|
|
98
|
+
view.setUint8(4, direction);
|
|
99
|
+
// bytes 5-11 are already zero
|
|
100
|
+
return new Uint8Array(buf);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Encrypt a command payload.
|
|
104
|
+
*
|
|
105
|
+
* Returns: [counter:4BLE][ciphertext:NB][tag:16B]
|
|
106
|
+
*/
|
|
107
|
+
static encryptCommand(sessionKey, counter, direction, plaintext) {
|
|
108
|
+
const nonce = BlerpcCrypto._buildNonce(counter, direction);
|
|
109
|
+
const aes = (0, aes_1.gcm)(sessionKey, nonce);
|
|
110
|
+
const sealed = aes.encrypt(plaintext); // ciphertext + tag appended
|
|
111
|
+
// counter(4) + sealed (ciphertext + tag)
|
|
112
|
+
const counterBytes = new Uint8Array(4);
|
|
113
|
+
new DataView(counterBytes.buffer).setUint32(0, counter, true);
|
|
114
|
+
return concatBytes(counterBytes, sealed);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Decrypt a command payload.
|
|
118
|
+
*
|
|
119
|
+
* Input: [counter:4BLE][ciphertext:NB][tag:16B]
|
|
120
|
+
* Returns: [counter, plaintext]
|
|
121
|
+
*/
|
|
122
|
+
static decryptCommand(sessionKey, direction, data) {
|
|
123
|
+
if (data.length < 20) {
|
|
124
|
+
throw new Error(`Encrypted payload too short: ${data.length}`);
|
|
125
|
+
}
|
|
126
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
127
|
+
const counter = view.getUint32(0, true);
|
|
128
|
+
const sealed = data.slice(4); // ciphertext + tag
|
|
129
|
+
const nonce = BlerpcCrypto._buildNonce(counter, direction);
|
|
130
|
+
const aes = (0, aes_1.gcm)(sessionKey, nonce);
|
|
131
|
+
const plaintext = aes.decrypt(sealed);
|
|
132
|
+
return [counter, plaintext];
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Encrypt a confirmation message for key exchange step 3/4.
|
|
136
|
+
*
|
|
137
|
+
* Returns: [nonce:12B][ciphertext:16B][tag:16B] = 44 bytes
|
|
138
|
+
*/
|
|
139
|
+
static encryptConfirmation(sessionKey, message) {
|
|
140
|
+
const nonce = (0, webcrypto_1.randomBytes)(12);
|
|
141
|
+
const aes = (0, aes_1.gcm)(sessionKey, nonce);
|
|
142
|
+
const sealed = aes.encrypt(message); // ciphertext + tag
|
|
143
|
+
return concatBytes(nonce, sealed);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Decrypt a confirmation message from key exchange step 3/4.
|
|
147
|
+
*
|
|
148
|
+
* Input: [nonce:12B][ciphertext:16B][tag:16B] = 44 bytes
|
|
149
|
+
* Returns: plaintext (16 bytes)
|
|
150
|
+
*/
|
|
151
|
+
static decryptConfirmation(sessionKey, data) {
|
|
152
|
+
if (data.length < 44) {
|
|
153
|
+
throw new Error(`Confirmation too short: ${data.length}`);
|
|
154
|
+
}
|
|
155
|
+
const nonce = data.slice(0, 12);
|
|
156
|
+
const sealed = data.slice(12); // ciphertext + tag
|
|
157
|
+
const aes = (0, aes_1.gcm)(sessionKey, nonce);
|
|
158
|
+
return aes.decrypt(sealed);
|
|
159
|
+
}
|
|
160
|
+
/** Build KEY_EXCHANGE step 1 payload (33 bytes). [step:u8=0x01][central_x25519_pubkey:32B] */
|
|
161
|
+
static buildStep1Payload(centralX25519Pubkey) {
|
|
162
|
+
return concatBytes(new Uint8Array([exports.KEY_EXCHANGE_STEP1]), centralX25519Pubkey);
|
|
163
|
+
}
|
|
164
|
+
/** Parse KEY_EXCHANGE step 1 payload. Returns central_x25519_pubkey (32 bytes). */
|
|
165
|
+
static parseStep1Payload(data) {
|
|
166
|
+
if (data.length < 33 || data[0] !== exports.KEY_EXCHANGE_STEP1) {
|
|
167
|
+
throw new Error("Invalid step 1 payload");
|
|
168
|
+
}
|
|
169
|
+
return data.slice(1, 33);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Build KEY_EXCHANGE step 2 payload (129 bytes).
|
|
173
|
+
* [step:u8=0x02][peripheral_x25519_pubkey:32B][ed25519_signature:64B][peripheral_ed25519_pubkey:32B]
|
|
174
|
+
*/
|
|
175
|
+
static buildStep2Payload(peripheralX25519Pubkey, ed25519Signature, peripheralEd25519Pubkey) {
|
|
176
|
+
return concatBytes(new Uint8Array([exports.KEY_EXCHANGE_STEP2]), peripheralX25519Pubkey, ed25519Signature, peripheralEd25519Pubkey);
|
|
177
|
+
}
|
|
178
|
+
/** Parse KEY_EXCHANGE step 2 payload. Returns [peripheral_x25519_pubkey, signature, peripheral_ed25519_pubkey]. */
|
|
179
|
+
static parseStep2Payload(data) {
|
|
180
|
+
if (data.length < 129 || data[0] !== exports.KEY_EXCHANGE_STEP2) {
|
|
181
|
+
throw new Error("Invalid step 2 payload");
|
|
182
|
+
}
|
|
183
|
+
return [data.slice(1, 33), data.slice(33, 97), data.slice(97, 129)];
|
|
184
|
+
}
|
|
185
|
+
/** Build KEY_EXCHANGE step 3 payload (45 bytes). [step:u8=0x03][nonce:12B][ciphertext:16B][tag:16B] */
|
|
186
|
+
static buildStep3Payload(confirmationEncrypted) {
|
|
187
|
+
return concatBytes(new Uint8Array([exports.KEY_EXCHANGE_STEP3]), confirmationEncrypted);
|
|
188
|
+
}
|
|
189
|
+
/** Parse KEY_EXCHANGE step 3 payload. Returns the encrypted confirmation (44 bytes). */
|
|
190
|
+
static parseStep3Payload(data) {
|
|
191
|
+
if (data.length < 45 || data[0] !== exports.KEY_EXCHANGE_STEP3) {
|
|
192
|
+
throw new Error("Invalid step 3 payload");
|
|
193
|
+
}
|
|
194
|
+
return data.slice(1, 45);
|
|
195
|
+
}
|
|
196
|
+
/** Build KEY_EXCHANGE step 4 payload (45 bytes). [step:u8=0x04][nonce:12B][ciphertext:16B][tag:16B] */
|
|
197
|
+
static buildStep4Payload(confirmationEncrypted) {
|
|
198
|
+
return concatBytes(new Uint8Array([exports.KEY_EXCHANGE_STEP4]), confirmationEncrypted);
|
|
199
|
+
}
|
|
200
|
+
/** Parse KEY_EXCHANGE step 4 payload. Returns the encrypted confirmation (44 bytes). */
|
|
201
|
+
static parseStep4Payload(data) {
|
|
202
|
+
if (data.length < 45 || data[0] !== exports.KEY_EXCHANGE_STEP4) {
|
|
203
|
+
throw new Error("Invalid step 4 payload");
|
|
204
|
+
}
|
|
205
|
+
return data.slice(1, 45);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
exports.BlerpcCrypto = BlerpcCrypto;
|
|
209
|
+
/** Encrypt/decrypt with counter management and replay detection. */
|
|
210
|
+
class BlerpcCryptoSession {
|
|
211
|
+
constructor(sessionKey, isCentral) {
|
|
212
|
+
/** TX counter, visible for testing. */
|
|
213
|
+
this.txCounter = 0;
|
|
214
|
+
this._rxCounter = 0;
|
|
215
|
+
this._rxFirstDone = false;
|
|
216
|
+
this._sessionKey = sessionKey;
|
|
217
|
+
this._txDirection = isCentral ? exports.DIRECTION_C2P : exports.DIRECTION_P2C;
|
|
218
|
+
this._rxDirection = isCentral ? exports.DIRECTION_P2C : exports.DIRECTION_C2P;
|
|
219
|
+
}
|
|
220
|
+
/** Encrypt plaintext with auto-incrementing TX counter. */
|
|
221
|
+
encrypt(plaintext) {
|
|
222
|
+
if (this.txCounter >= 0xffffffff) {
|
|
223
|
+
throw new Error("TX counter overflow: session must be rekeyed");
|
|
224
|
+
}
|
|
225
|
+
const encrypted = BlerpcCrypto.encryptCommand(this._sessionKey, this.txCounter, this._txDirection, plaintext);
|
|
226
|
+
this.txCounter++;
|
|
227
|
+
return encrypted;
|
|
228
|
+
}
|
|
229
|
+
/** Decrypt data with replay detection on RX counter. */
|
|
230
|
+
decrypt(data) {
|
|
231
|
+
const [counter, plaintext] = BlerpcCrypto.decryptCommand(this._sessionKey, this._rxDirection, data);
|
|
232
|
+
if (this._rxFirstDone && counter <= this._rxCounter) {
|
|
233
|
+
throw new Error(`Replay detected: counter=${counter}`);
|
|
234
|
+
}
|
|
235
|
+
this._rxCounter = counter;
|
|
236
|
+
this._rxFirstDone = true;
|
|
237
|
+
return plaintext;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
exports.BlerpcCryptoSession = BlerpcCryptoSession;
|
|
241
|
+
/**
|
|
242
|
+
* Central-side key exchange state machine.
|
|
243
|
+
*
|
|
244
|
+
* Usage:
|
|
245
|
+
* const kx = new CentralKeyExchange();
|
|
246
|
+
* const step1 = kx.start(); // send to peripheral
|
|
247
|
+
* const step3 = kx.processStep2(s2); // send to peripheral
|
|
248
|
+
* const session = kx.finish(s4); // BlerpcCryptoSession
|
|
249
|
+
*/
|
|
250
|
+
class CentralKeyExchange {
|
|
251
|
+
constructor() {
|
|
252
|
+
this._x25519PrivKey = null;
|
|
253
|
+
this._x25519Pubkey = null;
|
|
254
|
+
this._sessionKey = null;
|
|
255
|
+
this._state = 0;
|
|
256
|
+
}
|
|
257
|
+
/** Generate ephemeral X25519 keypair and return step 1 payload. */
|
|
258
|
+
start() {
|
|
259
|
+
if (this._state !== 0)
|
|
260
|
+
throw new Error("Invalid state for start()");
|
|
261
|
+
const [priv, pub] = BlerpcCrypto.generateX25519KeyPair();
|
|
262
|
+
this._x25519PrivKey = priv;
|
|
263
|
+
this._x25519Pubkey = pub;
|
|
264
|
+
this._state = 1;
|
|
265
|
+
return BlerpcCrypto.buildStep1Payload(pub);
|
|
266
|
+
}
|
|
267
|
+
/** Parse step 2, verify signature, derive session key, return step 3 payload. */
|
|
268
|
+
processStep2(step2Payload, verifyKeyCb) {
|
|
269
|
+
if (this._state !== 1)
|
|
270
|
+
throw new Error("Invalid state for processStep2()");
|
|
271
|
+
const [periphX25519Pub, signature, periphEd25519Pub] = BlerpcCrypto.parseStep2Payload(step2Payload);
|
|
272
|
+
const signMsg = concatBytes(this._x25519Pubkey, periphX25519Pub);
|
|
273
|
+
const valid = BlerpcCrypto.ed25519Verify(periphEd25519Pub, signMsg, signature);
|
|
274
|
+
if (!valid) {
|
|
275
|
+
throw new Error("Ed25519 signature verification failed");
|
|
276
|
+
}
|
|
277
|
+
if (verifyKeyCb && !verifyKeyCb(periphEd25519Pub)) {
|
|
278
|
+
throw new Error("Peripheral key rejected by verify callback");
|
|
279
|
+
}
|
|
280
|
+
const sharedSecret = BlerpcCrypto.x25519SharedSecret(this._x25519PrivKey, periphX25519Pub);
|
|
281
|
+
this._sessionKey = BlerpcCrypto.deriveSessionKey(sharedSecret, this._x25519Pubkey, periphX25519Pub);
|
|
282
|
+
const encryptedConfirm = BlerpcCrypto.encryptConfirmation(this._sessionKey, exports.CONFIRM_CENTRAL);
|
|
283
|
+
this._state = 2;
|
|
284
|
+
return BlerpcCrypto.buildStep3Payload(encryptedConfirm);
|
|
285
|
+
}
|
|
286
|
+
/** Parse step 4, verify peripheral confirmation, return session. */
|
|
287
|
+
finish(step4Payload) {
|
|
288
|
+
if (this._state !== 2)
|
|
289
|
+
throw new Error("Invalid state for finish()");
|
|
290
|
+
const encryptedPeriph = BlerpcCrypto.parseStep4Payload(step4Payload);
|
|
291
|
+
const plaintext = BlerpcCrypto.decryptConfirmation(this._sessionKey, encryptedPeriph);
|
|
292
|
+
if (!uint8ArrayEquals(plaintext, exports.CONFIRM_PERIPHERAL)) {
|
|
293
|
+
throw new Error("Peripheral confirmation mismatch");
|
|
294
|
+
}
|
|
295
|
+
return new BlerpcCryptoSession(this._sessionKey, true);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
exports.CentralKeyExchange = CentralKeyExchange;
|
|
299
|
+
/**
|
|
300
|
+
* Peripheral-side key exchange state machine.
|
|
301
|
+
*
|
|
302
|
+
* Usage:
|
|
303
|
+
* const kx = new PeripheralKeyExchange(ed25519PrivKey, ed25519PubKey);
|
|
304
|
+
* const step2 = kx.processStep1(s1); // send to central
|
|
305
|
+
* const [step4, session] = kx.processStep3(s3); // send + session
|
|
306
|
+
*/
|
|
307
|
+
class PeripheralKeyExchange {
|
|
308
|
+
constructor(ed25519PrivKey, ed25519PubKey) {
|
|
309
|
+
this._sessionKey = null;
|
|
310
|
+
this._state = 0;
|
|
311
|
+
this._ed25519PrivKey = ed25519PrivKey;
|
|
312
|
+
this._ed25519PubKey = ed25519PubKey;
|
|
313
|
+
}
|
|
314
|
+
/** Visible for testing. */
|
|
315
|
+
get sessionKey() {
|
|
316
|
+
return this._sessionKey;
|
|
317
|
+
}
|
|
318
|
+
/** Parse step 1, generate ephemeral X25519 keypair, sign, derive session key, return step 2 payload. */
|
|
319
|
+
processStep1(step1Payload) {
|
|
320
|
+
if (this._state !== 0)
|
|
321
|
+
throw new Error("Invalid state for processStep1()");
|
|
322
|
+
const centralX25519Pub = BlerpcCrypto.parseStep1Payload(step1Payload);
|
|
323
|
+
const [x25519Priv, x25519Pub] = BlerpcCrypto.generateX25519KeyPair();
|
|
324
|
+
const signMsg = concatBytes(centralX25519Pub, x25519Pub);
|
|
325
|
+
const signature = BlerpcCrypto.ed25519Sign(this._ed25519PrivKey, signMsg);
|
|
326
|
+
const sharedSecret = BlerpcCrypto.x25519SharedSecret(x25519Priv, centralX25519Pub);
|
|
327
|
+
this._sessionKey = BlerpcCrypto.deriveSessionKey(sharedSecret, centralX25519Pub, x25519Pub);
|
|
328
|
+
this._state = 1;
|
|
329
|
+
return BlerpcCrypto.buildStep2Payload(x25519Pub, signature, this._ed25519PubKey);
|
|
330
|
+
}
|
|
331
|
+
/** Parse step 3, verify confirmation, return [step4Payload, session]. */
|
|
332
|
+
processStep3(step3Payload) {
|
|
333
|
+
if (this._state !== 1)
|
|
334
|
+
throw new Error("Invalid state for processStep3()");
|
|
335
|
+
const encrypted = BlerpcCrypto.parseStep3Payload(step3Payload);
|
|
336
|
+
const plaintext = BlerpcCrypto.decryptConfirmation(this._sessionKey, encrypted);
|
|
337
|
+
if (!uint8ArrayEquals(plaintext, exports.CONFIRM_CENTRAL)) {
|
|
338
|
+
throw new Error("Central confirmation mismatch");
|
|
339
|
+
}
|
|
340
|
+
const encryptedConfirm = BlerpcCrypto.encryptConfirmation(this._sessionKey, exports.CONFIRM_PERIPHERAL);
|
|
341
|
+
const step4 = BlerpcCrypto.buildStep4Payload(encryptedConfirm);
|
|
342
|
+
const session = new BlerpcCryptoSession(this._sessionKey, false);
|
|
343
|
+
return [step4, session];
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Dispatch a key exchange payload by step byte.
|
|
347
|
+
* Returns [responsePayload, sessionOrNull].
|
|
348
|
+
*/
|
|
349
|
+
handleStep(payload) {
|
|
350
|
+
if (payload.length === 0) {
|
|
351
|
+
throw new Error("Empty key exchange payload");
|
|
352
|
+
}
|
|
353
|
+
const step = payload[0];
|
|
354
|
+
if (step === exports.KEY_EXCHANGE_STEP1) {
|
|
355
|
+
if (this._state !== 0)
|
|
356
|
+
throw new Error("Invalid state for step 1");
|
|
357
|
+
const response = this.processStep1(payload);
|
|
358
|
+
return [response, null];
|
|
359
|
+
}
|
|
360
|
+
else if (step === exports.KEY_EXCHANGE_STEP3) {
|
|
361
|
+
if (this._state !== 1)
|
|
362
|
+
throw new Error("Invalid state for step 3");
|
|
363
|
+
const [step4, session] = this.processStep3(payload);
|
|
364
|
+
return [step4, session];
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
throw new Error(`Invalid key exchange step: 0x${step.toString(16).padStart(2, "0")}`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
/** Reset key exchange state for new connection. */
|
|
371
|
+
reset() {
|
|
372
|
+
this._state = 0;
|
|
373
|
+
this._sessionKey = null;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
exports.PeripheralKeyExchange = PeripheralKeyExchange;
|
|
377
|
+
/** Perform the 4-step central key exchange using send/receive callbacks. */
|
|
378
|
+
async function centralPerformKeyExchange(options) {
|
|
379
|
+
const kx = new CentralKeyExchange();
|
|
380
|
+
// Step 1: Send central's ephemeral public key
|
|
381
|
+
const step1 = kx.start();
|
|
382
|
+
await options.send(step1);
|
|
383
|
+
// Step 2: Receive peripheral's response
|
|
384
|
+
const step2 = await options.receive();
|
|
385
|
+
// Step 2 -> Step 3: Verify and produce confirmation
|
|
386
|
+
const step3 = kx.processStep2(step2, options.verifyKeyCb);
|
|
387
|
+
await options.send(step3);
|
|
388
|
+
// Step 4: Receive peripheral's confirmation
|
|
389
|
+
const step4 = await options.receive();
|
|
390
|
+
return kx.finish(step4);
|
|
391
|
+
}
|
|
392
|
+
//# sourceMappingURL=crypto.js.map
|