tscircuit/ti
ti tscircuit library
- Version
- 1.0.85
- License
- unset
- Stars
- 0
lib/spice/encrypted-pspice-model.ts
export type EncryptedPspiceModel = {
format: "tscircuit-encrypted-pspice-model";
version: 1;
algorithm: "aes-256-gcm";
keyId: string;
iv: string;
authTag: string;
ciphertext: string;
};
const beginMarker = "* tscircuit-encrypted-pspice-model:v1";
const endMarker = "* /tscircuit-encrypted-pspice-model:v1";
const defaultKeyMaterial = "tscircuit-ti-local-pspice-model-encryption-key-v1";
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
const getCrypto = () => {
const cryptoApi = globalThis.crypto;
if (!cryptoApi?.subtle) {
throw new Error("Web Crypto API is required for encrypted PSPICE models");
}
return cryptoApi;
};
const getModelEncryptionKeyMaterial = () => {
const processEnv = (
globalThis as {
process?: { env?: Record<string, string | undefined> };
}
).process?.env;
const keyMaterial =
processEnv?.TSCIRCUIT_SPICE_MODEL_KEY ?? defaultKeyMaterial;
return textEncoder.encode(keyMaterial);
};
const getModelEncryptionKey = async () => {
const cryptoApi = getCrypto();
const keyBytes = await cryptoApi.subtle.digest(
"SHA-256",
getModelEncryptionKeyMaterial(),
);
return cryptoApi.subtle.importKey("raw", keyBytes, "AES-GCM", false, [
"encrypt",
"decrypt",
]);
};
const toBase64 = (bytes: Uint8Array) => {
let binary = "";
for (const byte of bytes) {
binary += String.fromCharCode(byte);
}
return btoa(binary);
};
const fromBase64 = (value: string) => {
const binary = atob(value);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
};
const concatenateBytes = (...byteArrays: Uint8Array[]) => {
const totalLength = byteArrays.reduce((sum, bytes) => sum + bytes.length, 0);
const result = new Uint8Array(totalLength);
let offset = 0;
for (const bytes of byteArrays) {
result.set(bytes, offset);
offset += bytes.length;
}
return result;
};
export const encryptPspiceModelSource = async (
source: string,
): Promise<EncryptedPspiceModel> => {
const cryptoApi = getCrypto();
const iv = cryptoApi.getRandomValues(new Uint8Array(12));
const encryptedBytes = new Uint8Array(
await cryptoApi.subtle.encrypt(
{ name: "AES-GCM", iv, tagLength: 128 },
await getModelEncryptionKey(),
textEncoder.encode(source),
),
);
const ciphertext = encryptedBytes.slice(0, -16);
const authTag = encryptedBytes.slice(-16);
return {
format: "tscircuit-encrypted-pspice-model",
version: 1,
algorithm: "aes-256-gcm",
keyId: "default",
iv: toBase64(iv),
authTag: toBase64(authTag),
ciphertext: toBase64(ciphertext),
};
};
export const decryptPspiceModelSource = async (model: EncryptedPspiceModel) => {
if (
model.format !== "tscircuit-encrypted-pspice-model" ||
model.version !== 1 ||
model.algorithm !== "aes-256-gcm"
) {
throw new Error("Unsupported encrypted PSPICE model format");
}
const cryptoApi = getCrypto();
const decryptedBytes = await cryptoApi.subtle.decrypt(
{ name: "AES-GCM", iv: fromBase64(model.iv), tagLength: 128 },
await getModelEncryptionKey(),
concatenateBytes(fromBase64(model.ciphertext), fromBase64(model.authTag)),
);
return textDecoder.decode(decryptedBytes);
};
export const toEncryptedPspiceModelSpiceSource = (
model: EncryptedPspiceModel,
subckt?: { name: string; pins: string[] },
) => `${beginMarker}
* ${JSON.stringify(model)}
${
subckt
? `.SUBCKT ${subckt.name} ${subckt.pins.join(" ")}
* Encrypted PSPICE model body is decrypted by the configured ngspice wrapper.
.ENDS ${subckt.name}
`
: ""
}
${endMarker}
`;
export const decryptEncryptedPspiceModelsInSpice = async (
spiceString: string,
) => {
const encryptedModelRegex = new RegExp(
[
beginMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\amp;"),
"\\r?\\n\\* (.+?)\\r?\\n[\\s\\S]*?",
endMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\amp;"),
"\\r?\\n?",
].join(""),
"g",
);
let decryptedSpiceString = "";
let lastIndex = 0;
for (const match of spiceString.matchAll(encryptedModelRegex)) {
decryptedSpiceString += spiceString.slice(lastIndex, match.index);
decryptedSpiceString += await decryptPspiceModelSource(
JSON.parse(match[1]),
);
lastIndex = match.index! + match[0].length;
}
return decryptedSpiceString + spiceString.slice(lastIndex);
};