프론트엔드 암호 모듈 리팩토링

This commit is contained in:
static
2025-01-03 12:21:53 +09:00
parent afe672228a
commit aad5617d25
15 changed files with 336 additions and 298 deletions

View File

@@ -1,227 +0,0 @@
export type RSAKeyPurpose = "encryption" | "signature";
export type RSAKeyType = "public" | "private";
export const encodeToBase64 = (data: ArrayBuffer) => {
return btoa(String.fromCharCode(...new Uint8Array(data)));
};
export const decodeFromBase64 = (data: string) => {
return Uint8Array.from(atob(data), (c) => c.charCodeAt(0)).buffer;
};
export const generateRSAKeyPair = async (purpose: RSAKeyPurpose) => {
return await window.crypto.subtle.generateKey(
{
name: purpose === "encryption" ? "RSA-OAEP" : "RSA-PSS",
modulusLength: 4096,
publicExponent: new Uint8Array([1, 0, 1]),
hash: "SHA-256",
} satisfies RsaHashedKeyGenParams,
true,
purpose === "encryption" ? ["encrypt", "decrypt", "wrapKey", "unwrapKey"] : ["sign", "verify"],
);
};
export const makeRSAKeyNonextractable = async (key: CryptoKey) => {
const { format, key: exportedKey } = await exportRSAKey(key);
return await window.crypto.subtle.importKey(
format,
exportedKey,
key.algorithm,
false,
key.usages,
);
};
export const exportRSAKey = async (key: CryptoKey) => {
const format = key.type === "public" ? ("spki" as const) : ("pkcs8" as const);
return {
format,
key: await window.crypto.subtle.exportKey(format, key),
};
};
export const exportRSAKeyToBase64 = async (key: CryptoKey) => {
return encodeToBase64((await exportRSAKey(key)).key);
};
export const encryptRSAPlaintext = async (plaintext: BufferSource, publicKey: CryptoKey) => {
return await window.crypto.subtle.encrypt(
{
name: "RSA-OAEP",
} satisfies RsaOaepParams,
publicKey,
plaintext,
);
};
export const decryptRSACiphertext = async (ciphertext: BufferSource, privateKey: CryptoKey) => {
return await window.crypto.subtle.decrypt(
{
name: "RSA-OAEP",
} satisfies RsaOaepParams,
privateKey,
ciphertext,
);
};
export const signRSAMessage = async (message: BufferSource, privateKey: CryptoKey) => {
return await window.crypto.subtle.sign(
{
name: "RSA-PSS",
saltLength: 32,
} satisfies RsaPssParams,
privateKey,
message,
);
};
export const verifyRSASignature = async (
message: BufferSource,
signature: BufferSource,
publicKey: CryptoKey,
) => {
return await window.crypto.subtle.verify(
{
name: "RSA-PSS",
saltLength: 32,
} satisfies RsaPssParams,
publicKey,
signature,
message,
);
};
export const generateAESMasterKey = async () => {
return await window.crypto.subtle.generateKey(
{
name: "AES-KW",
length: 256,
} satisfies AesKeyGenParams,
true,
["wrapKey", "unwrapKey"],
);
};
export const generateAESDataKey = async () => {
return await window.crypto.subtle.generateKey(
{
name: "AES-GCM",
length: 256,
} satisfies AesKeyGenParams,
true,
["encrypt", "decrypt"],
);
};
export const makeAESKeyNonextractable = async (key: CryptoKey) => {
return await window.crypto.subtle.importKey(
"raw",
await exportAESKey(key),
key.algorithm,
false,
key.usages,
);
};
export const exportAESKey = async (key: CryptoKey) => {
return await window.crypto.subtle.exportKey("raw", key);
};
export const encryptAESPlaintext = async (plaintext: BufferSource, aesKey: CryptoKey) => {
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await window.crypto.subtle.encrypt(
{
name: "AES-GCM",
iv,
} satisfies AesGcmParams,
aesKey,
plaintext,
);
return { ciphertext, iv };
};
export const decryptAESCiphertext = async (
ciphertext: BufferSource,
iv: BufferSource,
aesKey: CryptoKey,
) => {
return await window.crypto.subtle.decrypt(
{
name: "AES-GCM",
iv,
} satisfies AesGcmParams,
aesKey,
ciphertext,
);
};
export const wrapAESMasterKey = async (aesKey: CryptoKey, rsaPublicKey: CryptoKey) => {
return await window.crypto.subtle.wrapKey("raw", aesKey, rsaPublicKey, {
name: "RSA-OAEP",
} satisfies RsaOaepParams);
};
export const wrapAESDataKey = async (aesKey: CryptoKey, aesWrapKey: CryptoKey) => {
return await window.crypto.subtle.wrapKey("raw", aesKey, aesWrapKey, "AES-KW");
};
export const unwrapAESMasterKey = async (wrappedKey: BufferSource, rsaPrivateKey: CryptoKey) => {
return await window.crypto.subtle.unwrapKey(
"raw",
wrappedKey,
rsaPrivateKey,
{
name: "RSA-OAEP",
} satisfies RsaOaepParams,
"AES-KW",
true,
["wrapKey", "unwrapKey"],
);
};
export const unwrapAESDataKey = async (wrappedKey: BufferSource, aesMasterKey: CryptoKey) => {
return await window.crypto.subtle.unwrapKey(
"raw",
wrappedKey,
aesMasterKey,
"AES-KW",
"AES-GCM",
false,
["encrypt", "decrypt"],
);
};
export const digestSHA256 = async (data: BufferSource) => {
return await window.crypto.subtle.digest("SHA-256", data);
};
export const signRequest = async <T>(data: T, privateKey: CryptoKey) => {
const dataBuffer = new TextEncoder().encode(JSON.stringify(data));
const signature = await signRSAMessage(dataBuffer, privateKey);
return JSON.stringify({
data,
signature: encodeToBase64(signature),
});
};
export const signMasterKeyWrapped = async (
version: number,
masterKeyWrapped: ArrayBuffer,
privateKey: CryptoKey,
) => {
const data = JSON.stringify({ version, key: encodeToBase64(masterKeyWrapped) });
const dataBuffer = new TextEncoder().encode(data);
return encodeToBase64(await signRSAMessage(dataBuffer, privateKey));
};
export const verifyMasterKeyWrappedSig = async (
version: number,
masterKeyWrappedBase64: string,
masterKeyWrappedSig: string,
publicKey: CryptoKey,
) => {
const data = JSON.stringify({ version, key: masterKeyWrappedBase64 });
const dataBuffer = new TextEncoder().encode(data);
return await verifyRSASignature(dataBuffer, decodeFromBase64(masterKeyWrappedSig), publicKey);
};

View File

@@ -0,0 +1,83 @@
import { encodeToBase64, decodeFromBase64 } from "./util";
export const generateMasterKey = async () => {
return {
masterKey: await window.crypto.subtle.generateKey(
{
name: "AES-KW",
length: 256,
} satisfies AesKeyGenParams,
true,
["wrapKey", "unwrapKey"],
),
};
};
export const generateDataKey = async () => {
return {
dataKey: await window.crypto.subtle.generateKey(
{
name: "AES-GCM",
length: 256,
} satisfies AesKeyGenParams,
true,
["encrypt", "decrypt"],
),
};
};
const exportAESKey = async (key: CryptoKey) => {
return await window.crypto.subtle.exportKey("raw", key);
};
export const makeAESKeyNonextractable = async (key: CryptoKey) => {
return await window.crypto.subtle.importKey(
"raw",
await exportAESKey(key),
key.algorithm,
false,
key.usages,
);
};
export const wrapDataKey = async (dataKey: CryptoKey, masterKey: CryptoKey) => {
return encodeToBase64(await window.crypto.subtle.wrapKey("raw", dataKey, masterKey, "AES-KW"));
};
export const unwrapDataKey = async (dataKeyWrapped: string, masterKey: CryptoKey) => {
return {
dataKey: await window.crypto.subtle.unwrapKey(
"raw",
decodeFromBase64(dataKeyWrapped),
masterKey,
"AES-KW",
"AES-GCM",
false, // Non-extractable
["encrypt", "decrypt"],
),
};
};
export const encryptData = async (data: BufferSource, dataKey: CryptoKey) => {
const iv = window.crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await window.crypto.subtle.encrypt(
{
name: "AES-GCM",
iv,
} satisfies AesGcmParams,
dataKey,
data,
);
return { ciphertext, iv: encodeToBase64(iv.buffer) };
};
export const decryptData = async (ciphertext: BufferSource, iv: string, dataKey: CryptoKey) => {
return await window.crypto.subtle.decrypt(
{
name: "AES-GCM",
iv: decodeFromBase64(iv),
} satisfies AesGcmParams,
dataKey,
ciphertext,
);
};

View File

@@ -0,0 +1,4 @@
export * from "./aes";
export * from "./rsa";
export * from "./sha";
export * from "./util";

View File

@@ -0,0 +1,159 @@
import { encodeToBase64, decodeFromBase64 } from "./util";
export const generateEncryptionKeyPair = async () => {
const keyPair = await window.crypto.subtle.generateKey(
{
name: "RSA-OAEP",
modulusLength: 4096,
publicExponent: new Uint8Array([1, 0, 1]),
hash: "SHA-256",
} satisfies RsaHashedKeyGenParams,
true,
["encrypt", "decrypt", "wrapKey", "unwrapKey"],
);
return {
encryptKey: keyPair.publicKey,
decryptKey: keyPair.privateKey,
};
};
export const generateSigningKeyPair = async () => {
const keyPair = await window.crypto.subtle.generateKey(
{
name: "RSA-PSS",
modulusLength: 4096,
publicExponent: new Uint8Array([1, 0, 1]),
hash: "SHA-256",
} satisfies RsaHashedKeyGenParams,
true,
["sign", "verify"],
);
return {
signKey: keyPair.privateKey,
verifyKey: keyPair.publicKey,
};
};
export const exportRSAKey = async (key: CryptoKey) => {
const format = key.type === "public" ? ("spki" as const) : ("pkcs8" as const);
return {
key: await window.crypto.subtle.exportKey(format, key),
format,
};
};
export const exportRSAKeyToBase64 = async (key: CryptoKey) => {
return encodeToBase64((await exportRSAKey(key)).key);
};
export const makeRSAKeyNonextractable = async (key: CryptoKey) => {
const { key: exportedKey, format } = await exportRSAKey(key);
return await window.crypto.subtle.importKey(
format,
exportedKey,
key.algorithm,
false,
key.usages,
);
};
export const decryptChallenge = async (challenge: string, decryptKey: CryptoKey) => {
return await window.crypto.subtle.decrypt(
{
name: "RSA-OAEP",
} satisfies RsaOaepParams,
decryptKey,
decodeFromBase64(challenge),
);
};
export const wrapMasterKey = async (masterKey: CryptoKey, encryptKey: CryptoKey) => {
return encodeToBase64(
await window.crypto.subtle.wrapKey("raw", masterKey, encryptKey, {
name: "RSA-OAEP",
} satisfies RsaOaepParams),
);
};
export const unwrapMasterKey = async (
masterKeyWrapped: string,
decryptKey: CryptoKey,
extractable = false,
) => {
return {
masterKey: await window.crypto.subtle.unwrapKey(
"raw",
decodeFromBase64(masterKeyWrapped),
decryptKey,
{
name: "RSA-OAEP",
} satisfies RsaOaepParams,
"AES-KW",
extractable,
["wrapKey", "unwrapKey"],
),
};
};
export const signMessage = async (message: BufferSource, signKey: CryptoKey) => {
return await window.crypto.subtle.sign(
{
name: "RSA-PSS",
saltLength: 32, // SHA-256
} satisfies RsaPssParams,
signKey,
message,
);
};
export const verifySignature = async (
message: BufferSource,
signature: BufferSource,
verifyKey: CryptoKey,
) => {
return await window.crypto.subtle.verify(
{
name: "RSA-PSS",
saltLength: 32, // SHA-256
} satisfies RsaPssParams,
verifyKey,
signature,
message,
);
};
export const signRequestBody = async <T>(requestBody: T, signKey: CryptoKey) => {
const dataBuffer = new TextEncoder().encode(JSON.stringify(requestBody));
const signature = await signMessage(dataBuffer, signKey);
return JSON.stringify({
data: requestBody,
signature: encodeToBase64(signature),
});
};
export const signMasterKeyWrapped = async (
masterKeyVersion: number,
masterKeyWrapped: string,
signKey: CryptoKey,
) => {
const serialized = JSON.stringify({
version: masterKeyVersion,
key: masterKeyWrapped,
});
const serializedBuffer = new TextEncoder().encode(serialized);
return encodeToBase64(await signMessage(serializedBuffer, signKey));
};
export const verifyMasterKeyWrapped = async (
masterKeyVersion: number,
masterKeyWrapped: string,
masterKeyWrappedSig: string,
verifyKey: CryptoKey,
) => {
const serialized = JSON.stringify({
version: masterKeyVersion,
key: masterKeyWrapped,
});
const serializedBuffer = new TextEncoder().encode(serialized);
return await verifySignature(serializedBuffer, decodeFromBase64(masterKeyWrappedSig), verifyKey);
};

View File

@@ -0,0 +1,3 @@
export const digestMessage = async (message: BufferSource) => {
return await window.crypto.subtle.digest("SHA-256", message);
};

View File

@@ -0,0 +1,19 @@
export const encodeToBase64 = (data: ArrayBuffer) => {
return btoa(String.fromCharCode(...new Uint8Array(data)));
};
export const decodeFromBase64 = (data: string) => {
return Uint8Array.from(atob(data), (c) => c.charCodeAt(0)).buffer;
};
export const concatenateBuffers = (...buffers: ArrayBuffer[]) => {
const arrays = buffers.map((buffer) => new Uint8Array(buffer));
const totalLength = arrays.reduce((acc, array) => acc + array.length, 0);
const result = new Uint8Array(totalLength);
arrays.reduce((offset, array) => {
result.set(array, offset);
return offset + array.length;
}, 0);
return result;
};