mirror of
https://github.com/ramvignesh-b/pi-ku.git
synced 2026-05-04 08:56:52 +00:00
feat: implement master key derivation and persistence via IndexedDB
This commit is contained in:
@@ -16,6 +16,7 @@
|
|||||||
"axios": "^1.15.0",
|
"axios": "^1.15.0",
|
||||||
"daisyui": "^5.5.19",
|
"daisyui": "^5.5.19",
|
||||||
"fabric": "^7.2.0",
|
"fabric": "^7.2.0",
|
||||||
|
"idb": "^8.0.3",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"react-hook-form": "^7.72.1",
|
"react-hook-form": "^7.72.1",
|
||||||
@@ -272,6 +273,8 @@
|
|||||||
|
|
||||||
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||||
|
|
||||||
|
"idb": ["idb@8.0.3", "", {}, "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg=="],
|
||||||
|
|
||||||
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||||
|
|
||||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"axios": "^1.15.0",
|
"axios": "^1.15.0",
|
||||||
"daisyui": "^5.5.19",
|
"daisyui": "^5.5.19",
|
||||||
"fabric": "^7.2.0",
|
"fabric": "^7.2.0",
|
||||||
|
"idb": "^8.0.3",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"react-hook-form": "^7.72.1",
|
"react-hook-form": "^7.72.1",
|
||||||
|
|||||||
@@ -1,15 +1,35 @@
|
|||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { api, publicApi } from "../api/apiClient";
|
import { api, publicApi } from "../api/apiClient";
|
||||||
import { endpoints } from "../config/endpoints";
|
import { endpoints } from "../config/endpoints";
|
||||||
import { type UserProfile, useAuthStore } from "../store/useAuthStore";
|
import type { UserProfile } from "../store/useAuthStore";
|
||||||
|
import { useAuthStore } from "../store/useAuthStore";
|
||||||
|
import { useKeyStore } from "../store/useKeyStore";
|
||||||
|
import { CryptoUtils } from "../utils/crypto";
|
||||||
|
import {
|
||||||
|
clearMasterKey,
|
||||||
|
loadMasterKey,
|
||||||
|
saveMasterKey,
|
||||||
|
} from "../utils/keystore";
|
||||||
|
|
||||||
export const useAuth = () => {
|
export const useAuth = () => {
|
||||||
const { accessToken, user, isInitializing, setAuth, clearAuth } =
|
const { accessToken, user, isInitializing, setAuth, clearAuth } =
|
||||||
useAuthStore();
|
useAuthStore();
|
||||||
|
const { setMasterKey } = useKeyStore();
|
||||||
|
|
||||||
const isAuthenticated = !!accessToken;
|
const isAuthenticated = !!accessToken;
|
||||||
|
|
||||||
const login = (access: string, profile: UserProfile) => {
|
// called after successful login — derive & save master key
|
||||||
|
const login = async (
|
||||||
|
access: string,
|
||||||
|
profile: UserProfile,
|
||||||
|
password: string,
|
||||||
|
) => {
|
||||||
|
const masterKey = await CryptoUtils.deriveMasterKey(
|
||||||
|
password,
|
||||||
|
profile.email,
|
||||||
|
);
|
||||||
|
await saveMasterKey(masterKey);
|
||||||
|
setMasterKey(masterKey);
|
||||||
setAuth(access, profile);
|
setAuth(access, profile);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -18,6 +38,8 @@ export const useAuth = () => {
|
|||||||
await api.post(endpoints.LOGOUT);
|
await api.post(endpoints.LOGOUT);
|
||||||
} finally {
|
} finally {
|
||||||
clearAuth();
|
clearAuth();
|
||||||
|
setMasterKey(null);
|
||||||
|
await clearMasterKey();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -34,17 +56,20 @@ export const useAuth = () => {
|
|||||||
try {
|
try {
|
||||||
// try refresh
|
// try refresh
|
||||||
const { data: refreshData } = await publicApi.post(endpoints.REFRESH);
|
const { data: refreshData } = await publicApi.post(endpoints.REFRESH);
|
||||||
// fetch user profile with the new access token
|
|
||||||
const { data: userData } = await api.get(endpoints.ME, {
|
const { data: userData } = await api.get(endpoints.ME, {
|
||||||
headers: { Authorization: `Bearer ${refreshData.access}` },
|
headers: { Authorization: `Bearer ${refreshData.access}` },
|
||||||
});
|
});
|
||||||
// update auth details in memory
|
|
||||||
setAuth(refreshData.access, userData);
|
setAuth(refreshData.access, userData);
|
||||||
} catch (err) {
|
|
||||||
console.error("Initialization failed:", err);
|
// restore master key from IndexedDB
|
||||||
|
const masterKey = await loadMasterKey();
|
||||||
|
if (masterKey) setMasterKey(masterKey);
|
||||||
|
} catch {
|
||||||
clearAuth();
|
clearAuth();
|
||||||
|
setMasterKey(null);
|
||||||
|
await clearMasterKey();
|
||||||
}
|
}
|
||||||
}, []);
|
}, [setMasterKey]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export default function Login() {
|
|||||||
headers: { Authorization: `Bearer ${authData.access}` },
|
headers: { Authorization: `Bearer ${authData.access}` },
|
||||||
});
|
});
|
||||||
|
|
||||||
login(authData.access, userData);
|
login(authData.access, userData, data.password);
|
||||||
|
|
||||||
navigate(ROUTES.DRAWER);
|
navigate(ROUTES.DRAWER);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -11,18 +11,36 @@ export interface EncryptedLetter {
|
|||||||
export interface EncryptedImageUpload {
|
export interface EncryptedImageUpload {
|
||||||
encryptedBlob: Blob;
|
encryptedBlob: Blob;
|
||||||
encrypted_dek: string;
|
encrypted_dek: string;
|
||||||
|
filename: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Wrapper functions
|
||||||
|
*/
|
||||||
|
interface SealedEnvelope {
|
||||||
|
encryptedContent: string;
|
||||||
|
encrypted_dek: string;
|
||||||
sharingKey: string;
|
sharingKey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PBKDF2_ITERATIONS = 100_000;
|
export class CryptoUtils {
|
||||||
const AES_GCM = { name: "AES-GCM", length: 256 } as const;
|
private dek: CryptoKey = {} as CryptoKey;
|
||||||
|
private static readonly PBKDF2_ITERATIONS = 100_000;
|
||||||
|
private static readonly AES_GCM = { name: "AES-GCM", length: 256 };
|
||||||
|
|
||||||
|
async initialize() {
|
||||||
|
this.dek = await crypto.subtle.generateKey(CryptoUtils.AES_GCM, true, [
|
||||||
|
"encrypt",
|
||||||
|
"decrypt",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
// base64 conversion for transit
|
// base64 conversion for transit
|
||||||
const toBase64 = (buf: Uint8Array): string =>
|
toBase64 = (buf: Uint8Array): string =>
|
||||||
btoa(buf.reduce((s, b) => s + String.fromCharCode(b), ""));
|
btoa(buf.reduce((s, b) => s + String.fromCharCode(b), ""));
|
||||||
|
|
||||||
// explicit loop ensures Uint8Array<ArrayBuffer> (not ArrayBufferLike)
|
// explicit loop ensures Uint8Array<ArrayBuffer> (not ArrayBufferLike)
|
||||||
const fromBase64 = (b64: string): Uint8Array<ArrayBuffer> => {
|
fromBase64 = (b64: string): Uint8Array<ArrayBuffer> => {
|
||||||
const str = atob(b64);
|
const str = atob(b64);
|
||||||
const arr = new Uint8Array(str.length);
|
const arr = new Uint8Array(str.length);
|
||||||
for (let i = 0; i < str.length; i++) arr[i] = str.charCodeAt(i);
|
for (let i = 0; i < str.length; i++) arr[i] = str.charCodeAt(i);
|
||||||
@@ -30,18 +48,18 @@ const fromBase64 = (b64: string): Uint8Array<ArrayBuffer> => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// bundle IV + data into a single base64 string
|
// bundle IV + data into a single base64 string
|
||||||
const packWithIv = (iv: Uint8Array, data: ArrayBuffer): string => {
|
packWithIv = (iv: Uint8Array, data: ArrayBuffer): string => {
|
||||||
const packed = new Uint8Array(iv.length + data.byteLength);
|
const packed = new Uint8Array(iv.length + data.byteLength);
|
||||||
packed.set(iv);
|
packed.set(iv);
|
||||||
packed.set(new Uint8Array(data), iv.length);
|
packed.set(new Uint8Array(data), iv.length);
|
||||||
return toBase64(packed);
|
return this.toBase64(packed);
|
||||||
};
|
};
|
||||||
|
|
||||||
// split IV (first 12 bytes) back out from a packed base64 bundle
|
// split IV (first 12 bytes) back out from a packed base64 bundle
|
||||||
const unpackWithIv = (
|
unpackWithIv = (
|
||||||
b64: string,
|
b64: string,
|
||||||
): [Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>] => {
|
): [Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>] => {
|
||||||
const buf = fromBase64(b64); // ArrayBuffer-backed, so buf.buffer is ArrayBuffer
|
const buf = this.fromBase64(b64); // ArrayBuffer-backed, so buf.buffer is ArrayBuffer
|
||||||
return [new Uint8Array(buf.buffer, 0, 12), new Uint8Array(buf.buffer, 12)];
|
return [new Uint8Array(buf.buffer, 0, 12), new Uint8Array(buf.buffer, 12)];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -49,7 +67,7 @@ const unpackWithIv = (
|
|||||||
* Derives a Master Key from a password and email (salt).
|
* Derives a Master Key from a password and email (salt).
|
||||||
* Deterministic — same credentials always produce the same key.
|
* Deterministic — same credentials always produce the same key.
|
||||||
*/
|
*/
|
||||||
export async function deriveMasterKey(
|
public static async deriveMasterKey(
|
||||||
password: string,
|
password: string,
|
||||||
email: string,
|
email: string,
|
||||||
): Promise<CryptoKey> {
|
): Promise<CryptoKey> {
|
||||||
@@ -67,82 +85,67 @@ export async function deriveMasterKey(
|
|||||||
{
|
{
|
||||||
name: "PBKDF2",
|
name: "PBKDF2",
|
||||||
salt: enc.encode(email.toLowerCase()),
|
salt: enc.encode(email.toLowerCase()),
|
||||||
iterations: PBKDF2_ITERATIONS,
|
iterations: CryptoUtils.PBKDF2_ITERATIONS,
|
||||||
hash: "SHA-256",
|
hash: "SHA-256",
|
||||||
},
|
},
|
||||||
baseKey,
|
baseKey,
|
||||||
AES_GCM,
|
CryptoUtils.AES_GCM,
|
||||||
false,
|
false,
|
||||||
["encrypt", "decrypt", "wrapKey", "unwrapKey"],
|
["encrypt", "decrypt", "wrapKey", "unwrapKey"],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
private async sealEnvelope(
|
||||||
* Wrapper functions
|
|
||||||
*/
|
|
||||||
interface SealedEnvelope {
|
|
||||||
encryptedContent: string;
|
|
||||||
encrypted_dek: string;
|
|
||||||
sharingKey: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sealEnvelope(
|
|
||||||
input: Uint8Array,
|
input: Uint8Array,
|
||||||
masterKey: CryptoKey,
|
masterKey: CryptoKey,
|
||||||
): Promise<SealedEnvelope> {
|
): Promise<SealedEnvelope> {
|
||||||
// copy into a fresh ArrayBuffer — WebCrypto requires ArrayBuffer-backed arrays
|
// copy into a fresh ArrayBuffer — WebCrypto requires ArrayBuffer-backed arrays
|
||||||
const plainBytes = new Uint8Array(input);
|
const plainBytes = new Uint8Array(input);
|
||||||
|
|
||||||
// 1-time DEK for this payload
|
|
||||||
const dek = await crypto.subtle.generateKey(AES_GCM, true, [
|
|
||||||
"encrypt",
|
|
||||||
"decrypt",
|
|
||||||
]);
|
|
||||||
|
|
||||||
// encrypt the content with the DEK
|
// encrypt the content with the DEK
|
||||||
const contentIv = crypto.getRandomValues(new Uint8Array(12));
|
const contentIv = crypto.getRandomValues(new Uint8Array(12));
|
||||||
const ciphertext = await crypto.subtle.encrypt(
|
const ciphertext = await crypto.subtle.encrypt(
|
||||||
{ name: "AES-GCM", iv: contentIv },
|
{ name: "AES-GCM", iv: contentIv },
|
||||||
dek,
|
this.dek,
|
||||||
plainBytes,
|
plainBytes,
|
||||||
);
|
);
|
||||||
|
|
||||||
// wrap the DEK with the Master Key (for self/owner access)
|
// wrap the DEK with the Master Key (for self/owner access)
|
||||||
const dekIv = crypto.getRandomValues(new Uint8Array(12));
|
const dekIv = crypto.getRandomValues(new Uint8Array(12));
|
||||||
const wrappedDek = await crypto.subtle.wrapKey("raw", dek, masterKey, {
|
const wrappedDek = await crypto.subtle.wrapKey("raw", this.dek, masterKey, {
|
||||||
name: "AES-GCM",
|
name: "AES-GCM",
|
||||||
iv: dekIv,
|
iv: dekIv,
|
||||||
});
|
});
|
||||||
|
|
||||||
// export raw DEK for the share URL (recipient access, no master key needed)
|
// export raw DEK for the share URL (recipient access, no master key needed)
|
||||||
const rawDek = await crypto.subtle.exportKey("raw", dek);
|
const rawDek = await crypto.subtle.exportKey("raw", this.dek);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
encryptedContent: packWithIv(contentIv, ciphertext),
|
encryptedContent: this.packWithIv(contentIv, ciphertext),
|
||||||
encrypted_dek: packWithIv(dekIv, wrappedDek),
|
encrypted_dek: this.packWithIv(dekIv, wrappedDek),
|
||||||
sharingKey: toBase64(new Uint8Array(rawDek)),
|
sharingKey: this.toBase64(new Uint8Array(rawDek)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openEnvelope(
|
private async openEnvelope(
|
||||||
encryptedContent: string,
|
encryptedContent: string,
|
||||||
encrypted_dek: string,
|
encrypted_dek: string,
|
||||||
masterKey: CryptoKey,
|
masterKey: CryptoKey,
|
||||||
): Promise<Uint8Array<ArrayBuffer>> {
|
): Promise<Uint8Array<ArrayBuffer>> {
|
||||||
// unwrap the DEK using the master key
|
// unwrap the DEK using the master key
|
||||||
const [dekIv, wrappedDek] = unpackWithIv(encrypted_dek);
|
const [dekIv, wrappedDek] = this.unpackWithIv(encrypted_dek);
|
||||||
const dek = await crypto.subtle.unwrapKey(
|
const dek = await crypto.subtle.unwrapKey(
|
||||||
"raw",
|
"raw",
|
||||||
wrappedDek,
|
wrappedDek,
|
||||||
masterKey,
|
masterKey,
|
||||||
{ name: "AES-GCM", iv: dekIv },
|
{ name: "AES-GCM", iv: dekIv },
|
||||||
AES_GCM,
|
CryptoUtils.AES_GCM,
|
||||||
false,
|
false,
|
||||||
["decrypt"],
|
["decrypt"],
|
||||||
);
|
);
|
||||||
|
|
||||||
// decrypt the content with the recovered DEK
|
// decrypt the content with the recovered DEK
|
||||||
const [contentIv, ciphertext] = unpackWithIv(encryptedContent);
|
const [contentIv, ciphertext] = this.unpackWithIv(encryptedContent);
|
||||||
const plainBytes = await crypto.subtle.decrypt(
|
const plainBytes = await crypto.subtle.decrypt(
|
||||||
{ name: "AES-GCM", iv: contentIv },
|
{ name: "AES-GCM", iv: contentIv },
|
||||||
dek,
|
dek,
|
||||||
@@ -155,23 +158,21 @@ async function openEnvelope(
|
|||||||
/*
|
/*
|
||||||
* Letter functions
|
* Letter functions
|
||||||
*/
|
*/
|
||||||
export async function encryptLetter(
|
public async encryptLetter(
|
||||||
plaintext: string,
|
plaintext: string,
|
||||||
masterKey: CryptoKey,
|
masterKey: CryptoKey,
|
||||||
): Promise<EncryptedLetter> {
|
): Promise<EncryptedLetter> {
|
||||||
const { encryptedContent, encrypted_dek, sharingKey } = await sealEnvelope(
|
const { encryptedContent, encrypted_dek, sharingKey } =
|
||||||
new TextEncoder().encode(plaintext),
|
await this.sealEnvelope(new TextEncoder().encode(plaintext), masterKey);
|
||||||
masterKey,
|
|
||||||
);
|
|
||||||
|
|
||||||
return { encrypted_content: encryptedContent, encrypted_dek, sharingKey };
|
return { encrypted_content: encryptedContent, encrypted_dek, sharingKey };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function decryptLetter(
|
public async decryptLetter(
|
||||||
{ encrypted_content, encrypted_dek }: EncryptedLetter,
|
{ encrypted_content, encrypted_dek }: EncryptedLetter,
|
||||||
masterKey: CryptoKey,
|
masterKey: CryptoKey,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const plainBytes = await openEnvelope(
|
const plainBytes = await this.openEnvelope(
|
||||||
encrypted_content,
|
encrypted_content,
|
||||||
encrypted_dek,
|
encrypted_dek,
|
||||||
masterKey,
|
masterKey,
|
||||||
@@ -182,11 +183,12 @@ export async function decryptLetter(
|
|||||||
/*
|
/*
|
||||||
* Metadata functions
|
* Metadata functions
|
||||||
*/
|
*/
|
||||||
export async function encryptMetadata(
|
public async encryptMetadata(
|
||||||
metadata: Record<string, string>,
|
metadata: Record<string, string>,
|
||||||
masterKey: CryptoKey,
|
masterKey: CryptoKey,
|
||||||
): Promise<EncryptedLetter> {
|
): Promise<EncryptedLetter> {
|
||||||
const { encryptedContent, encrypted_dek, sharingKey } = await sealEnvelope(
|
const { encryptedContent, encrypted_dek, sharingKey } =
|
||||||
|
await this.sealEnvelope(
|
||||||
new TextEncoder().encode(JSON.stringify(metadata)),
|
new TextEncoder().encode(JSON.stringify(metadata)),
|
||||||
masterKey,
|
masterKey,
|
||||||
);
|
);
|
||||||
@@ -194,11 +196,11 @@ export async function encryptMetadata(
|
|||||||
return { encrypted_content: encryptedContent, encrypted_dek, sharingKey };
|
return { encrypted_content: encryptedContent, encrypted_dek, sharingKey };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function decryptMetadata(
|
public async decryptMetadata(
|
||||||
encrypted_metadata: EncryptedLetter,
|
encrypted_metadata: EncryptedLetter,
|
||||||
masterKey: CryptoKey,
|
masterKey: CryptoKey,
|
||||||
): Promise<Record<string, string>> {
|
): Promise<Record<string, string>> {
|
||||||
const plainBytes = await openEnvelope(
|
const plainBytes = await this.openEnvelope(
|
||||||
encrypted_metadata.encrypted_content,
|
encrypted_metadata.encrypted_content,
|
||||||
encrypted_metadata.encrypted_dek,
|
encrypted_metadata.encrypted_dek,
|
||||||
masterKey,
|
masterKey,
|
||||||
@@ -209,34 +211,31 @@ export async function decryptMetadata(
|
|||||||
/*
|
/*
|
||||||
* Image functions
|
* Image functions
|
||||||
*/
|
*/
|
||||||
export async function encryptImage(
|
public async encryptImage(
|
||||||
file: File,
|
file: File,
|
||||||
masterKey: CryptoKey,
|
masterKey: CryptoKey,
|
||||||
): Promise<EncryptedImageUpload> {
|
): Promise<EncryptedImageUpload> {
|
||||||
const plainBytes = new Uint8Array(await file.arrayBuffer());
|
const plainBytes = new Uint8Array(await file.arrayBuffer());
|
||||||
const { encryptedContent, encrypted_dek, sharingKey } = await sealEnvelope(
|
const { encryptedContent, encrypted_dek } = await this.sealEnvelope(
|
||||||
plainBytes,
|
plainBytes,
|
||||||
masterKey,
|
masterKey,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
encryptedBlob: new Blob([fromBase64(encryptedContent)]),
|
encryptedBlob: new Blob([this.fromBase64(encryptedContent)]),
|
||||||
encrypted_dek,
|
encrypted_dek,
|
||||||
sharingKey,
|
filename: `${crypto.randomUUID()}.bin`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function decryptImage(
|
public async decryptImage(
|
||||||
encryptedUrl: string,
|
encryptedBlob: Blob,
|
||||||
encrypted_dek: string,
|
encrypted_dek: string,
|
||||||
masterKey: CryptoKey,
|
masterKey: CryptoKey,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
// fetch encrypted bytes from server and repack as base64 for openEnvelope
|
const encryptedBytes = new Uint8Array(await encryptedBlob.arrayBuffer());
|
||||||
const encryptedBytes = new Uint8Array(
|
const plainBytes = await this.openEnvelope(
|
||||||
await (await fetch(encryptedUrl)).arrayBuffer(),
|
this.toBase64(encryptedBytes),
|
||||||
);
|
|
||||||
const plainBytes = await openEnvelope(
|
|
||||||
toBase64(encryptedBytes),
|
|
||||||
encrypted_dek,
|
encrypted_dek,
|
||||||
masterKey,
|
masterKey,
|
||||||
);
|
);
|
||||||
@@ -244,3 +243,4 @@ export async function decryptImage(
|
|||||||
// return as object URL for use in Fabric / <img>
|
// return as object URL for use in Fabric / <img>
|
||||||
return URL.createObjectURL(new Blob([plainBytes]));
|
return URL.createObjectURL(new Blob([plainBytes]));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { openDB } from "idb";
|
||||||
|
|
||||||
|
// we use this to store master key in browser - secure and good UX
|
||||||
|
const db = openDB("piku-keys", 1, {
|
||||||
|
upgrade(db) {
|
||||||
|
db.createObjectStore("master-key");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const saveMasterKey = async (key: CryptoKey) =>
|
||||||
|
(await db).put("master-key", key, "masterKey");
|
||||||
|
|
||||||
|
export const loadMasterKey = async (): Promise<CryptoKey | null> =>
|
||||||
|
(await db).get("master-key", "masterKey") ?? null;
|
||||||
|
|
||||||
|
export const clearMasterKey = async () =>
|
||||||
|
(await db).delete("master-key", "masterKey");
|
||||||
Reference in New Issue
Block a user