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.
- package/README.md +288 -172
- package/docs/IOS-BACKGROUND-BLE.md +231 -0
- package/docs/OPTIMIZATION.md +183 -0
- package/docs/SPEC-v2.1.md +308 -0
- package/package.json +1 -1
- package/src/MeshNetwork.js +667 -465
- package/src/constants/index.js +1 -0
- package/src/crypto/AutoCrypto.js +90 -0
- package/src/crypto/CryptoProvider.js +99 -0
- package/src/crypto/index.js +15 -63
- package/src/crypto/providers/ExpoCryptoProvider.js +126 -0
- package/src/crypto/providers/QuickCryptoProvider.js +158 -0
- package/src/crypto/providers/TweetNaClProvider.js +124 -0
- package/src/crypto/providers/index.js +11 -0
- package/src/errors/MeshError.js +2 -1
- package/src/expo/withBLEMesh.js +102 -0
- package/src/hooks/AppStateManager.js +9 -1
- package/src/hooks/useMesh.js +47 -13
- package/src/hooks/useMessages.js +6 -4
- package/src/hooks/usePeers.js +13 -9
- package/src/index.js +23 -8
- package/src/mesh/dedup/BloomFilter.js +44 -57
- package/src/mesh/dedup/DedupManager.js +67 -10
- package/src/mesh/fragment/Assembler.js +5 -0
- package/src/mesh/fragment/Fragmenter.js +1 -1
- package/src/mesh/index.js +1 -1
- package/src/mesh/monitor/ConnectionQuality.js +433 -0
- package/src/mesh/monitor/NetworkMonitor.js +376 -320
- package/src/mesh/monitor/index.js +7 -3
- package/src/mesh/peer/Peer.js +5 -2
- package/src/mesh/peer/PeerManager.js +21 -4
- package/src/mesh/router/MessageRouter.js +38 -19
- package/src/mesh/router/RouteTable.js +24 -8
- package/src/mesh/store/StoreAndForwardManager.js +305 -296
- package/src/mesh/store/index.js +1 -1
- package/src/protocol/deserializer.js +9 -10
- package/src/protocol/header.js +13 -7
- package/src/protocol/message.js +15 -3
- package/src/protocol/serializer.js +7 -10
- package/src/protocol/validator.js +23 -5
- package/src/service/BatteryOptimizer.js +285 -278
- package/src/service/EmergencyManager.js +224 -214
- package/src/service/HandshakeManager.js +163 -13
- package/src/service/MeshService.js +72 -6
- package/src/service/SessionManager.js +79 -2
- package/src/service/audio/AudioManager.js +8 -2
- package/src/service/file/FileAssembler.js +106 -0
- package/src/service/file/FileChunker.js +79 -0
- package/src/service/file/FileManager.js +307 -0
- package/src/service/file/FileMessage.js +122 -0
- package/src/service/file/index.js +15 -0
- package/src/service/text/TextManager.js +21 -15
- package/src/service/text/broadcast/BroadcastManager.js +16 -0
- package/src/storage/MessageStore.js +55 -2
- package/src/transport/BLETransport.js +141 -10
- package/src/transport/MockTransport.js +1 -1
- package/src/transport/MultiTransport.js +330 -0
- package/src/transport/WiFiDirectTransport.js +296 -0
- package/src/transport/adapters/NodeBLEAdapter.js +34 -0
- package/src/transport/adapters/RNBLEAdapter.js +56 -1
- package/src/transport/index.js +6 -0
- package/src/utils/EventEmitter.js +6 -9
- package/src/utils/bytes.js +12 -10
- package/src/utils/compression.js +293 -291
- package/src/utils/encoding.js +33 -8
- package/src/crypto/aead.js +0 -189
- package/src/crypto/chacha20.js +0 -181
- package/src/crypto/hkdf.js +0 -187
- package/src/crypto/hmac.js +0 -143
- package/src/crypto/keys/KeyManager.js +0 -271
- package/src/crypto/keys/KeyPair.js +0 -216
- package/src/crypto/keys/SecureStorage.js +0 -219
- package/src/crypto/keys/index.js +0 -32
- package/src/crypto/noise/handshake.js +0 -410
- package/src/crypto/noise/index.js +0 -27
- package/src/crypto/noise/session.js +0 -253
- package/src/crypto/noise/state.js +0 -268
- package/src/crypto/poly1305.js +0 -113
- package/src/crypto/sha256.js +0 -240
- package/src/crypto/x25519.js +0 -154
package/src/utils/encoding.js
CHANGED
|
@@ -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
|
-
|
|
47
|
+
const parts = new Array(bytes.length);
|
|
24
48
|
for (let i = 0; i < bytes.length; i++) {
|
|
25
|
-
|
|
26
|
-
result += HEX_CHARS[bytes[i] & 0x0f];
|
|
49
|
+
parts[i] = HEX_TABLE[bytes[i]];
|
|
27
50
|
}
|
|
28
|
-
return
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
|
|
103
|
-
|
|
126
|
+
const decoder = _getDecoder();
|
|
127
|
+
if (decoder) {
|
|
128
|
+
return decoder.decode(bytes);
|
|
104
129
|
}
|
|
105
130
|
|
|
106
131
|
// Fallback for environments without TextDecoder
|
package/src/crypto/aead.js
DELETED
|
@@ -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
|
-
};
|
package/src/crypto/chacha20.js
DELETED
|
@@ -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
|
-
};
|
package/src/crypto/hkdf.js
DELETED
|
@@ -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
|
-
};
|