415gotit
This commit is contained in:
@@ -1,289 +1,66 @@
|
||||
'use strict';
|
||||
/**
|
||||
* Crypto Mock
|
||||
*
|
||||
* Web Crypto API implementation using Node.js crypto module.
|
||||
* P1: Crypto / Storage / IDBFactory / atob / btoa mock
|
||||
*/
|
||||
|
||||
import nodeCrypto from 'crypto';
|
||||
const { createNative, nativeClass } = require('./native');
|
||||
const nodeCrypto = require('crypto');
|
||||
|
||||
export function createCrypto() {
|
||||
return {
|
||||
getRandomValues(array) {
|
||||
const bytes = nodeCrypto.randomBytes(array.byteLength);
|
||||
const view = new Uint8Array(array.buffer, array.byteOffset, array.byteLength);
|
||||
view.set(new Uint8Array(bytes));
|
||||
return array;
|
||||
},
|
||||
// ── Crypto ───────────────────────────────────────────────────
|
||||
const cryptoMock = {
|
||||
getRandomValues: createNative('getRandomValues', function (array) {
|
||||
return nodeCrypto.randomFillSync(array);
|
||||
}),
|
||||
randomUUID: createNative('randomUUID', function () {
|
||||
return nodeCrypto.randomUUID();
|
||||
}),
|
||||
subtle: {
|
||||
digest: createNative('digest', function () { return Promise.resolve(new ArrayBuffer(32)); }),
|
||||
encrypt: createNative('encrypt', function () { return Promise.resolve(new ArrayBuffer(0)); }),
|
||||
decrypt: createNative('decrypt', function () { return Promise.resolve(new ArrayBuffer(0)); }),
|
||||
sign: createNative('sign', function () { return Promise.resolve(new ArrayBuffer(32)); }),
|
||||
verify: createNative('verify', function () { return Promise.resolve(true); }),
|
||||
generateKey: createNative('generateKey', function () { return Promise.resolve({}); }),
|
||||
importKey: createNative('importKey', function () { return Promise.resolve({}); }),
|
||||
exportKey: createNative('exportKey', function () { return Promise.resolve({}); }),
|
||||
},
|
||||
};
|
||||
|
||||
randomUUID() {
|
||||
return nodeCrypto.randomUUID();
|
||||
},
|
||||
|
||||
subtle: {
|
||||
async digest(algorithm, data) {
|
||||
const algoName = typeof algorithm === 'string'
|
||||
? algorithm
|
||||
: algorithm.name;
|
||||
|
||||
const hashMap = {
|
||||
'SHA-1': 'sha1',
|
||||
'SHA-256': 'sha256',
|
||||
'SHA-384': 'sha384',
|
||||
'SHA-512': 'sha512',
|
||||
};
|
||||
|
||||
const nodeAlgo = hashMap[algoName.toUpperCase()] || 'sha256';
|
||||
const hash = nodeCrypto.createHash(nodeAlgo);
|
||||
|
||||
// Handle different data types
|
||||
if (data instanceof ArrayBuffer) {
|
||||
hash.update(Buffer.from(data));
|
||||
} else if (ArrayBuffer.isView(data)) {
|
||||
hash.update(Buffer.from(data.buffer, data.byteOffset, data.byteLength));
|
||||
} else {
|
||||
hash.update(Buffer.from(data));
|
||||
}
|
||||
|
||||
const result = hash.digest();
|
||||
return result.buffer.slice(result.byteOffset, result.byteOffset + result.byteLength);
|
||||
},
|
||||
|
||||
async encrypt(algorithm, key, data) {
|
||||
const algoName = algorithm.name || algorithm;
|
||||
|
||||
if (algoName === 'AES-GCM') {
|
||||
const cipher = nodeCrypto.createCipheriv(
|
||||
'aes-256-gcm',
|
||||
Buffer.from(key.key || key),
|
||||
Buffer.from(algorithm.iv)
|
||||
);
|
||||
|
||||
const encrypted = Buffer.concat([
|
||||
cipher.update(Buffer.from(data)),
|
||||
cipher.final(),
|
||||
cipher.getAuthTag()
|
||||
]);
|
||||
|
||||
return encrypted.buffer;
|
||||
}
|
||||
|
||||
if (algoName === 'AES-CBC') {
|
||||
const cipher = nodeCrypto.createCipheriv(
|
||||
'aes-256-cbc',
|
||||
Buffer.from(key.key || key),
|
||||
Buffer.from(algorithm.iv)
|
||||
);
|
||||
|
||||
const encrypted = Buffer.concat([
|
||||
cipher.update(Buffer.from(data)),
|
||||
cipher.final()
|
||||
]);
|
||||
|
||||
return encrypted.buffer;
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported encryption algorithm: ${algoName}`);
|
||||
},
|
||||
|
||||
async decrypt(algorithm, key, data) {
|
||||
const algoName = algorithm.name || algorithm;
|
||||
|
||||
if (algoName === 'AES-GCM') {
|
||||
const buffer = Buffer.from(data);
|
||||
const authTag = buffer.slice(-16);
|
||||
const encrypted = buffer.slice(0, -16);
|
||||
|
||||
const decipher = nodeCrypto.createDecipheriv(
|
||||
'aes-256-gcm',
|
||||
Buffer.from(key.key || key),
|
||||
Buffer.from(algorithm.iv)
|
||||
);
|
||||
decipher.setAuthTag(authTag);
|
||||
|
||||
const decrypted = Buffer.concat([
|
||||
decipher.update(encrypted),
|
||||
decipher.final()
|
||||
]);
|
||||
|
||||
return decrypted.buffer;
|
||||
}
|
||||
|
||||
if (algoName === 'AES-CBC') {
|
||||
const decipher = nodeCrypto.createDecipheriv(
|
||||
'aes-256-cbc',
|
||||
Buffer.from(key.key || key),
|
||||
Buffer.from(algorithm.iv)
|
||||
);
|
||||
|
||||
const decrypted = Buffer.concat([
|
||||
decipher.update(Buffer.from(data)),
|
||||
decipher.final()
|
||||
]);
|
||||
|
||||
return decrypted.buffer;
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported decryption algorithm: ${algoName}`);
|
||||
},
|
||||
|
||||
async sign(algorithm, key, data) {
|
||||
const algoName = algorithm.name || algorithm;
|
||||
|
||||
if (algoName === 'HMAC') {
|
||||
const hashAlgo = algorithm.hash?.name || 'SHA-256';
|
||||
const nodeHash = hashAlgo.replace('-', '').toLowerCase();
|
||||
|
||||
const hmac = nodeCrypto.createHmac(nodeHash, Buffer.from(key.key || key));
|
||||
hmac.update(Buffer.from(data));
|
||||
|
||||
return hmac.digest().buffer;
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported signing algorithm: ${algoName}`);
|
||||
},
|
||||
|
||||
async verify(algorithm, key, signature, data) {
|
||||
const expected = await this.sign(algorithm, key, data);
|
||||
const sig = Buffer.from(signature);
|
||||
const exp = Buffer.from(expected);
|
||||
|
||||
return sig.length === exp.length && nodeCrypto.timingSafeEqual(sig, exp);
|
||||
},
|
||||
|
||||
async generateKey(algorithm, extractable, keyUsages) {
|
||||
const algoName = algorithm.name || algorithm;
|
||||
|
||||
if (algoName === 'AES-GCM' || algoName === 'AES-CBC') {
|
||||
const length = algorithm.length || 256;
|
||||
const key = nodeCrypto.randomBytes(length / 8);
|
||||
|
||||
return {
|
||||
type: 'secret',
|
||||
extractable,
|
||||
algorithm: { name: algoName, length },
|
||||
usages: keyUsages,
|
||||
key,
|
||||
};
|
||||
}
|
||||
|
||||
if (algoName === 'HMAC') {
|
||||
const hashAlgo = algorithm.hash?.name || 'SHA-256';
|
||||
const length = algorithm.length || 256;
|
||||
const key = nodeCrypto.randomBytes(length / 8);
|
||||
|
||||
return {
|
||||
type: 'secret',
|
||||
extractable,
|
||||
algorithm: { name: algoName, hash: { name: hashAlgo }, length },
|
||||
usages: keyUsages,
|
||||
key,
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported key generation algorithm: ${algoName}`);
|
||||
},
|
||||
|
||||
async importKey(format, keyData, algorithm, extractable, keyUsages) {
|
||||
const algoName = algorithm.name || algorithm;
|
||||
|
||||
let key;
|
||||
if (format === 'raw') {
|
||||
key = Buffer.from(keyData);
|
||||
} else if (format === 'jwk') {
|
||||
// Basic JWK support
|
||||
key = Buffer.from(keyData.k, 'base64url');
|
||||
} else {
|
||||
throw new Error(`Unsupported key format: ${format}`);
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'secret',
|
||||
extractable,
|
||||
algorithm: typeof algorithm === 'string' ? { name: algorithm } : algorithm,
|
||||
usages: keyUsages,
|
||||
key,
|
||||
};
|
||||
},
|
||||
|
||||
async exportKey(format, key) {
|
||||
if (format === 'raw') {
|
||||
return key.key.buffer;
|
||||
}
|
||||
|
||||
if (format === 'jwk') {
|
||||
return {
|
||||
kty: 'oct',
|
||||
k: key.key.toString('base64url'),
|
||||
alg: key.algorithm.name,
|
||||
ext: key.extractable,
|
||||
key_ops: key.usages,
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported export format: ${format}`);
|
||||
},
|
||||
|
||||
async deriveBits(algorithm, baseKey, length) {
|
||||
const algoName = algorithm.name || algorithm;
|
||||
|
||||
if (algoName === 'PBKDF2') {
|
||||
const salt = Buffer.from(algorithm.salt);
|
||||
const iterations = algorithm.iterations;
|
||||
const hashAlgo = algorithm.hash?.name?.replace('-', '').toLowerCase() || 'sha256';
|
||||
|
||||
const derived = nodeCrypto.pbkdf2Sync(
|
||||
Buffer.from(baseKey.key || baseKey),
|
||||
salt,
|
||||
iterations,
|
||||
length / 8,
|
||||
hashAlgo
|
||||
);
|
||||
|
||||
return derived.buffer;
|
||||
}
|
||||
|
||||
if (algoName === 'HKDF') {
|
||||
const salt = Buffer.from(algorithm.salt || []);
|
||||
const info = Buffer.from(algorithm.info || []);
|
||||
const hashAlgo = algorithm.hash?.name?.replace('-', '').toLowerCase() || 'sha256';
|
||||
|
||||
const derived = nodeCrypto.hkdfSync(
|
||||
hashAlgo,
|
||||
Buffer.from(baseKey.key || baseKey),
|
||||
salt,
|
||||
info,
|
||||
length / 8
|
||||
);
|
||||
|
||||
return Buffer.from(derived).buffer;
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported deriveBits algorithm: ${algoName}`);
|
||||
},
|
||||
|
||||
async deriveKey(algorithm, baseKey, derivedKeyAlgorithm, extractable, keyUsages) {
|
||||
const bits = await this.deriveBits(algorithm, baseKey, derivedKeyAlgorithm.length || 256);
|
||||
|
||||
return {
|
||||
type: 'secret',
|
||||
extractable,
|
||||
algorithm: derivedKeyAlgorithm,
|
||||
usages: keyUsages,
|
||||
key: Buffer.from(bits),
|
||||
};
|
||||
},
|
||||
|
||||
async wrapKey(format, key, wrappingKey, wrapAlgorithm) {
|
||||
const exported = await this.exportKey(format, key);
|
||||
const data = format === 'raw' ? exported : Buffer.from(JSON.stringify(exported));
|
||||
return this.encrypt(wrapAlgorithm, wrappingKey, data);
|
||||
},
|
||||
|
||||
async unwrapKey(format, wrappedKey, unwrappingKey, unwrapAlgorithm, unwrappedKeyAlgorithm, extractable, keyUsages) {
|
||||
const decrypted = await this.decrypt(unwrapAlgorithm, unwrappingKey, wrappedKey);
|
||||
const keyData = format === 'raw' ? decrypted : JSON.parse(Buffer.from(decrypted).toString());
|
||||
return this.importKey(format, keyData, unwrappedKeyAlgorithm, extractable, keyUsages);
|
||||
},
|
||||
},
|
||||
};
|
||||
// ── Storage (localStorage / sessionStorage) ──────────────────
|
||||
class Storage {
|
||||
constructor() { this._store = {}; }
|
||||
get length() { return Object.keys(this._store).length; }
|
||||
key(i) { return Object.keys(this._store)[i] || null; }
|
||||
getItem(k) { return Object.prototype.hasOwnProperty.call(this._store, k) ? this._store[k] : null; }
|
||||
setItem(k, v) { this._store[String(k)] = String(v); }
|
||||
removeItem(k) { delete this._store[k]; }
|
||||
clear() { this._store = {}; }
|
||||
}
|
||||
nativeClass(Storage);
|
||||
|
||||
// ── IDBFactory (indexedDB) ────────────────────────────────────
|
||||
class IDBFactory {
|
||||
open() { return { result: null, onerror: null, onsuccess: null }; }
|
||||
deleteDatabase() { return {}; }
|
||||
databases() { return Promise.resolve([]); }
|
||||
cmp() { return 0; }
|
||||
}
|
||||
nativeClass(IDBFactory);
|
||||
|
||||
// ── Notification ──────────────────────────────────────────────
|
||||
class Notification {
|
||||
constructor(title, opts) {
|
||||
this.title = title;
|
||||
this.options = opts || {};
|
||||
}
|
||||
close() {}
|
||||
static get permission() { return 'denied'; } // P2: denied 或 default
|
||||
static requestPermission() { return Promise.resolve('denied'); }
|
||||
}
|
||||
nativeClass(Notification);
|
||||
|
||||
// ── atob / btoa ───────────────────────────────────────────────
|
||||
const atob = createNative('atob', (str) => Buffer.from(str, 'base64').toString('binary'));
|
||||
const btoa = createNative('btoa', (str) => Buffer.from(str, 'binary').toString('base64'));
|
||||
|
||||
module.exports = { cryptoMock, Storage, IDBFactory, Notification, atob, btoa };
|
||||
|
||||
Reference in New Issue
Block a user