Files
yaklang-chrome-extension/src/features/browser-crypto/adapters/cryptojs.ts
T

210 lines
8.4 KiB
TypeScript

import { callableOperationKind, cryptoFamily } from './common';
import type {
CryptoAdapterInvocationPlan,
CryptoAdapterOperation,
CryptoAdapterScope,
CryptoAdapterToolkit,
PageCryptoAdapter,
} from './contract';
import { cryptoJsManifest } from './catalog';
const PATHS = [
'AES.encrypt', 'AES.decrypt', 'DES.encrypt', 'DES.decrypt', 'TripleDES.encrypt', 'TripleDES.decrypt',
'RC4.encrypt', 'RC4.decrypt', 'Rabbit.encrypt', 'Rabbit.decrypt', 'MD5', 'SHA1', 'SHA224', 'SHA256',
'SHA384', 'SHA512', 'SHA3', 'RIPEMD160', 'HmacMD5', 'HmacSHA1', 'HmacSHA224', 'HmacSHA256',
'HmacSHA384', 'HmacSHA512', 'PBKDF2', 'EvpKDF',
];
function ownValue(value: unknown, key: string): unknown {
if (!value || typeof value !== 'object') return undefined;
try {
const descriptor = Object.getOwnPropertyDescriptor(value, key);
return descriptor && 'value' in descriptor ? descriptor.value : undefined;
} catch {
return undefined;
}
}
function memberName(cryptoJs: Record<string, unknown>, group: 'mode' | 'pad', value: unknown): string | undefined {
const members = cryptoJs[group];
if (!members || typeof members !== 'object') return undefined;
try {
return Object.entries(members as Record<string, unknown>).find(([, candidate]) => candidate === value)?.[0]?.slice(0, 80);
} catch {
return undefined;
}
}
function optionsMetadata(
cryptoJs: Record<string, unknown>,
value: unknown,
toolkit: CryptoAdapterToolkit,
): { summary?: string; mode?: string; padding?: string } {
if (!value || typeof value !== 'object') return {};
const mode = memberName(cryptoJs, 'mode', ownValue(value, 'mode'));
const padding = memberName(cryptoJs, 'pad', ownValue(value, 'padding'));
const iv = ownValue(value, 'iv');
const parts: string[] = [];
if (mode) parts.push(`mode=${mode}`);
if (padding) parts.push(`padding=${padding}`);
if (iv !== undefined) parts.push(`ivBytes=${toolkit.byteLength(iv) || 0}`);
return { summary: parts.length ? parts.join(' ').slice(0, 240) : undefined, mode, padding };
}
function describe(
scope: CryptoAdapterScope,
path: string,
args: unknown[],
toolkit: CryptoAdapterToolkit,
): CryptoAdapterInvocationPlan {
const cryptoJs = (scope.window as unknown as { CryptoJS?: Record<string, unknown> }).CryptoJS || {};
const normalized = path.toLowerCase();
const encrypting = normalized.includes('encrypt');
const decrypting = normalized.includes('decrypt');
const outputEncoding = encrypting ? 'base64' : decrypting ? 'hex' : 'auto';
const options = normalized.includes('encrypt') || normalized.includes('decrypt')
? optionsMetadata(cryptoJs, args[2], toolkit)
: {};
let roles: Array<'data' | 'key' | 'salt' | 'options' | 'unknown'> = ['data'];
if (normalized.includes('hmac')) roles = ['data', 'key'];
else if (normalized.includes('pbkdf2') || normalized.includes('evpkdf')) roles = ['data', 'salt', 'options'];
else if (normalized.includes('.encrypt') || normalized.includes('.decrypt')) roles = ['data', 'key', 'options'];
const callableKind = callableOperationKind(path);
const adaptData = (value: unknown) => {
const originalInput = args[0];
if (originalInput && typeof originalInput === 'object'
&& typeof (originalInput as { sigBytes?: unknown }).sigBytes === 'number') {
const bytes = toolkit.bytesForInput(value);
const encoder = (cryptoJs as { enc?: { Base64?: { parse?(input: string): unknown } } }).enc?.Base64;
if (bytes && typeof encoder?.parse === 'function') return encoder.parse(toolkit.bytesToBase64(bytes));
}
return toolkit.defaultAdaptInput(value, originalInput);
};
const adaptWordArray = (value: unknown, originalInput: unknown) => {
if (!originalInput || typeof originalInput !== 'object'
|| typeof (originalInput as { sigBytes?: unknown }).sigBytes !== 'number') {
return toolkit.defaultAdaptInput(value, originalInput);
}
const enc = (cryptoJs as {
enc?: {
Hex?: { parse?(input: string): unknown };
Base64?: { parse?(input: string): unknown };
};
}).enc;
if (typeof value === 'string' && /^[0-9a-f]+$/i.test(value) && value.length % 2 === 0
&& typeof enc?.Hex?.parse === 'function') return enc.Hex.parse(value);
const bytes = toolkit.bytesForInput(value);
if (bytes && typeof enc?.Base64?.parse === 'function') return enc.Base64.parse(toolkit.bytesToBase64(bytes));
return toolkit.defaultAdaptInput(value, originalInput);
};
return {
crypto: {
adapterId: cryptoJsManifest.id,
providerKind: cryptoJsManifest.providerKind,
family: cryptoFamily(path, path),
operation: path,
algorithm: path,
mode: options.mode,
padding: options.padding,
inputEncoding: 'auto',
outputEncoding,
state: { model: 'stateless', phase: 'one-shot' },
},
inputIndex: 0,
callableKind,
outputEncoding,
replayInputs: decrypting ? [
{
path: '$input', name: 'data', role: 'data', originalInput: args[0],
apply: (nextArgs, value) => { nextArgs[0] = adaptData(value); },
},
{
path: '$input.key', name: 'key', role: 'key', originalInput: args[1],
apply: (nextArgs, value) => { nextArgs[1] = adaptWordArray(value, args[1]); },
},
{
path: '$input.iv', name: 'iv', role: 'iv', originalInput: ownValue(args[2], 'iv'),
apply: (nextArgs, value) => {
const options = nextArgs[2] && typeof nextArgs[2] === 'object'
? nextArgs[2] as Record<string, unknown>
: {};
nextArgs[2] = { ...options, iv: adaptWordArray(value, ownValue(args[2], 'iv')) };
},
},
] : undefined,
arguments: args.slice(0, 8).map((value, index) => toolkit.argument(
index,
roles[index] || 'unknown',
value,
index === 0,
Boolean(callableKind),
roles[index] === 'options' ? options.summary : undefined,
)),
inputEvidence() {
const evidence = toolkit.collectEvidence(args[0], '$input');
for (let index = 1; index < Math.min(args.length, roles.length); index += 1) {
const role = roles[index];
if (role === 'options') {
const iv = ownValue(args[index], 'iv');
if (iv !== undefined) evidence.push(...toolkit.collectEvidence(iv, '$input.iv'));
continue;
}
if (role !== 'unknown') evidence.push(...toolkit.collectEvidence(args[index], `$input.${role}`));
}
return evidence.slice(0, 48);
},
outputEvidence(value) {
const output = toolkit.defaultOutputEvidence(value);
if (!value || (typeof value !== 'object' && typeof value !== 'function') || output.length >= 48) return output;
try {
const toString = (value as { toString?: unknown }).toString;
if (typeof toString !== 'function') return output;
const text = Reflect.apply(toString, value, []);
if (typeof text !== 'string' || !text || text === '[object Object]') return output;
const extra = toolkit.collectEvidence(text, '$output:string')[0];
if (extra && !output.some((item) => item.path === extra.path && item.fingerprint === extra.fingerprint)) output.push(extra);
} catch {
// Compatible CryptoJS result objects are best-effort evidence only.
}
return output.slice(0, 48);
},
adaptInput(value) {
return adaptData(value);
},
};
}
export const cryptoJsAdapter: PageCryptoAdapter = {
manifest: cryptoJsManifest,
discover(scope): CryptoAdapterOperation[] {
const cryptoJs = (scope.window as unknown as { CryptoJS?: Record<string, unknown> }).CryptoJS;
if (!cryptoJs) return [];
const output: CryptoAdapterOperation[] = [];
for (const path of PATHS) {
const segments = path.split('.');
let owner: Record<string, unknown> | undefined = cryptoJs;
for (const segment of segments.slice(0, -1)) {
const next = owner?.[segment];
if (!next || (typeof next !== 'object' && typeof next !== 'function')) {
owner = undefined;
break;
}
owner = next as Record<string, unknown>;
}
if (!owner) continue;
output.push({
id: `cryptojs.${path}`,
operation: path,
owner,
key: segments.at(-1)!,
resultMode: 'sync',
describe: (_thisArg, args, toolkit) => describe(scope, path, args, toolkit),
createWrapper: (_original, invoke) => function recordedCryptoJs(this: unknown, ...args: unknown[]) {
return invoke(this, args);
},
});
}
return output;
},
};