react-native-ble-mesh 1.1.1 → 2.0.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 +70 -0
- package/docs/SPEC-v2.1.md +308 -0
- package/package.json +1 -1
- package/src/MeshNetwork.js +659 -465
- package/src/constants/index.js +1 -0
- package/src/crypto/AutoCrypto.js +79 -0
- package/src/crypto/CryptoProvider.js +99 -0
- package/src/crypto/index.js +15 -63
- package/src/crypto/providers/ExpoCryptoProvider.js +125 -0
- package/src/crypto/providers/QuickCryptoProvider.js +134 -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/useMesh.js +30 -9
- package/src/hooks/useMessages.js +2 -0
- package/src/index.js +23 -8
- package/src/mesh/dedup/DedupManager.js +36 -10
- package/src/mesh/fragment/Assembler.js +5 -0
- package/src/mesh/index.js +1 -1
- package/src/mesh/monitor/ConnectionQuality.js +408 -0
- package/src/mesh/monitor/NetworkMonitor.js +327 -316
- package/src/mesh/monitor/index.js +7 -3
- package/src/mesh/peer/PeerManager.js +6 -1
- package/src/mesh/router/MessageRouter.js +26 -15
- package/src/mesh/router/RouteTable.js +7 -1
- package/src/mesh/store/StoreAndForwardManager.js +295 -297
- package/src/mesh/store/index.js +1 -1
- package/src/service/BatteryOptimizer.js +282 -278
- package/src/service/EmergencyManager.js +224 -214
- package/src/service/HandshakeManager.js +167 -13
- package/src/service/MeshService.js +72 -6
- package/src/service/SessionManager.js +77 -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/broadcast/BroadcastManager.js +16 -0
- package/src/transport/BLETransport.js +131 -9
- package/src/transport/MockTransport.js +1 -1
- package/src/transport/MultiTransport.js +305 -0
- package/src/transport/WiFiDirectTransport.js +295 -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/compression.js +291 -291
- 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/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
|
-
};
|
package/src/crypto/hmac.js
DELETED
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* HMAC-SHA256 Implementation (RFC 2104)
|
|
5
|
-
* Pure JavaScript implementation for BLE Mesh Network
|
|
6
|
-
* @module crypto/hmac
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
const { hash } = require('./sha256');
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* HMAC block size in bytes (SHA-256 uses 64-byte blocks)
|
|
13
|
-
* @constant {number}
|
|
14
|
-
*/
|
|
15
|
-
const BLOCK_SIZE = 64;
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Inner padding byte value
|
|
19
|
-
* @constant {number}
|
|
20
|
-
*/
|
|
21
|
-
const IPAD = 0x36;
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Outer padding byte value
|
|
25
|
-
* @constant {number}
|
|
26
|
-
*/
|
|
27
|
-
const OPAD = 0x5c;
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Compute HMAC-SHA256 of data with given key
|
|
31
|
-
*
|
|
32
|
-
* HMAC is computed as:
|
|
33
|
-
* HMAC(K, m) = H((K' XOR opad) || H((K' XOR ipad) || m))
|
|
34
|
-
*
|
|
35
|
-
* Where K' is the key padded/hashed to block size
|
|
36
|
-
*
|
|
37
|
-
* @param {Uint8Array} key - Secret key (any length)
|
|
38
|
-
* @param {Uint8Array} data - Message to authenticate
|
|
39
|
-
* @returns {Uint8Array} 32-byte HMAC digest
|
|
40
|
-
* @throws {TypeError} If key or data is not a Uint8Array
|
|
41
|
-
*
|
|
42
|
-
* @example
|
|
43
|
-
* const key = new Uint8Array([0x0b, 0x0b, ...]);
|
|
44
|
-
* const data = new TextEncoder().encode('Hi There');
|
|
45
|
-
* const mac = hmacSha256(key, data);
|
|
46
|
-
*/
|
|
47
|
-
function hmacSha256(key, data) {
|
|
48
|
-
if (!(key instanceof Uint8Array)) {
|
|
49
|
-
throw new TypeError('Key must be a Uint8Array');
|
|
50
|
-
}
|
|
51
|
-
if (!(data instanceof Uint8Array)) {
|
|
52
|
-
throw new TypeError('Data must be a Uint8Array');
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Step 1: Prepare the key
|
|
56
|
-
// If key is longer than block size, hash it first
|
|
57
|
-
// If shorter, it will be padded with zeros
|
|
58
|
-
let keyPrime;
|
|
59
|
-
if (key.length > BLOCK_SIZE) {
|
|
60
|
-
keyPrime = hash(key);
|
|
61
|
-
} else {
|
|
62
|
-
keyPrime = key;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Step 2: Create padded key blocks
|
|
66
|
-
const keyPadded = new Uint8Array(BLOCK_SIZE);
|
|
67
|
-
keyPadded.set(keyPrime);
|
|
68
|
-
// Remaining bytes are already 0 from Uint8Array initialization
|
|
69
|
-
|
|
70
|
-
// Step 3: Create inner and outer padded keys
|
|
71
|
-
const innerKey = new Uint8Array(BLOCK_SIZE);
|
|
72
|
-
const outerKey = new Uint8Array(BLOCK_SIZE);
|
|
73
|
-
|
|
74
|
-
for (let i = 0; i < BLOCK_SIZE; i++) {
|
|
75
|
-
innerKey[i] = keyPadded[i] ^ IPAD;
|
|
76
|
-
outerKey[i] = keyPadded[i] ^ OPAD;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Step 4: Compute inner hash: H((K' XOR ipad) || message)
|
|
80
|
-
const innerData = new Uint8Array(BLOCK_SIZE + data.length);
|
|
81
|
-
innerData.set(innerKey);
|
|
82
|
-
innerData.set(data, BLOCK_SIZE);
|
|
83
|
-
const innerHash = hash(innerData);
|
|
84
|
-
|
|
85
|
-
// Step 5: Compute outer hash: H((K' XOR opad) || innerHash)
|
|
86
|
-
const outerData = new Uint8Array(BLOCK_SIZE + 32);
|
|
87
|
-
outerData.set(outerKey);
|
|
88
|
-
outerData.set(innerHash, BLOCK_SIZE);
|
|
89
|
-
|
|
90
|
-
return hash(outerData);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Verify an HMAC-SHA256 digest
|
|
95
|
-
* Uses constant-time comparison to prevent timing attacks
|
|
96
|
-
*
|
|
97
|
-
* @param {Uint8Array} key - Secret key
|
|
98
|
-
* @param {Uint8Array} data - Message that was authenticated
|
|
99
|
-
* @param {Uint8Array} expectedMac - Expected HMAC digest to verify against
|
|
100
|
-
* @returns {boolean} True if MAC is valid, false otherwise
|
|
101
|
-
*
|
|
102
|
-
* @example
|
|
103
|
-
* const isValid = verifyHmac(key, data, receivedMac);
|
|
104
|
-
* if (!isValid) {
|
|
105
|
-
* throw new Error('Message authentication failed');
|
|
106
|
-
* }
|
|
107
|
-
*/
|
|
108
|
-
function verifyHmac(key, data, expectedMac) {
|
|
109
|
-
if (!(expectedMac instanceof Uint8Array) || expectedMac.length !== 32) {
|
|
110
|
-
return false;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const computedMac = hmacSha256(key, data);
|
|
114
|
-
return constantTimeEqual(computedMac, expectedMac);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Constant-time comparison of two byte arrays
|
|
119
|
-
* Prevents timing attacks by always comparing all bytes
|
|
120
|
-
*
|
|
121
|
-
* @param {Uint8Array} a - First array
|
|
122
|
-
* @param {Uint8Array} b - Second array
|
|
123
|
-
* @returns {boolean} True if arrays are equal
|
|
124
|
-
* @private
|
|
125
|
-
*/
|
|
126
|
-
function constantTimeEqual(a, b) {
|
|
127
|
-
if (a.length !== b.length) {
|
|
128
|
-
return false;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
let result = 0;
|
|
132
|
-
for (let i = 0; i < a.length; i++) {
|
|
133
|
-
result |= a[i] ^ b[i];
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return result === 0;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
module.exports = {
|
|
140
|
-
hmacSha256,
|
|
141
|
-
verifyHmac,
|
|
142
|
-
BLOCK_SIZE
|
|
143
|
-
};
|