mirror of
https://github.com/kmc7468/arkvault.git
synced 2025-12-14 22:08:45 +00:00
프론트엔드 암호 모듈 리팩토링
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { signRequest } from "$lib/modules/crypto";
|
import { signRequestBody } from "$lib/modules/crypto";
|
||||||
|
|
||||||
export const refreshToken = async () => {
|
export const refreshToken = async () => {
|
||||||
return await fetch("/api/auth/refreshToken", { method: "POST" });
|
return await fetch("/api/auth/refreshToken", { method: "POST" });
|
||||||
@@ -36,6 +36,6 @@ export const callSignedPostApi = async <T>(input: RequestInfo, payload: T, signK
|
|||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: await signRequest(payload, signKey),
|
body: await signRequestBody(payload, signKey),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ interface KeyExportState {
|
|||||||
signKeyBase64: string;
|
signKeyBase64: string;
|
||||||
verifyKeyBase64: string;
|
verifyKeyBase64: string;
|
||||||
|
|
||||||
masterKeyWrapped: ArrayBuffer;
|
masterKeyWrapped: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useAutoNull = <T>(value: T | null) => {
|
const useAutoNull = <T>(value: T | null) => {
|
||||||
|
|||||||
@@ -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);
|
|
||||||
};
|
|
||||||
83
src/lib/modules/crypto/aes.ts
Normal file
83
src/lib/modules/crypto/aes.ts
Normal 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,
|
||||||
|
);
|
||||||
|
};
|
||||||
4
src/lib/modules/crypto/index.ts
Normal file
4
src/lib/modules/crypto/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export * from "./aes";
|
||||||
|
export * from "./rsa";
|
||||||
|
export * from "./sha";
|
||||||
|
export * from "./util";
|
||||||
159
src/lib/modules/crypto/rsa.ts
Normal file
159
src/lib/modules/crypto/rsa.ts
Normal 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);
|
||||||
|
};
|
||||||
3
src/lib/modules/crypto/sha.ts
Normal file
3
src/lib/modules/crypto/sha.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const digestMessage = async (message: BufferSource) => {
|
||||||
|
return await window.crypto.subtle.digest("SHA-256", message);
|
||||||
|
};
|
||||||
19
src/lib/modules/crypto/util.ts
Normal file
19
src/lib/modules/crypto/util.ts
Normal 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;
|
||||||
|
};
|
||||||
@@ -1,9 +1,4 @@
|
|||||||
import {
|
import { encodeToBase64, decryptChallenge, signMessage } from "$lib/modules/crypto";
|
||||||
encodeToBase64,
|
|
||||||
decodeFromBase64,
|
|
||||||
decryptRSACiphertext,
|
|
||||||
signRSAMessage,
|
|
||||||
} from "$lib/modules/crypto";
|
|
||||||
import type {
|
import type {
|
||||||
TokenUpgradeRequest,
|
TokenUpgradeRequest,
|
||||||
TokenUpgradeResponse,
|
TokenUpgradeResponse,
|
||||||
@@ -29,8 +24,8 @@ export const requestTokenUpgrade = async (
|
|||||||
if (!res.ok) return false;
|
if (!res.ok) return false;
|
||||||
|
|
||||||
const { challenge }: TokenUpgradeResponse = await res.json();
|
const { challenge }: TokenUpgradeResponse = await res.json();
|
||||||
const answer = await decryptRSACiphertext(decodeFromBase64(challenge), decryptKey);
|
const answer = await decryptChallenge(challenge, decryptKey);
|
||||||
const sigAnswer = await signRSAMessage(answer, signKey);
|
const sigAnswer = await signMessage(answer, signKey);
|
||||||
|
|
||||||
res = await fetch("/api/auth/upgradeToken/verify", {
|
res = await fetch("/api/auth/upgradeToken/verify", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
@@ -2,12 +2,10 @@ import { callGetApi, callPostApi } from "$lib/hooks";
|
|||||||
import { storeMasterKeys } from "$lib/indexedDB";
|
import { storeMasterKeys } from "$lib/indexedDB";
|
||||||
import {
|
import {
|
||||||
encodeToBase64,
|
encodeToBase64,
|
||||||
decodeFromBase64,
|
decryptChallenge,
|
||||||
decryptRSACiphertext,
|
signMessage,
|
||||||
signRSAMessage,
|
unwrapMasterKey,
|
||||||
makeAESKeyNonextractable,
|
verifyMasterKeyWrapped,
|
||||||
unwrapAESMasterKey,
|
|
||||||
verifyMasterKeyWrappedSig,
|
|
||||||
} from "$lib/modules/crypto";
|
} from "$lib/modules/crypto";
|
||||||
import type {
|
import type {
|
||||||
ClientRegisterRequest,
|
ClientRegisterRequest,
|
||||||
@@ -30,8 +28,8 @@ export const requestClientRegistration = async (
|
|||||||
if (!res.ok) return false;
|
if (!res.ok) return false;
|
||||||
|
|
||||||
const { challenge }: ClientRegisterResponse = await res.json();
|
const { challenge }: ClientRegisterResponse = await res.json();
|
||||||
const answer = await decryptRSACiphertext(decodeFromBase64(challenge), decryptKey);
|
const answer = await decryptChallenge(challenge, decryptKey);
|
||||||
const sigAnswer = await signRSAMessage(answer, signKey);
|
const sigAnswer = await signMessage(answer, signKey);
|
||||||
|
|
||||||
res = await callPostApi<ClientRegisterVerifyRequest>("/api/client/register/verify", {
|
res = await callPostApi<ClientRegisterVerifyRequest>("/api/client/register/verify", {
|
||||||
answer: encodeToBase64(answer),
|
answer: encodeToBase64(answer),
|
||||||
@@ -47,19 +45,20 @@ export const requestMasterKeyDownload = async (decryptKey: CryptoKey, verfiyKey:
|
|||||||
const { meks: masterKeysWrapped }: MasterKeyListResponse = await res.json();
|
const { meks: masterKeysWrapped }: MasterKeyListResponse = await res.json();
|
||||||
const masterKeys = await Promise.all(
|
const masterKeys = await Promise.all(
|
||||||
masterKeysWrapped.map(
|
masterKeysWrapped.map(
|
||||||
async ({ version, state, mek: masterKeyWrapped, mekSig: masterKeyWrappedSig }) => ({
|
async ({ version, state, mek: masterKeyWrapped, mekSig: masterKeyWrappedSig }) => {
|
||||||
version,
|
const { masterKey } = await unwrapMasterKey(masterKeyWrapped, decryptKey);
|
||||||
state,
|
return {
|
||||||
masterKey: await makeAESKeyNonextractable(
|
|
||||||
await unwrapAESMasterKey(decodeFromBase64(masterKeyWrapped), decryptKey),
|
|
||||||
),
|
|
||||||
isValid: await verifyMasterKeyWrappedSig(
|
|
||||||
version,
|
version,
|
||||||
masterKeyWrapped,
|
state,
|
||||||
masterKeyWrappedSig,
|
masterKey,
|
||||||
verfiyKey,
|
isValid: await verifyMasterKeyWrapped(
|
||||||
),
|
version,
|
||||||
}),
|
masterKeyWrapped,
|
||||||
|
masterKeyWrappedSig,
|
||||||
|
verfiyKey,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (!masterKeys.every(({ isValid }) => isValid)) return false;
|
if (!masterKeys.every(({ isValid }) => isValid)) return false;
|
||||||
|
|||||||
@@ -9,7 +9,9 @@
|
|||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
const fingerprint = $derived(
|
const fingerprint = $derived(
|
||||||
$clientKeyStore ? generateEncryptKeyFingerprint($clientKeyStore.encryptKey) : undefined,
|
$clientKeyStore
|
||||||
|
? generateEncryptKeyFingerprint($clientKeyStore.encryptKey, $clientKeyStore.verifyKey)
|
||||||
|
: undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import { exportRSAKey, digestSHA256 } from "$lib/modules/crypto";
|
import { concatenateBuffers, exportRSAKey, digestMessage } from "$lib/modules/crypto";
|
||||||
|
|
||||||
export { requestMasterKeyDownload } from "$lib/services/key";
|
export { requestMasterKeyDownload } from "$lib/services/key";
|
||||||
|
|
||||||
export const generateEncryptKeyFingerprint = async (encryptKey: CryptoKey) => {
|
export const generateEncryptKeyFingerprint = async (
|
||||||
const { key } = await exportRSAKey(encryptKey);
|
encryptKey: CryptoKey,
|
||||||
const digest = await digestSHA256(key);
|
verifyKey: CryptoKey,
|
||||||
|
) => {
|
||||||
|
const { key: encryptKeyBuffer } = await exportRSAKey(encryptKey);
|
||||||
|
const { key: verifyKeyBuffer } = await exportRSAKey(verifyKey);
|
||||||
|
const digest = await digestMessage(concatenateBuffers(encryptKeyBuffer, verifyKeyBuffer));
|
||||||
return Array.from(new Uint8Array(digest))
|
return Array.from(new Uint8Array(digest))
|
||||||
.map((byte) => byte.toString(16).padStart(2, "0"))
|
.map((byte) => byte.toString(16).padStart(2, "0"))
|
||||||
.join("")
|
.join("")
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { callSignedPostApi } from "$lib/hooks";
|
import { callSignedPostApi } from "$lib/hooks";
|
||||||
import { storeClientKey } from "$lib/indexedDB";
|
import { storeClientKey } from "$lib/indexedDB";
|
||||||
import { encodeToBase64, signMasterKeyWrapped } from "$lib/modules/crypto";
|
import { signMasterKeyWrapped } from "$lib/modules/crypto";
|
||||||
import type { InitialMasterKeyRegisterRequest } from "$lib/server/schemas";
|
import type { InitialMasterKeyRegisterRequest } from "$lib/server/schemas";
|
||||||
import type { ClientKeys } from "$lib/stores";
|
import type { ClientKeys } from "$lib/stores";
|
||||||
|
|
||||||
@@ -43,13 +43,13 @@ export const storeClientKeys = async (clientKeys: ClientKeys) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const requestInitialMasterKeyRegistration = async (
|
export const requestInitialMasterKeyRegistration = async (
|
||||||
masterKeyWrapped: ArrayBuffer,
|
masterKeyWrapped: string,
|
||||||
signKey: CryptoKey,
|
signKey: CryptoKey,
|
||||||
) => {
|
) => {
|
||||||
const res = await callSignedPostApi<InitialMasterKeyRegisterRequest>(
|
const res = await callSignedPostApi<InitialMasterKeyRegisterRequest>(
|
||||||
"/api/mek/register/initial",
|
"/api/mek/register/initial",
|
||||||
{
|
{
|
||||||
mek: encodeToBase64(masterKeyWrapped),
|
mek: masterKeyWrapped,
|
||||||
mekSig: await signMasterKeyWrapped(1, masterKeyWrapped, signKey),
|
mekSig: await signMasterKeyWrapped(1, masterKeyWrapped, signKey),
|
||||||
},
|
},
|
||||||
signKey,
|
signKey,
|
||||||
|
|||||||
@@ -1,35 +1,36 @@
|
|||||||
import {
|
import {
|
||||||
generateRSAKeyPair,
|
generateEncryptionKeyPair,
|
||||||
makeRSAKeyNonextractable,
|
generateSigningKeyPair,
|
||||||
exportRSAKeyToBase64,
|
exportRSAKeyToBase64,
|
||||||
generateAESMasterKey,
|
makeRSAKeyNonextractable,
|
||||||
wrapAESMasterKey,
|
generateMasterKey,
|
||||||
|
wrapMasterKey,
|
||||||
} from "$lib/modules/crypto";
|
} from "$lib/modules/crypto";
|
||||||
import { clientKeyStore } from "$lib/stores";
|
import { clientKeyStore } from "$lib/stores";
|
||||||
|
|
||||||
export const generateClientKeys = async () => {
|
export const generateClientKeys = async () => {
|
||||||
const encKeyPair = await generateRSAKeyPair("encryption");
|
const { encryptKey, decryptKey } = await generateEncryptionKeyPair();
|
||||||
const sigKeyPair = await generateRSAKeyPair("signature");
|
const { signKey, verifyKey } = await generateSigningKeyPair();
|
||||||
|
|
||||||
clientKeyStore.set({
|
clientKeyStore.set({
|
||||||
encryptKey: encKeyPair.publicKey,
|
encryptKey,
|
||||||
decryptKey: await makeRSAKeyNonextractable(encKeyPair.privateKey),
|
decryptKey: await makeRSAKeyNonextractable(decryptKey),
|
||||||
signKey: await makeRSAKeyNonextractable(sigKeyPair.privateKey),
|
signKey: await makeRSAKeyNonextractable(signKey),
|
||||||
verifyKey: sigKeyPair.publicKey,
|
verifyKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
encryptKey: encKeyPair.publicKey,
|
encryptKey,
|
||||||
encryptKeyBase64: await exportRSAKeyToBase64(encKeyPair.publicKey),
|
encryptKeyBase64: await exportRSAKeyToBase64(encryptKey),
|
||||||
decryptKeyBase64: await exportRSAKeyToBase64(encKeyPair.privateKey),
|
decryptKeyBase64: await exportRSAKeyToBase64(decryptKey),
|
||||||
signKeyBase64: await exportRSAKeyToBase64(sigKeyPair.privateKey),
|
signKeyBase64: await exportRSAKeyToBase64(signKey),
|
||||||
verifyKeyBase64: await exportRSAKeyToBase64(sigKeyPair.publicKey),
|
verifyKeyBase64: await exportRSAKeyToBase64(verifyKey),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const generateInitialMasterKey = async (encryptKey: CryptoKey) => {
|
export const generateInitialMasterKey = async (encryptKey: CryptoKey) => {
|
||||||
const masterKey = await generateAESMasterKey();
|
const { masterKey } = await generateMasterKey();
|
||||||
return {
|
return {
|
||||||
masterKeyWrapped: await wrapAESMasterKey(masterKey, encryptKey),
|
masterKeyWrapped: await wrapMasterKey(masterKey, encryptKey),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import { callSignedPostApi } from "$lib/hooks";
|
|||||||
import {
|
import {
|
||||||
encodeToBase64,
|
encodeToBase64,
|
||||||
decodeFromBase64,
|
decodeFromBase64,
|
||||||
generateAESDataKey,
|
generateDataKey,
|
||||||
encryptAESPlaintext,
|
wrapDataKey,
|
||||||
decryptAESCiphertext,
|
unwrapDataKey,
|
||||||
wrapAESDataKey,
|
encryptData,
|
||||||
unwrapAESDataKey,
|
decryptData,
|
||||||
} from "$lib/modules/crypto";
|
} from "$lib/modules/crypto";
|
||||||
import type { DirectroyInfoResponse, DirectoryCreateRequest } from "$lib/server/schemas";
|
import type { DirectroyInfoResponse, DirectoryCreateRequest } from "$lib/server/schemas";
|
||||||
import type { MasterKey } from "$lib/stores";
|
import type { MasterKey } from "$lib/stores";
|
||||||
@@ -15,14 +15,10 @@ export const decryptDirectroyMetadata = async (
|
|||||||
metadata: NonNullable<DirectroyInfoResponse["metadata"]>,
|
metadata: NonNullable<DirectroyInfoResponse["metadata"]>,
|
||||||
masterKey: CryptoKey,
|
masterKey: CryptoKey,
|
||||||
) => {
|
) => {
|
||||||
const dataDecryptKey = await unwrapAESDataKey(decodeFromBase64(metadata.dek), masterKey);
|
const { dataKey } = await unwrapDataKey(metadata.dek, masterKey);
|
||||||
return {
|
return {
|
||||||
name: new TextDecoder().decode(
|
name: new TextDecoder().decode(
|
||||||
await decryptAESCiphertext(
|
await decryptData(decodeFromBase64(metadata.name), metadata.nameIv, dataKey),
|
||||||
decodeFromBase64(metadata.name),
|
|
||||||
decodeFromBase64(metadata.nameIv),
|
|
||||||
dataDecryptKey,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -33,16 +29,16 @@ export const requestDirectroyCreation = async (
|
|||||||
masterKey: MasterKey,
|
masterKey: MasterKey,
|
||||||
signKey: CryptoKey,
|
signKey: CryptoKey,
|
||||||
) => {
|
) => {
|
||||||
const dataKey = await generateAESDataKey();
|
const { dataKey } = await generateDataKey();
|
||||||
const nameEncrypted = await encryptAESPlaintext(new TextEncoder().encode(name), dataKey);
|
const nameEncrypted = await encryptData(new TextEncoder().encode(name), dataKey);
|
||||||
return await callSignedPostApi<DirectoryCreateRequest>(
|
return await callSignedPostApi<DirectoryCreateRequest>(
|
||||||
"/api/directory/create",
|
"/api/directory/create",
|
||||||
{
|
{
|
||||||
parentId,
|
parentId,
|
||||||
mekVersion: masterKey.version,
|
mekVersion: masterKey.version,
|
||||||
dek: encodeToBase64(await wrapAESDataKey(dataKey, masterKey.key)),
|
dek: await wrapDataKey(dataKey, masterKey.key),
|
||||||
name: encodeToBase64(nameEncrypted.ciphertext),
|
name: encodeToBase64(nameEncrypted.ciphertext),
|
||||||
nameIv: encodeToBase64(nameEncrypted.iv.buffer),
|
nameIv: nameEncrypted.iv,
|
||||||
},
|
},
|
||||||
signKey,
|
signKey,
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user