-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathmaster-key-derivation.js
More file actions
237 lines (209 loc) Β· 6.96 KB
/
master-key-derivation.js
File metadata and controls
237 lines (209 loc) Β· 6.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
/**
* @fileoverview Master key derivation from user credentials
* Derives encryption keys from phone number + PIN/password using PBKDF2
*/
import { browser } from '$app/environment';
/**
* Master key derivation service
* Uses PBKDF2 with SHA-256 to derive keys from user credentials
*/
export class MasterKeyDerivation {
/**
* Derive master key from phone number and PIN
* @param {string} phoneNumber - User's phone number
* @param {string} pin - User's PIN or password
* @param {number} iterations - PBKDF2 iterations (default: 100000)
* @returns {Promise<Uint8Array>} 256-bit master key
*/
static async deriveFromCredentials(phoneNumber, pin, iterations = 100000) {
if (!browser) {
throw new Error('Master key derivation only available in browser');
}
try {
// Create salt from phone number (deterministic but unique per user)
const salt = await this.createPhoneSalt(phoneNumber);
return await this.deriveWithSalt(phoneNumber, pin, salt, iterations);
} catch (error) {
console.error('π Failed to derive master key from credentials:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
throw new Error(`Master key derivation failed: ${errorMessage}`);
}
}
/**
* Derive master key with custom salt
* @param {string} phoneNumber - User's phone number
* @param {string} pin - User's PIN or password
* @param {Uint8Array} salt - Custom salt
* @param {number} iterations - PBKDF2 iterations
* @returns {Promise<Uint8Array>} 256-bit master key
*/
static async deriveWithSalt(phoneNumber, pin, salt, iterations = 100000) {
if (!browser) {
throw new Error('Master key derivation only available in browser');
}
try {
// Combine phone number and PIN as password
const password = `${phoneNumber}:${pin}`;
const passwordBuffer = new TextEncoder().encode(password);
// Import password as key material
const keyMaterial = await crypto.subtle.importKey(
'raw',
passwordBuffer,
{ name: 'PBKDF2' },
false,
['deriveKey']
);
// Derive 256-bit key using PBKDF2
const derivedKey = await crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: salt,
iterations: iterations,
hash: 'SHA-256'
},
keyMaterial,
{
name: 'AES-GCM',
length: 256
},
true, // extractable
['encrypt', 'decrypt']
);
// Export key as raw bytes
const keyBuffer = await crypto.subtle.exportKey('raw', derivedKey);
const masterKey = new Uint8Array(keyBuffer);
console.log('π Successfully derived master key');
return masterKey;
} catch (error) {
console.error('π Failed to derive master key with salt:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
throw new Error(`Master key derivation failed: ${errorMessage}`);
}
}
/**
* Create deterministic salt from phone number
* @param {string} phoneNumber - User's phone number
* @returns {Promise<Uint8Array>} 256-bit salt
*/
static async createPhoneSalt(phoneNumber) {
try {
// Use phone number + fixed string to create deterministic salt
const saltInput = `qryptchat-salt-${phoneNumber}`;
const saltBuffer = new TextEncoder().encode(saltInput);
// Hash to create 256-bit salt
const hashBuffer = await crypto.subtle.digest('SHA-256', saltBuffer);
return new Uint8Array(hashBuffer);
} catch (error) {
console.error('π Failed to create phone salt:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
throw new Error(`Salt creation failed: ${errorMessage}`);
}
}
/**
* Generate random salt for additional security
* @returns {Uint8Array} 256-bit random salt
*/
static generateSalt() {
const salt = new Uint8Array(32); // 256 bits
crypto.getRandomValues(salt);
return salt;
}
/**
* Verify PIN strength
* @param {string} pin - PIN to verify
* @returns {Object} Verification result with strength score
*/
static verifyPinStrength(pin) {
const result = {
isValid: false,
strength: 0,
issues: /** @type {string[]} */ ([])
};
if (!pin || typeof pin !== 'string') {
result.issues.push('PIN is required');
return result;
}
// Minimum length check
if (pin.length < 6) {
result.issues.push('PIN must be at least 6 characters');
} else {
result.strength += 20;
}
// Maximum length check
if (pin.length > 50) {
result.issues.push('PIN must be less than 50 characters');
return result;
}
// Character variety checks
const hasNumbers = /\d/.test(pin);
const hasLowercase = /[a-z]/.test(pin);
const hasUppercase = /[A-Z]/.test(pin);
const hasSpecialChars = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(pin);
if (hasNumbers) result.strength += 20;
if (hasLowercase) result.strength += 20;
if (hasUppercase) result.strength += 20;
if (hasSpecialChars) result.strength += 20;
// Length bonus
if (pin.length >= 8) result.strength += 10;
if (pin.length >= 12) result.strength += 10;
// Common pattern penalties
if (/^(\d)\1+$/.test(pin)) {
result.issues.push('PIN cannot be all the same digit');
result.strength -= 50;
}
if (/^(012|123|234|345|456|567|678|789|890|987|876|765|654|543|432|321|210)/.test(pin)) {
result.issues.push('PIN cannot be a simple sequence');
result.strength -= 30;
}
// Set validity
result.isValid = result.issues.length === 0 && result.strength >= 40;
result.strength = Math.max(0, Math.min(100, result.strength));
return result;
}
/**
* Create backup verification hash
* Used to verify PIN without storing it
* @param {string} phoneNumber - User's phone number
* @param {string} pin - User's PIN
* @returns {Promise<string>} Base64 verification hash
*/
static async createVerificationHash(phoneNumber, pin) {
try {
const input = `verify-${phoneNumber}-${pin}`;
const inputBuffer = new TextEncoder().encode(input);
const hashBuffer = await crypto.subtle.digest('SHA-256', inputBuffer);
const hashArray = new Uint8Array(hashBuffer);
// Convert to base64
return btoa(String.fromCharCode(...hashArray));
} catch (error) {
console.error('π Failed to create verification hash:', error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
throw new Error(`Verification hash creation failed: ${errorMessage}`);
}
}
/**
* Verify PIN against stored hash
* @param {string} phoneNumber - User's phone number
* @param {string} pin - PIN to verify
* @param {string} storedHash - Stored verification hash
* @returns {Promise<boolean>} Whether PIN is correct
*/
static async verifyPin(phoneNumber, pin, storedHash) {
try {
const computedHash = await this.createVerificationHash(phoneNumber, pin);
return computedHash === storedHash;
} catch (error) {
console.error('π Failed to verify PIN:', error);
return false;
}
}
/**
* Securely clear sensitive data from memory
* @param {Uint8Array} data - Data to clear
*/
static secureClear(data) {
if (data && data.fill) {
data.fill(0);
}
}
}