94 lines
2.3 KiB
JavaScript
94 lines
2.3 KiB
JavaScript
export async function deriveKeyFromSecret(secret, salt = 'toolbox_salt_v1') {
|
|
if (!secret || typeof secret !== 'string') {
|
|
throw new Error('Invalid secret: must be a non-empty string');
|
|
}
|
|
|
|
const enc = new TextEncoder();
|
|
const secretKey = await crypto.subtle.importKey(
|
|
'raw',
|
|
enc.encode(secret),
|
|
{ name: 'PBKDF2' },
|
|
false,
|
|
['deriveKey']
|
|
);
|
|
|
|
const key = await crypto.subtle.deriveKey(
|
|
{
|
|
name: 'PBKDF2',
|
|
salt: enc.encode(salt),
|
|
iterations: 100000,
|
|
hash: 'SHA-256'
|
|
},
|
|
secretKey,
|
|
{ name: 'AES-GCM', length: 256 },
|
|
true,
|
|
['encrypt', 'decrypt']
|
|
);
|
|
|
|
return key;
|
|
}
|
|
|
|
function _randBytes(len = 12) {
|
|
const b = new Uint8Array(len);
|
|
crypto.getRandomValues(b);
|
|
return b;
|
|
}
|
|
|
|
export async function encryptJSON(key, obj) {
|
|
if (!key || !(key instanceof CryptoKey)) {
|
|
throw new Error('Valid CryptoKey is required');
|
|
}
|
|
|
|
const enc = new TextEncoder();
|
|
const iv = _randBytes(12);
|
|
const plain = enc.encode(JSON.stringify(obj));
|
|
|
|
const cipher = await crypto.subtle.encrypt(
|
|
{ name: 'AES-GCM', iv: iv },
|
|
key,
|
|
plain
|
|
);
|
|
|
|
const combined = new Uint8Array(iv.byteLength + cipher.byteLength);
|
|
combined.set(iv, 0);
|
|
combined.set(new Uint8Array(cipher), iv.byteLength);
|
|
|
|
// Безопасное преобразование в base64
|
|
const binaryString = Array.from(combined, byte =>
|
|
String.fromCharCode(byte)).join('');
|
|
return btoa(binaryString);
|
|
}
|
|
|
|
export async function decryptToJSON(key, b64) {
|
|
if (!key || !(key instanceof CryptoKey)) {
|
|
throw new Error('Valid CryptoKey is required');
|
|
}
|
|
|
|
if (!b64 || typeof b64 !== 'string') {
|
|
throw new Error('Invalid base64 string');
|
|
}
|
|
|
|
try {
|
|
// Безопасное преобразование из base64
|
|
const binaryString = atob(b64);
|
|
const raw = new Uint8Array(binaryString.length);
|
|
for (let i = 0; i < binaryString.length; i++) {
|
|
raw[i] = binaryString.charCodeAt(i);
|
|
}
|
|
|
|
const iv = raw.slice(0, 12);
|
|
const cipher = raw.slice(12);
|
|
|
|
const plainBuf = await crypto.subtle.decrypt(
|
|
{ name: 'AES-GCM', iv: iv },
|
|
key,
|
|
cipher
|
|
);
|
|
|
|
const dec = new TextDecoder();
|
|
return JSON.parse(dec.decode(plainBuf));
|
|
} catch (error) {
|
|
console.error('Decryption error:', error);
|
|
throw new Error('Failed to decrypt data: ' + error.message);
|
|
}
|
|
} |