react-native-ble-mesh 1.1.1 → 2.1.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.
Files changed (80) hide show
  1. package/README.md +288 -172
  2. package/docs/IOS-BACKGROUND-BLE.md +231 -0
  3. package/docs/OPTIMIZATION.md +183 -0
  4. package/docs/SPEC-v2.1.md +308 -0
  5. package/package.json +1 -1
  6. package/src/MeshNetwork.js +667 -465
  7. package/src/constants/index.js +1 -0
  8. package/src/crypto/AutoCrypto.js +90 -0
  9. package/src/crypto/CryptoProvider.js +99 -0
  10. package/src/crypto/index.js +15 -63
  11. package/src/crypto/providers/ExpoCryptoProvider.js +126 -0
  12. package/src/crypto/providers/QuickCryptoProvider.js +158 -0
  13. package/src/crypto/providers/TweetNaClProvider.js +124 -0
  14. package/src/crypto/providers/index.js +11 -0
  15. package/src/errors/MeshError.js +2 -1
  16. package/src/expo/withBLEMesh.js +102 -0
  17. package/src/hooks/AppStateManager.js +9 -1
  18. package/src/hooks/useMesh.js +47 -13
  19. package/src/hooks/useMessages.js +6 -4
  20. package/src/hooks/usePeers.js +13 -9
  21. package/src/index.js +23 -8
  22. package/src/mesh/dedup/BloomFilter.js +44 -57
  23. package/src/mesh/dedup/DedupManager.js +67 -10
  24. package/src/mesh/fragment/Assembler.js +5 -0
  25. package/src/mesh/fragment/Fragmenter.js +1 -1
  26. package/src/mesh/index.js +1 -1
  27. package/src/mesh/monitor/ConnectionQuality.js +433 -0
  28. package/src/mesh/monitor/NetworkMonitor.js +376 -320
  29. package/src/mesh/monitor/index.js +7 -3
  30. package/src/mesh/peer/Peer.js +5 -2
  31. package/src/mesh/peer/PeerManager.js +21 -4
  32. package/src/mesh/router/MessageRouter.js +38 -19
  33. package/src/mesh/router/RouteTable.js +24 -8
  34. package/src/mesh/store/StoreAndForwardManager.js +305 -296
  35. package/src/mesh/store/index.js +1 -1
  36. package/src/protocol/deserializer.js +9 -10
  37. package/src/protocol/header.js +13 -7
  38. package/src/protocol/message.js +15 -3
  39. package/src/protocol/serializer.js +7 -10
  40. package/src/protocol/validator.js +23 -5
  41. package/src/service/BatteryOptimizer.js +285 -278
  42. package/src/service/EmergencyManager.js +224 -214
  43. package/src/service/HandshakeManager.js +163 -13
  44. package/src/service/MeshService.js +72 -6
  45. package/src/service/SessionManager.js +79 -2
  46. package/src/service/audio/AudioManager.js +8 -2
  47. package/src/service/file/FileAssembler.js +106 -0
  48. package/src/service/file/FileChunker.js +79 -0
  49. package/src/service/file/FileManager.js +307 -0
  50. package/src/service/file/FileMessage.js +122 -0
  51. package/src/service/file/index.js +15 -0
  52. package/src/service/text/TextManager.js +21 -15
  53. package/src/service/text/broadcast/BroadcastManager.js +16 -0
  54. package/src/storage/MessageStore.js +55 -2
  55. package/src/transport/BLETransport.js +141 -10
  56. package/src/transport/MockTransport.js +1 -1
  57. package/src/transport/MultiTransport.js +330 -0
  58. package/src/transport/WiFiDirectTransport.js +296 -0
  59. package/src/transport/adapters/NodeBLEAdapter.js +34 -0
  60. package/src/transport/adapters/RNBLEAdapter.js +56 -1
  61. package/src/transport/index.js +6 -0
  62. package/src/utils/EventEmitter.js +6 -9
  63. package/src/utils/bytes.js +12 -10
  64. package/src/utils/compression.js +293 -291
  65. package/src/utils/encoding.js +33 -8
  66. package/src/crypto/aead.js +0 -189
  67. package/src/crypto/chacha20.js +0 -181
  68. package/src/crypto/hkdf.js +0 -187
  69. package/src/crypto/hmac.js +0 -143
  70. package/src/crypto/keys/KeyManager.js +0 -271
  71. package/src/crypto/keys/KeyPair.js +0 -216
  72. package/src/crypto/keys/SecureStorage.js +0 -219
  73. package/src/crypto/keys/index.js +0 -32
  74. package/src/crypto/noise/handshake.js +0 -410
  75. package/src/crypto/noise/index.js +0 -27
  76. package/src/crypto/noise/session.js +0 -253
  77. package/src/crypto/noise/state.js +0 -268
  78. package/src/crypto/poly1305.js +0 -113
  79. package/src/crypto/sha256.js +0 -240
  80. package/src/crypto/x25519.js +0 -154
@@ -14,18 +14,41 @@ for (let i = 0; i < 16; i++) {
14
14
  HEX_LOOKUP[HEX_CHARS.toUpperCase().charCodeAt(i)] = i;
15
15
  }
16
16
 
17
+ // Pre-computed byte-to-hex lookup table (avoids per-byte toString + padStart)
18
+ const HEX_TABLE = new Array(256);
19
+ for (let i = 0; i < 256; i++) {
20
+ HEX_TABLE[i] = HEX_CHARS[i >> 4] + HEX_CHARS[i & 0x0f];
21
+ }
22
+
23
+ // Cached TextEncoder/TextDecoder singletons (avoid per-call allocation)
24
+ let _cachedEncoder = null;
25
+ let _cachedDecoder = null;
26
+
27
+ function _getEncoder() {
28
+ if (!_cachedEncoder && typeof TextEncoder !== 'undefined') {
29
+ _cachedEncoder = new TextEncoder();
30
+ }
31
+ return _cachedEncoder;
32
+ }
33
+
34
+ function _getDecoder() {
35
+ if (!_cachedDecoder && typeof TextDecoder !== 'undefined') {
36
+ _cachedDecoder = new TextDecoder();
37
+ }
38
+ return _cachedDecoder;
39
+ }
40
+
17
41
  /**
18
42
  * Converts a byte array to a hexadecimal string
19
43
  * @param {Uint8Array} bytes - Bytes to convert
20
44
  * @returns {string} Hexadecimal string
21
45
  */
22
46
  function bytesToHex(bytes) {
23
- let result = '';
47
+ const parts = new Array(bytes.length);
24
48
  for (let i = 0; i < bytes.length; i++) {
25
- result += HEX_CHARS[bytes[i] >> 4];
26
- result += HEX_CHARS[bytes[i] & 0x0f];
49
+ parts[i] = HEX_TABLE[bytes[i]];
27
50
  }
28
- return result;
51
+ return parts.join('');
29
52
  }
30
53
 
31
54
  /**
@@ -62,8 +85,9 @@ function hexToBytes(hex) {
62
85
  * @returns {Uint8Array} UTF-8 encoded bytes
63
86
  */
64
87
  function stringToBytes(str) {
65
- if (typeof TextEncoder !== 'undefined') {
66
- return new TextEncoder().encode(str);
88
+ const encoder = _getEncoder();
89
+ if (encoder) {
90
+ return encoder.encode(str);
67
91
  }
68
92
 
69
93
  // Fallback for environments without TextEncoder
@@ -99,8 +123,9 @@ function stringToBytes(str) {
99
123
  * @returns {string} Decoded string
100
124
  */
101
125
  function bytesToString(bytes) {
102
- if (typeof TextDecoder !== 'undefined') {
103
- return new TextDecoder().decode(bytes);
126
+ const decoder = _getDecoder();
127
+ if (decoder) {
128
+ return decoder.decode(bytes);
104
129
  }
105
130
 
106
131
  // Fallback for environments without TextDecoder
@@ -1,189 +0,0 @@
1
- /**
2
- * @fileoverview ChaCha20-Poly1305 AEAD implementation (RFC 8439)
3
- * @module crypto/aead
4
- */
5
-
6
- 'use strict';
7
-
8
- const { chacha20 } = require('./chacha20');
9
- const { poly1305 } = require('./poly1305');
10
-
11
- /**
12
- * Generates Poly1305 key using ChaCha20 with counter=0
13
- * @param {Uint8Array} key - 32-byte encryption key
14
- * @param {Uint8Array} nonce - 12-byte nonce
15
- * @returns {Uint8Array} 32-byte Poly1305 key
16
- */
17
- function generatePolyKey(key, nonce) {
18
- const zeros = new Uint8Array(64);
19
- const block = chacha20(key, nonce, 0, zeros);
20
- return block.subarray(0, 32);
21
- }
22
-
23
- /**
24
- * Pads data to 16-byte boundary with zeros
25
- * @param {number} length - Current data length
26
- * @returns {Uint8Array} Padding bytes (0-15 bytes)
27
- */
28
- function pad16(length) {
29
- const remainder = length % 16;
30
- if (remainder === 0) {
31
- return new Uint8Array(0);
32
- }
33
- return new Uint8Array(16 - remainder);
34
- }
35
-
36
- /**
37
- * Encodes a 64-bit little-endian integer
38
- * @param {number} value - Value to encode
39
- * @returns {Uint8Array} 8-byte little-endian representation
40
- */
41
- function encode64LE(value) {
42
- const bytes = new Uint8Array(8);
43
- const view = new DataView(bytes.buffer);
44
- // JavaScript numbers are 64-bit floats, safe for values up to 2^53-1
45
- view.setUint32(0, value >>> 0, true);
46
- view.setUint32(4, Math.floor(value / 0x100000000) >>> 0, true);
47
- return bytes;
48
- }
49
-
50
- /**
51
- * Constructs Poly1305 MAC input per RFC 8439
52
- * @param {Uint8Array} aad - Additional authenticated data
53
- * @param {Uint8Array} ciphertext - Ciphertext
54
- * @returns {Uint8Array} MAC input
55
- */
56
- function constructMacData(aad, ciphertext) {
57
- const aadPad = pad16(aad.length);
58
- const ctPad = pad16(ciphertext.length);
59
- const aadLen = encode64LE(aad.length);
60
- const ctLen = encode64LE(ciphertext.length);
61
-
62
- const totalLen = aad.length + aadPad.length + ciphertext.length +
63
- ctPad.length + 8 + 8;
64
- const data = new Uint8Array(totalLen);
65
-
66
- let offset = 0;
67
- data.set(aad, offset);
68
- offset += aad.length;
69
- data.set(aadPad, offset);
70
- offset += aadPad.length;
71
- data.set(ciphertext, offset);
72
- offset += ciphertext.length;
73
- data.set(ctPad, offset);
74
- offset += ctPad.length;
75
- data.set(aadLen, offset);
76
- offset += 8;
77
- data.set(ctLen, offset);
78
-
79
- return data;
80
- }
81
-
82
- /**
83
- * Constant-time comparison of two byte arrays
84
- * @param {Uint8Array} a - First array
85
- * @param {Uint8Array} b - Second array
86
- * @returns {boolean} True if arrays are equal
87
- */
88
- function constantTimeEqual(a, b) {
89
- if (a.length !== b.length) {
90
- return false;
91
- }
92
- let diff = 0;
93
- for (let i = 0; i < a.length; i++) {
94
- diff |= a[i] ^ b[i];
95
- }
96
- return diff === 0;
97
- }
98
-
99
- /**
100
- * Encrypts and authenticates data using ChaCha20-Poly1305 AEAD
101
- * @param {Uint8Array} key - 32-byte encryption key
102
- * @param {Uint8Array} nonce - 12-byte nonce (must be unique per key)
103
- * @param {Uint8Array} plaintext - Data to encrypt
104
- * @param {Uint8Array} [aad] - Additional authenticated data (optional)
105
- * @returns {Uint8Array} Ciphertext with appended 16-byte authentication tag
106
- * @throws {Error} If key is not 32 bytes or nonce is not 12 bytes
107
- */
108
- function encrypt(key, nonce, plaintext, aad = new Uint8Array(0)) {
109
- if (!(key instanceof Uint8Array) || key.length !== 32) {
110
- throw new Error('Key must be 32 bytes');
111
- }
112
- if (!(nonce instanceof Uint8Array) || nonce.length !== 12) {
113
- throw new Error('Nonce must be 12 bytes');
114
- }
115
- if (!(plaintext instanceof Uint8Array)) {
116
- throw new Error('Plaintext must be a Uint8Array');
117
- }
118
- if (!(aad instanceof Uint8Array)) {
119
- throw new Error('AAD must be a Uint8Array');
120
- }
121
-
122
- // Generate Poly1305 key
123
- const polyKey = generatePolyKey(key, nonce);
124
-
125
- // Encrypt plaintext with counter starting at 1
126
- const ciphertext = chacha20(key, nonce, 1, plaintext);
127
-
128
- // Compute MAC
129
- const macData = constructMacData(aad, ciphertext);
130
- const tag = poly1305(polyKey, macData);
131
-
132
- // Concatenate ciphertext and tag
133
- const result = new Uint8Array(ciphertext.length + 16);
134
- result.set(ciphertext);
135
- result.set(tag, ciphertext.length);
136
-
137
- return result;
138
- }
139
-
140
- /**
141
- * Decrypts and verifies data using ChaCha20-Poly1305 AEAD
142
- * @param {Uint8Array} key - 32-byte encryption key
143
- * @param {Uint8Array} nonce - 12-byte nonce
144
- * @param {Uint8Array} ciphertext - Ciphertext with 16-byte authentication tag
145
- * @param {Uint8Array} [aad] - Additional authenticated data (optional)
146
- * @returns {Uint8Array|null} Decrypted plaintext, or null if authentication fails
147
- * @throws {Error} If key is not 32 bytes, nonce is not 12 bytes, or ciphertext too short
148
- */
149
- function decrypt(key, nonce, ciphertext, aad = new Uint8Array(0)) {
150
- if (!(key instanceof Uint8Array) || key.length !== 32) {
151
- throw new Error('Key must be 32 bytes');
152
- }
153
- if (!(nonce instanceof Uint8Array) || nonce.length !== 12) {
154
- throw new Error('Nonce must be 12 bytes');
155
- }
156
- if (!(ciphertext instanceof Uint8Array)) {
157
- throw new Error('Ciphertext must be a Uint8Array');
158
- }
159
- if (ciphertext.length < 16) {
160
- throw new Error('Ciphertext must be at least 16 bytes (tag size)');
161
- }
162
- if (!(aad instanceof Uint8Array)) {
163
- throw new Error('AAD must be a Uint8Array');
164
- }
165
-
166
- // Split ciphertext and tag
167
- const ct = ciphertext.subarray(0, ciphertext.length - 16);
168
- const receivedTag = ciphertext.subarray(ciphertext.length - 16);
169
-
170
- // Generate Poly1305 key
171
- const polyKey = generatePolyKey(key, nonce);
172
-
173
- // Compute expected MAC
174
- const macData = constructMacData(aad, ct);
175
- const expectedTag = poly1305(polyKey, macData);
176
-
177
- // Verify tag using constant-time comparison
178
- if (!constantTimeEqual(receivedTag, expectedTag)) {
179
- return null;
180
- }
181
-
182
- // Decrypt ciphertext with counter starting at 1
183
- return chacha20(key, nonce, 1, ct);
184
- }
185
-
186
- module.exports = {
187
- encrypt,
188
- decrypt
189
- };
@@ -1,181 +0,0 @@
1
- /**
2
- * @fileoverview ChaCha20 stream cipher implementation (RFC 8439)
3
- * @module crypto/chacha20
4
- */
5
-
6
- 'use strict';
7
-
8
- /**
9
- * ChaCha20 constants: "expand 32-byte k" in ASCII
10
- * @constant {Uint32Array}
11
- */
12
- const CONSTANTS = new Uint32Array([0x61707865, 0x3320646e, 0x79622d32, 0x6b206574]);
13
-
14
- /**
15
- * Performs left rotation on a 32-bit unsigned integer
16
- * @param {number} v - Value to rotate
17
- * @param {number} c - Number of bits to rotate
18
- * @returns {number} Rotated value
19
- */
20
- function rotl32(v, c) {
21
- return ((v << c) | (v >>> (32 - c))) >>> 0;
22
- }
23
-
24
- /**
25
- * ChaCha20 quarter round operation
26
- * Modifies state array in place
27
- * @param {Uint32Array} state - 16-element state array
28
- * @param {number} a - Index a
29
- * @param {number} b - Index b
30
- * @param {number} c - Index c
31
- * @param {number} d - Index d
32
- */
33
- function quarterRound(state, a, b, c, d) {
34
- state[a] = (state[a] + state[b]) >>> 0;
35
- state[d] = rotl32(state[d] ^ state[a], 16);
36
-
37
- state[c] = (state[c] + state[d]) >>> 0;
38
- state[b] = rotl32(state[b] ^ state[c], 12);
39
-
40
- state[a] = (state[a] + state[b]) >>> 0;
41
- state[d] = rotl32(state[d] ^ state[a], 8);
42
-
43
- state[c] = (state[c] + state[d]) >>> 0;
44
- state[b] = rotl32(state[b] ^ state[c], 7);
45
- }
46
-
47
- /**
48
- * Creates initial ChaCha20 state from key, counter, and nonce
49
- * @param {Uint8Array} key - 32-byte key
50
- * @param {number} counter - 32-bit block counter
51
- * @param {Uint8Array} nonce - 12-byte nonce
52
- * @returns {Uint32Array} Initial state (16 x 32-bit words)
53
- */
54
- function createState(key, counter, nonce) {
55
- const state = new Uint32Array(16);
56
- const keyView = new DataView(key.buffer, key.byteOffset, key.byteLength);
57
- const nonceView = new DataView(nonce.buffer, nonce.byteOffset, nonce.byteLength);
58
-
59
- // Constants
60
- state[0] = CONSTANTS[0];
61
- state[1] = CONSTANTS[1];
62
- state[2] = CONSTANTS[2];
63
- state[3] = CONSTANTS[3];
64
-
65
- // Key (little-endian)
66
- for (let i = 0; i < 8; i++) {
67
- state[4 + i] = keyView.getUint32(i * 4, true);
68
- }
69
-
70
- // Counter
71
- state[12] = counter >>> 0;
72
-
73
- // Nonce (little-endian)
74
- for (let i = 0; i < 3; i++) {
75
- state[13 + i] = nonceView.getUint32(i * 4, true);
76
- }
77
-
78
- return state;
79
- }
80
-
81
- /**
82
- * Computes a single ChaCha20 block
83
- * @param {Uint32Array} state - 16-element initial state
84
- * @returns {Uint32Array} 16-element output state
85
- */
86
- function chacha20Block(state) {
87
- if (!(state instanceof Uint32Array) || state.length !== 16) {
88
- throw new Error('State must be a 16-element Uint32Array');
89
- }
90
-
91
- const working = new Uint32Array(state);
92
-
93
- // 20 rounds = 10 double rounds
94
- for (let i = 0; i < 10; i++) {
95
- // Column rounds
96
- quarterRound(working, 0, 4, 8, 12);
97
- quarterRound(working, 1, 5, 9, 13);
98
- quarterRound(working, 2, 6, 10, 14);
99
- quarterRound(working, 3, 7, 11, 15);
100
- // Diagonal rounds
101
- quarterRound(working, 0, 5, 10, 15);
102
- quarterRound(working, 1, 6, 11, 12);
103
- quarterRound(working, 2, 7, 8, 13);
104
- quarterRound(working, 3, 4, 9, 14);
105
- }
106
-
107
- // Add original state to working state
108
- const output = new Uint32Array(16);
109
- for (let i = 0; i < 16; i++) {
110
- output[i] = (working[i] + state[i]) >>> 0;
111
- }
112
-
113
- return output;
114
- }
115
-
116
- /**
117
- * Serializes 32-bit words to bytes (little-endian)
118
- * @param {Uint32Array} words - Array of 32-bit words
119
- * @returns {Uint8Array} Byte array
120
- */
121
- function wordsToBytes(words) {
122
- const bytes = new Uint8Array(words.length * 4);
123
- const view = new DataView(bytes.buffer);
124
- for (let i = 0; i < words.length; i++) {
125
- view.setUint32(i * 4, words[i], true);
126
- }
127
- return bytes;
128
- }
129
-
130
- /**
131
- * ChaCha20 encryption/decryption (XOR with keystream)
132
- * @param {Uint8Array} key - 32-byte key
133
- * @param {Uint8Array} nonce - 12-byte nonce
134
- * @param {number} counter - Initial block counter
135
- * @param {Uint8Array} data - Data to encrypt/decrypt
136
- * @returns {Uint8Array} Encrypted/decrypted data
137
- * @throws {Error} If key is not 32 bytes or nonce is not 12 bytes
138
- */
139
- function chacha20(key, nonce, counter, data) {
140
- if (!(key instanceof Uint8Array) || key.length !== 32) {
141
- throw new Error('Key must be 32 bytes');
142
- }
143
- if (!(nonce instanceof Uint8Array) || nonce.length !== 12) {
144
- throw new Error('Nonce must be 12 bytes');
145
- }
146
- if (!(data instanceof Uint8Array)) {
147
- throw new Error('Data must be a Uint8Array');
148
- }
149
- if (typeof counter !== 'number' || counter < 0 || counter > 0xFFFFFFFF) {
150
- throw new Error('Counter must be a 32-bit unsigned integer');
151
- }
152
-
153
- const output = new Uint8Array(data.length);
154
- let offset = 0;
155
- let blockCounter = counter;
156
-
157
- while (offset < data.length) {
158
- const state = createState(key, blockCounter, nonce);
159
- const keystream = wordsToBytes(chacha20Block(state));
160
- const remaining = data.length - offset;
161
- const blockSize = Math.min(64, remaining);
162
-
163
- for (let i = 0; i < blockSize; i++) {
164
- output[offset + i] = data[offset + i] ^ keystream[i];
165
- }
166
-
167
- offset += blockSize;
168
- blockCounter = (blockCounter + 1) >>> 0;
169
-
170
- if (blockCounter === 0 && offset < data.length) {
171
- throw new Error('Counter overflow');
172
- }
173
- }
174
-
175
- return output;
176
- }
177
-
178
- module.exports = {
179
- chacha20Block,
180
- chacha20
181
- };
@@ -1,187 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * HKDF Key Derivation Function (RFC 5869)
5
- * Pure JavaScript implementation using HMAC-SHA256
6
- * @module crypto/hkdf
7
- */
8
-
9
- const { hmacSha256 } = require('./hmac');
10
-
11
- /**
12
- * Hash output length in bytes (SHA-256 = 32 bytes)
13
- * @constant {number}
14
- */
15
- const HASH_LENGTH = 32;
16
-
17
- /**
18
- * Maximum output length (255 * hash length)
19
- * @constant {number}
20
- */
21
- const MAX_OUTPUT_LENGTH = 255 * HASH_LENGTH;
22
-
23
- /**
24
- * Default salt (32 zero bytes for SHA-256)
25
- * @constant {Uint8Array}
26
- */
27
- const DEFAULT_SALT = new Uint8Array(HASH_LENGTH);
28
-
29
- /**
30
- * HKDF-Extract: Extract a pseudorandom key from input keying material
31
- *
32
- * PRK = HMAC-Hash(salt, IKM)
33
- *
34
- * @param {Uint8Array|null} salt - Optional salt value (non-secret random value)
35
- * If null/undefined/empty, uses zeros
36
- * @param {Uint8Array} ikm - Input keying material
37
- * @returns {Uint8Array} Pseudorandom key (32 bytes)
38
- * @throws {TypeError} If ikm is not a Uint8Array
39
- *
40
- * @example
41
- * const ikm = new Uint8Array([...sharedSecret]);
42
- * const salt = crypto.getRandomValues(new Uint8Array(32));
43
- * const prk = extract(salt, ikm);
44
- */
45
- function extract(salt, ikm) {
46
- if (!(ikm instanceof Uint8Array)) {
47
- throw new TypeError('IKM must be a Uint8Array');
48
- }
49
-
50
- // Use default salt if not provided or empty
51
- const actualSalt = (salt && salt.length > 0) ? salt : DEFAULT_SALT;
52
-
53
- return hmacSha256(actualSalt, ikm);
54
- }
55
-
56
- /**
57
- * HKDF-Expand: Expand a pseudorandom key to desired length
58
- *
59
- * T(0) = empty string
60
- * T(1) = HMAC-Hash(PRK, T(0) | info | 0x01)
61
- * T(2) = HMAC-Hash(PRK, T(1) | info | 0x02)
62
- * ...
63
- * OKM = first L bytes of T(1) | T(2) | ...
64
- *
65
- * @param {Uint8Array} prk - Pseudorandom key (from extract)
66
- * @param {Uint8Array} info - Context and application specific info
67
- * @param {number} length - Desired output length in bytes (max 8160)
68
- * @returns {Uint8Array} Output keying material of specified length
69
- * @throws {TypeError} If prk or info is not a Uint8Array
70
- * @throws {RangeError} If length exceeds maximum (255 * 32 = 8160 bytes)
71
- *
72
- * @example
73
- * const prk = extract(salt, ikm);
74
- * const info = new TextEncoder().encode('encryption-key');
75
- * const key = expand(prk, info, 32);
76
- */
77
- function expand(prk, info, length) {
78
- if (!(prk instanceof Uint8Array)) {
79
- throw new TypeError('PRK must be a Uint8Array');
80
- }
81
- if (!(info instanceof Uint8Array)) {
82
- throw new TypeError('Info must be a Uint8Array');
83
- }
84
- if (!Number.isInteger(length) || length < 0) {
85
- throw new TypeError('Length must be a non-negative integer');
86
- }
87
- if (length > MAX_OUTPUT_LENGTH) {
88
- throw new RangeError(`Length must not exceed ${MAX_OUTPUT_LENGTH} bytes`);
89
- }
90
- if (length === 0) {
91
- return new Uint8Array(0);
92
- }
93
-
94
- // Calculate number of iterations needed
95
- const n = Math.ceil(length / HASH_LENGTH);
96
- const okm = new Uint8Array(n * HASH_LENGTH);
97
-
98
- // T(0) = empty, T(i) = HMAC(PRK, T(i-1) | info | i)
99
- let previous = new Uint8Array(0);
100
-
101
- for (let i = 1; i <= n; i++) {
102
- // Construct input: T(i-1) | info | counter
103
- const inputLength = previous.length + info.length + 1;
104
- const input = new Uint8Array(inputLength);
105
-
106
- let offset = 0;
107
- if (previous.length > 0) {
108
- input.set(previous, offset);
109
- offset += previous.length;
110
- }
111
- input.set(info, offset);
112
- offset += info.length;
113
- input[offset] = i; // Counter byte (1-indexed)
114
-
115
- // T(i) = HMAC-Hash(PRK, input)
116
- previous = hmacSha256(prk, input);
117
-
118
- // Copy to output
119
- okm.set(previous, (i - 1) * HASH_LENGTH);
120
- }
121
-
122
- // Return only the requested number of bytes
123
- return okm.subarray(0, length);
124
- }
125
-
126
- /**
127
- * HKDF: Combined extract-then-expand operation
128
- *
129
- * Convenience function that performs both HKDF-Extract and HKDF-Expand
130
- *
131
- * @param {Uint8Array} ikm - Input keying material
132
- * @param {Uint8Array|null} salt - Optional salt (uses zeros if null/empty)
133
- * @param {Uint8Array} info - Context and application specific info
134
- * @param {number} length - Desired output length in bytes
135
- * @returns {Uint8Array} Derived key material of specified length
136
- * @throws {TypeError} If ikm or info is not a Uint8Array
137
- * @throws {RangeError} If length exceeds maximum
138
- *
139
- * @example
140
- * const sharedSecret = performDH(myPrivate, theirPublic);
141
- * const info = new TextEncoder().encode('noise-handshake-v1');
142
- * const key = derive(sharedSecret, null, info, 32);
143
- */
144
- function derive(ikm, salt, info, length) {
145
- const prk = extract(salt, ikm);
146
- return expand(prk, info, length);
147
- }
148
-
149
- /**
150
- * Derive multiple keys in one operation
151
- * Useful for deriving encryption and MAC keys together
152
- *
153
- * @param {Uint8Array} ikm - Input keying material
154
- * @param {Uint8Array|null} salt - Optional salt
155
- * @param {Uint8Array} info - Context info
156
- * @param {number[]} lengths - Array of key lengths to derive
157
- * @returns {Uint8Array[]} Array of derived keys
158
- *
159
- * @example
160
- * const [encKey, macKey] = deriveMultiple(secret, salt, info, [32, 32]);
161
- */
162
- function deriveMultiple(ikm, salt, info, lengths) {
163
- if (!Array.isArray(lengths) || lengths.length === 0) {
164
- throw new TypeError('Lengths must be a non-empty array');
165
- }
166
-
167
- const totalLength = lengths.reduce((sum, len) => sum + len, 0);
168
- const combined = derive(ikm, salt, info, totalLength);
169
-
170
- const keys = [];
171
- let offset = 0;
172
- for (const len of lengths) {
173
- keys.push(combined.slice(offset, offset + len));
174
- offset += len;
175
- }
176
-
177
- return keys;
178
- }
179
-
180
- module.exports = {
181
- extract,
182
- expand,
183
- derive,
184
- deriveMultiple,
185
- HASH_LENGTH,
186
- MAX_OUTPUT_LENGTH
187
- };