서버, 클라이언트, 서비스 워커 공용 코드를 lib/shared로 분리하고 브라우저 전용 모듈을 lib/client로 재구성

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
staticandClaude Opus 5.5 committed 2026-10-12 00:04:56 +09:00
1 parent 6a6aa1765b
commit aee87fd9b6
82 files changed
+326 -221

No files matched your search

+134
View File
@@ -0,0 +1,134 @@
import { AES_GCM_IV_SIZE } from "#lib/shared/format/index.js";
import {
encodeString,
decodeString,
encodeToBase64,
decodeFromBase64,
concatenateBuffers,
} from "./encoding";
export const generateMasterKey = async () => {
return {
masterKey: await crypto.subtle.generateKey(
{
name: "AES-KW",
length: 256,
} satisfies AesKeyGenParams,
true,
["wrapKey", "unwrapKey"],
),
};
};
export const generateDataKey = async () => {
return {
dataKey: await crypto.subtle.generateKey(
{
name: "AES-GCM",
length: 256,
} satisfies AesKeyGenParams,
true,
["encrypt", "decrypt"],
),
dataKeyVersion: new Date(),
};
};
export const makeAESKeyNonextractable = async (key: CryptoKey) => {
return await crypto.subtle.importKey(
"raw",
await crypto.subtle.exportKey("raw", key),
key.algorithm,
false,
key.usages,
);
};
export const wrapDataKey = async (dataKey: CryptoKey, masterKey: CryptoKey) => {
return encodeToBase64(await crypto.subtle.wrapKey("raw", dataKey, masterKey, "AES-KW"));
};
export const unwrapDataKey = async (dataKeyWrapped: string, masterKey: CryptoKey) => {
return {
dataKey: await crypto.subtle.unwrapKey(
"raw",
decodeFromBase64(dataKeyWrapped),
masterKey,
"AES-KW",
"AES-GCM",
false, // Nonextractable
["encrypt", "decrypt"],
),
};
};
export const wrapHmacSecret = async (hmacSecret: CryptoKey, masterKey: CryptoKey) => {
return encodeToBase64(await crypto.subtle.wrapKey("raw", hmacSecret, masterKey, "AES-KW"));
};
export const unwrapHmacSecret = async (hmacSecretWrapped: string, masterKey: CryptoKey) => {
return {
hmacSecret: await crypto.subtle.unwrapKey(
"raw",
decodeFromBase64(hmacSecretWrapped),
masterKey,
"AES-KW",
{
name: "HMAC",
hash: "SHA-256",
} satisfies HmacImportParams,
true, // Extractable
["sign", "verify"],
),
};
};
export const encryptData = async (data: BufferSource, dataKey: CryptoKey) => {
const iv = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt(
{
name: "AES-GCM",
iv,
} satisfies AesGcmParams,
dataKey,
data,
);
return { ciphertext, iv: iv.buffer };
};
export const decryptData = async (
ciphertext: BufferSource,
iv: string | BufferSource,
dataKey: CryptoKey,
) => {
return await crypto.subtle.decrypt(
{
name: "AES-GCM",
iv: typeof iv === "string" ? decodeFromBase64(iv) : iv,
} satisfies AesGcmParams,
dataKey,
ciphertext,
);
};
export const encryptString = async (plaintext: string, dataKey: CryptoKey) => {
const { ciphertext, iv } = await encryptData(encodeString(plaintext), dataKey);
return { ciphertext: encodeToBase64(ciphertext), iv: encodeToBase64(iv) };
};
export const decryptString = async (ciphertext: string, iv: string, dataKey: CryptoKey) => {
return decodeString(await decryptData(decodeFromBase64(ciphertext), iv, dataKey));
};
export const encryptChunk = async (chunk: ArrayBuffer, dataKey: CryptoKey) => {
const { ciphertext, iv } = await encryptData(chunk, dataKey);
return concatenateBuffers(iv, ciphertext).buffer;
};
export const decryptChunk = async (encryptedChunk: ArrayBuffer, dataKey: CryptoKey) => {
return await decryptData(
encryptedChunk.slice(AES_GCM_IV_SIZE),
encryptedChunk.slice(0, AES_GCM_IV_SIZE),
dataKey,
);
};
+30
View File
@@ -0,0 +1,30 @@
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
export const encodeString = (data: string) => {
return textEncoder.encode(data);
};
export const decodeString = (data: ArrayBuffer) => {
return textDecoder.decode(data);
};
export const encodeToBase64 = (data: ArrayBuffer | Uint8Array) => {
return (data instanceof ArrayBuffer ? new Uint8Array(data) : data).toBase64();
};
export const decodeFromBase64 = (data: string) => {
return Uint8Array.fromBase64(data).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;
};
+4
View File
@@ -0,0 +1,4 @@
export * from "./aes";
export * from "./encoding";
export * from "./rsa";
export * from "./sha";
+196
View File
@@ -0,0 +1,196 @@
import { encodeString, encodeToBase64, decodeFromBase64 } from "./encoding";
export const generateEncryptionKeyPair = async () => {
const keyPair = await 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 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 crypto.subtle.exportKey(format, key),
format,
};
};
export const exportRSAKeyToBase64 = async (key: CryptoKey) => {
return encodeToBase64((await exportRSAKey(key)).key);
};
export const importEncryptionKeyPairFromBase64 = async (
encryptKeyBase64: string,
decryptKeyBase64: string,
) => {
const algorithm: RsaHashedImportParams = {
name: "RSA-OAEP",
hash: "SHA-256",
};
const encryptKey = await crypto.subtle.importKey(
"spki",
decodeFromBase64(encryptKeyBase64),
algorithm,
true,
["encrypt", "wrapKey"],
);
const decryptKey = await crypto.subtle.importKey(
"pkcs8",
decodeFromBase64(decryptKeyBase64),
algorithm,
true,
["decrypt", "unwrapKey"],
);
return { encryptKey, decryptKey };
};
export const importSigningKeyPairFromBase64 = async (
signKeyBase64: string,
verifyKeyBase64: string,
) => {
const algorithm: RsaHashedImportParams = {
name: "RSA-PSS",
hash: "SHA-256",
};
const signKey = await crypto.subtle.importKey(
"pkcs8",
decodeFromBase64(signKeyBase64),
algorithm,
true,
["sign"],
);
const verifyKey = await crypto.subtle.importKey(
"spki",
decodeFromBase64(verifyKeyBase64),
algorithm,
true,
["verify"],
);
return { signKey, verifyKey };
};
export const makeRSAKeyNonextractable = async (key: CryptoKey) => {
const { key: exportedKey, format } = await exportRSAKey(key);
return await crypto.subtle.importKey(format, exportedKey, key.algorithm, false, key.usages);
};
export const decryptChallenge = async (challenge: string, decryptKey: CryptoKey) => {
return await crypto.subtle.decrypt(
{
name: "RSA-OAEP",
} satisfies RsaOaepParams,
decryptKey,
decodeFromBase64(challenge),
);
};
export const wrapMasterKey = async (masterKey: CryptoKey, encryptKey: CryptoKey) => {
return encodeToBase64(
await crypto.subtle.wrapKey("raw", masterKey, encryptKey, {
name: "RSA-OAEP",
} satisfies RsaOaepParams),
);
};
export const unwrapMasterKey = async (
masterKeyWrapped: string,
decryptKey: CryptoKey,
extractable = false,
) => {
return {
masterKey: await crypto.subtle.unwrapKey(
"raw",
decodeFromBase64(masterKeyWrapped),
decryptKey,
{
name: "RSA-OAEP",
} satisfies RsaOaepParams,
"AES-KW",
extractable,
["wrapKey", "unwrapKey"],
),
};
};
export const signMessageRSA = async (message: BufferSource, signKey: CryptoKey) => {
return await crypto.subtle.sign(
{
name: "RSA-PSS",
saltLength: 32, // SHA-256
} satisfies RsaPssParams,
signKey,
message,
);
};
export const verifySignatureRSA = async (
message: BufferSource,
signature: BufferSource,
verifyKey: CryptoKey,
) => {
return await crypto.subtle.verify(
{
name: "RSA-PSS",
saltLength: 32, // SHA-256
} satisfies RsaPssParams,
verifyKey,
signature,
message,
);
};
export const signMasterKeyWrapped = async (
masterKeyWrapped: string,
masterKeyVersion: number,
signKey: CryptoKey,
) => {
const serialized = JSON.stringify({
version: masterKeyVersion,
key: masterKeyWrapped,
});
return encodeToBase64(await signMessageRSA(encodeString(serialized), signKey));
};
export const verifyMasterKeyWrapped = async (
masterKeyWrapped: string,
masterKeyVersion: number,
masterKeyWrappedSig: string,
verifyKey: CryptoKey,
) => {
const serialized = JSON.stringify({
version: masterKeyVersion,
key: masterKeyWrapped,
});
return await verifySignatureRSA(
encodeString(serialized),
decodeFromBase64(masterKeyWrappedSig),
verifyKey,
);
};
+16
View File
@@ -0,0 +1,16 @@
export const digestMessage = async (message: BufferSource) => {
return await crypto.subtle.digest("SHA-256", message);
};
export const generateHmacSecret = async () => {
return {
hmacSecret: await crypto.subtle.generateKey(
{
name: "HMAC",
hash: "SHA-256",
} satisfies HmacKeyGenParams,
true,
["sign", "verify"],
),
};
};