Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions components/secrets-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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 =
Expand Down Expand Up @@ -374,7 +372,7 @@ function SecretsList({ vaultId, vault }: SecretsListProps) {
<SecretsListBase
queryKey={[SECRETS_LIST_QUERY_KEY, vaultId]}
queryFn={privateKey =>
secretsClient.getSecretsWithDecryptedData(vaultId, privateKey)
secretsClient.getSecretsWithDecryptedData(vault, privateKey)
}
emptyStateMessage={{
primary: "No secrets in this vault yet.",
Expand Down
25 changes: 15 additions & 10 deletions lib/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,36 +57,41 @@ export class CryptoService {

public async encryptSecret({
secret,
publicKey,
vaultKey,
}: {
secret: string;
publicKey: CryptoKey;
vaultKey: CryptoKey;
}): Promise<string> {
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<string> {
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);
}
Expand Down
179 changes: 15 additions & 164 deletions lib/secrets-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type {
SecretWithDecryptedData,
SecretWithDecryptedDataAndVault,
} from "@/types/secret";
import type { VaultWithAccess } from "@/types/vault";
import { CryptoService } from "./crypto";

export class SecretsClient {
Expand All @@ -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,
Expand Down Expand Up @@ -95,39 +85,24 @@ export class SecretsClient {
}

async getSecretsWithDecryptedData(
vaultId: string,
vault: VaultWithAccess,
privateKey: CryptoKey
): Promise<SecretWithDecryptedData[]> {
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({
Expand All @@ -153,63 +128,6 @@ export class SecretsClient {
}
}

async getSecretWithDecryptedData(
secretId: string,
privateKey: CryptoKey
): Promise<SecretWithDecryptedData | null> {
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,
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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);
}
}