diff --git a/components/secrets-list.tsx b/components/secrets-list.tsx index 907e971..012bb49 100644 --- a/components/secrets-list.tsx +++ b/components/secrets-list.tsx @@ -11,6 +11,7 @@ import type { SecretWithDecryptedData, SecretWithDecryptedDataAndVault, } from "@/types/secret"; +import type { VaultWithAccess } from "@/types/vault"; import { SecretFormDialog } from "./secret-form-dialog"; import { Badge } from "./ui/badge"; import { Button } from "./ui/button"; @@ -28,10 +29,7 @@ const ALL_SECRETS_QUERY_KEY = "all-secrets-list"; interface SecretsListProps { vaultId: string; - vault?: { - isOwner?: boolean; - role?: string; - }; + vault: VaultWithAccess; } type SecretWithOptionalVault = @@ -374,7 +372,7 @@ function SecretsList({ vaultId, vault }: SecretsListProps) { - secretsClient.getSecretsWithDecryptedData(vaultId, privateKey) + secretsClient.getSecretsWithDecryptedData(vault, privateKey) } emptyStateMessage={{ primary: "No secrets in this vault yet.", diff --git a/lib/crypto.ts b/lib/crypto.ts index 86d7c5e..1d0889c 100644 --- a/lib/crypto.ts +++ b/lib/crypto.ts @@ -57,36 +57,41 @@ export class CryptoService { public async encryptSecret({ secret, - publicKey, + vaultKey, }: { secret: string; - publicKey: CryptoKey; + vaultKey: CryptoKey; }): Promise { const data = new TextEncoder().encode(secret); + const iv = crypto.getRandomValues(new Uint8Array(12)); const encrypted = await crypto.subtle.encrypt( { - name: "RSA-OAEP", + name: "AES-GCM", + iv, }, - publicKey, + vaultKey, data ); - return BufferTransformer.arrayBufferToBase64(encrypted); + const packed = this.pack(iv, encrypted); + return BufferTransformer.arrayBufferToBase64(packed); } public async decryptSecret({ encryptedSecret, - privateKey, + vaultKey, }: { encryptedSecret: string; - privateKey: CryptoKey; + vaultKey: CryptoKey; }): Promise { const data = BufferTransformer.base64ToArrayBuffer(encryptedSecret); + const { iv, data: encryptedData } = this.unpack(data); const decrypted = await crypto.subtle.decrypt( { - name: "RSA-OAEP", + name: "AES-GCM", + iv, }, - privateKey, - data + vaultKey, + encryptedData ); return new TextDecoder().decode(decrypted); } diff --git a/lib/secrets-client.ts b/lib/secrets-client.ts index af584fc..d528a48 100644 --- a/lib/secrets-client.ts +++ b/lib/secrets-client.ts @@ -19,6 +19,7 @@ import type { SecretWithDecryptedData, SecretWithDecryptedDataAndVault, } from "@/types/secret"; +import type { VaultWithAccess } from "@/types/vault"; import { CryptoService } from "./crypto"; export class SecretsClient { @@ -45,21 +46,10 @@ export class SecretsClient { // Encrypt the secret data with the vault key const dataString = JSON.stringify(input.data); - const data = new TextEncoder().encode(dataString); - const iv = crypto.getRandomValues(new Uint8Array(12)); - - const encrypted = await crypto.subtle.encrypt( - { - name: "AES-GCM", - iv, - }, + const encryptedData = await this.cryptoService.encryptSecret({ + secret: dataString, vaultKey, - data - ); - - // Pack IV and encrypted data - const packedData = this.pack(iv, encrypted); - const encryptedData = this.arrayBufferToBase64(packedData); + }); const response = await createSecret({ vaultId: input.vaultId, @@ -95,39 +85,24 @@ export class SecretsClient { } async getSecretsWithDecryptedData( - vaultId: string, + vault: VaultWithAccess, privateKey: CryptoKey ): Promise { try { - // Get vault to access the wrapped vault key - const vaultResponse = await getVault({ vaultId }); - const vault = handleActionResponse(vaultResponse); - - // Unwrap the vault key const vaultKey = await this.cryptoService.unwrapVaultKey({ wrappedKey: vault.wrappedKey, privateKey, }); - const encryptedSecrets = await this.getSecrets(vaultId); + const encryptedSecrets = await this.getSecrets(vault.id); const decryptedSecrets: SecretWithDecryptedData[] = []; for (const secret of encryptedSecrets) { try { - // Decrypt with vault key - const encryptedData = this.base64ToArrayBuffer(secret.encryptedData); - const { iv, data } = this.unpack(encryptedData); - - const decrypted = await crypto.subtle.decrypt( - { - name: "AES-GCM", - iv, - }, + const decryptedDataString = await this.cryptoService.decryptSecret({ + encryptedSecret: secret.encryptedData, vaultKey, - data - ); - - const decryptedDataString = new TextDecoder().decode(decrypted); + }); const secretData: SecretData = JSON.parse(decryptedDataString); decryptedSecrets.push({ @@ -153,63 +128,6 @@ export class SecretsClient { } } - async getSecretWithDecryptedData( - secretId: string, - privateKey: CryptoKey - ): Promise { - try { - const encryptedSecret = await this.getSecret(secretId); - - if (!encryptedSecret) { - return null; - } - - // Get vault to access the wrapped vault key - const vaultResponse = await getVault({ - vaultId: encryptedSecret.vaultId, - }); - const vault = handleActionResponse(vaultResponse); - - // Unwrap the vault key - const vaultKey = await this.cryptoService.unwrapVaultKey({ - wrappedKey: vault.wrappedKey, - privateKey, - }); - - // Decrypt with vault key - const encryptedData = this.base64ToArrayBuffer( - encryptedSecret.encryptedData - ); - const { iv, data } = this.unpack(encryptedData); - - const decrypted = await crypto.subtle.decrypt( - { - name: "AES-GCM", - iv, - }, - vaultKey, - data - ); - - const decryptedDataString = new TextDecoder().decode(decrypted); - const secretData: SecretData = JSON.parse(decryptedDataString); - - return { - id: encryptedSecret.id, - vaultId: encryptedSecret.vaultId, - title: encryptedSecret.title, - data: secretData, - createdAt: encryptedSecret.createdAt, - updatedAt: encryptedSecret.updatedAt, - }; - } catch (error) { - console.error("Failed to get secret with decrypted data:", error); - throw new Error( - "Failed to decrypt secret. Please check your password and try again." - ); - } - } - async updateSecret( secretId: string, updatedSecret: UpdateSecretInput, @@ -242,21 +160,10 @@ export class SecretsClient { // Encrypt the new data with the vault key const dataString = JSON.stringify(updatedSecret.data); - const data = new TextEncoder().encode(dataString); - const iv = crypto.getRandomValues(new Uint8Array(12)); - - const encrypted = await crypto.subtle.encrypt( - { - name: "AES-GCM", - iv, - }, + serverUpdates.encryptedData = await this.cryptoService.encryptSecret({ + secret: dataString, vaultKey, - data - ); - - // Pack IV and encrypted data - const packedData = this.pack(iv, encrypted); - serverUpdates.encryptedData = this.arrayBufferToBase64(packedData); + }); } const response = await updateSecret({ @@ -299,21 +206,10 @@ export class SecretsClient { vaultKeys.set(secretWithVault.vault.id, vaultKey); } - const encryptedData = this.base64ToArrayBuffer( - secretWithVault.encryptedData - ); - const { iv, data } = this.unpack(encryptedData); - - const decrypted = await crypto.subtle.decrypt( - { - name: "AES-GCM", - iv, - }, + const decryptedDataString = await this.cryptoService.decryptSecret({ + encryptedSecret: secretWithVault.encryptedData, vaultKey, - data - ); - - const decryptedDataString = new TextDecoder().decode(decrypted); + }); const secretData: SecretData = JSON.parse(decryptedDataString); decryptedSecrets.push({ @@ -345,49 +241,4 @@ export class SecretsClient { ); } } - - private pack( - iv: Uint8Array, - data: ArrayBuffer | ArrayBufferView - ): ArrayBuffer { - const dataBytes = ArrayBuffer.isView(data) - ? new Uint8Array(data.buffer, data.byteOffset, data.byteLength) - : new Uint8Array(data); - const out = new Uint8Array(iv.byteLength + dataBytes.byteLength); - out.set(iv, 0); - out.set(dataBytes, iv.byteLength); - return out.buffer; - } - - private unpack(src: ArrayBuffer | ArrayBufferView): { - iv: Uint8Array; - data: Uint8Array; - } { - const bytes = ArrayBuffer.isView(src) - ? new Uint8Array(src.buffer, src.byteOffset, src.byteLength) - : new Uint8Array(src); - const iv = bytes.subarray(0, 12); - const data = bytes.subarray(12); - return { iv, data }; - } - - private base64ToArrayBuffer(base64: string): ArrayBuffer { - const binaryString = atob(base64); - const bytes = new Uint8Array(binaryString.length); - for (let i = 0; i < binaryString.length; i++) { - // eslint-disable-next-line security/detect-object-injection - bytes[i] = binaryString.charCodeAt(i); - } - return bytes.buffer as ArrayBuffer; - } - - private arrayBufferToBase64(buffer: ArrayBuffer): string { - const bytes = new Uint8Array(buffer); - let binary = ""; - for (let i = 0; i < bytes.byteLength; i++) { - // eslint-disable-next-line security/detect-object-injection - binary += String.fromCharCode(bytes[i]); - } - return btoa(binary); - } }