mirror of
https://github.com/kmc7468/arkvault.git
synced 2026-11-04 08:10:28 +09:00
키 관리 로직을 Keyring으로 통합하여 하드코딩된 MEK 및 HSK 버전을 제거하고 Svelte Store를 Rune으로 대체
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
aee87fd9b6
commit
2ce73aa331
50 files changed
+633
-641
No files matched your search
Vendored
+1
-1
@@ -1,2 +1,2 @@
|
||||
export * from "./fileCache";
|
||||
export * from "./thumbnailCache";
|
||||
export * from "./thumbnailCache.svelte";
|
||||
+29
-20
@@ -1,12 +1,19 @@
|
||||
import { LRUCache } from "lru-cache";
|
||||
import { writable, type Writable } from "svelte/store";
|
||||
import { getThumbnailUrl } from "#lib/client/media/thumbnail.js";
|
||||
import { readFile, writeFile, deleteFile, deleteDirectory } from "#lib/client/storage/opfs.js";
|
||||
import { deleteDirectory, deleteFile, readFile, writeFile } from "#lib/client/storage/opfs.js";
|
||||
import type { SummarizedFileInfo } from "#lib/modules/filesystem/index.js";
|
||||
import { decryptChunk } from "#lib/shared/crypto/index.js";
|
||||
import { decryptChunk, encodeToBase64 } from "#lib/shared/crypto/index.js";
|
||||
|
||||
const loadedThumbnails = new LRUCache<number, Writable<string>>({ max: 100 });
|
||||
const loadingThumbnails = new Map<number, Writable<string | undefined>>();
|
||||
export interface Thumbnail {
|
||||
url?: string;
|
||||
}
|
||||
|
||||
const getThumbnailUrl = (thumbnailBuffer: ArrayBuffer) => {
|
||||
return `data:image/webp;base64,${encodeToBase64(thumbnailBuffer)}`;
|
||||
};
|
||||
|
||||
// Thumbnails are reactive so that they can be updated in place
|
||||
const loadedThumbnails = new LRUCache<number, Thumbnail>({ max: 100 });
|
||||
const loadingThumbnails = new Map<number, Thumbnail>();
|
||||
|
||||
const fetchFromOpfs = async (fileId: number) => {
|
||||
const thumbnailBuffer = await readFile(`/thumbnail/file/${fileId}`);
|
||||
@@ -30,22 +37,22 @@ export const getFileThumbnail = (file: SummarizedFileInfo) => {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const thumbnail = loadedThumbnails.get(file.id);
|
||||
const thumbnail = loadedThumbnails.get(file.id) ?? loadingThumbnails.get(file.id);
|
||||
if (thumbnail) return thumbnail;
|
||||
|
||||
let loadingThumbnail = loadingThumbnails.get(file.id);
|
||||
if (loadingThumbnail) return loadingThumbnail;
|
||||
|
||||
loadingThumbnail = writable(undefined);
|
||||
const loadingThumbnail: Thumbnail = $state({});
|
||||
loadingThumbnails.set(file.id, loadingThumbnail);
|
||||
|
||||
fetchFromOpfs(file.id)
|
||||
.then((thumbnail) => thumbnail ?? (file.dataKey && fetchFromServer(file.id, file.dataKey.key)))
|
||||
.then((thumbnail) => {
|
||||
if (thumbnail) {
|
||||
loadingThumbnail.set(thumbnail);
|
||||
loadedThumbnails.set(file.id, loadingThumbnail as Writable<string>);
|
||||
.then((url) => url ?? (file.dataKey && fetchFromServer(file.id, file.dataKey.key)))
|
||||
.then((url) => {
|
||||
if (url) {
|
||||
loadingThumbnail.url = url;
|
||||
loadedThumbnails.set(file.id, loadingThumbnail);
|
||||
}
|
||||
})
|
||||
.catch(console.error)
|
||||
.finally(() => {
|
||||
loadingThumbnails.delete(file.id);
|
||||
});
|
||||
return loadingThumbnail;
|
||||
@@ -54,11 +61,13 @@ export const getFileThumbnail = (file: SummarizedFileInfo) => {
|
||||
export const storeFileThumbnailCache = async (fileId: number, thumbnailBuffer: ArrayBuffer) => {
|
||||
await writeFile(`/thumbnail/file/${fileId}`, thumbnailBuffer);
|
||||
|
||||
const oldThumbnail = loadedThumbnails.get(fileId);
|
||||
if (oldThumbnail) {
|
||||
oldThumbnail.set(getThumbnailUrl(thumbnailBuffer));
|
||||
const url = getThumbnailUrl(thumbnailBuffer);
|
||||
const thumbnail = loadedThumbnails.get(fileId) ?? loadingThumbnails.get(fileId);
|
||||
if (thumbnail) {
|
||||
thumbnail.url = url;
|
||||
} else {
|
||||
loadedThumbnails.set(fileId, writable(getThumbnailUrl(thumbnailBuffer)));
|
||||
const newThumbnail: Thumbnail = $state({ url });
|
||||
loadedThumbnails.set(fileId, newThumbnail);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import {
|
||||
concatenateBuffers,
|
||||
digestMessage,
|
||||
exportRSAKey,
|
||||
exportRSAKeyToBase64,
|
||||
generateEncryptionKeyPair,
|
||||
generateHmacSecret,
|
||||
generateMasterKey,
|
||||
generateSigningKeyPair,
|
||||
importEncryptionKeyPairFromBase64,
|
||||
importSigningKeyPairFromBase64,
|
||||
makeAESKeyNonextractable,
|
||||
makeRSAKeyNonextractable,
|
||||
wrapHmacSecret,
|
||||
wrapMasterKey,
|
||||
} from "#lib/shared/crypto/index.js";
|
||||
import { keyring } from "./keyring.svelte";
|
||||
import { deserializeClientKeys } from "./serialization";
|
||||
|
||||
// Generates new client keys and keeps them in the keyring without persisting
|
||||
export const generateClientKeys = async () => {
|
||||
const { encryptKey, decryptKey } = await generateEncryptionKeyPair();
|
||||
const { signKey, verifyKey } = await generateSigningKeyPair();
|
||||
|
||||
keyring.setClientKeys({
|
||||
encryptKey,
|
||||
decryptKey: await makeRSAKeyNonextractable(decryptKey),
|
||||
signKey: await makeRSAKeyNonextractable(signKey),
|
||||
verifyKey,
|
||||
});
|
||||
|
||||
return {
|
||||
encryptKey,
|
||||
encryptKeyBase64: await exportRSAKeyToBase64(encryptKey),
|
||||
decryptKeyBase64: await exportRSAKeyToBase64(decryptKey),
|
||||
signKeyBase64: await exportRSAKeyToBase64(signKey),
|
||||
verifyKeyBase64: await exportRSAKeyToBase64(verifyKey),
|
||||
};
|
||||
};
|
||||
|
||||
export const importClientKeys = async (clientKeysSerialized: string) => {
|
||||
const clientKeys = deserializeClientKeys(clientKeysSerialized);
|
||||
if (!clientKeys) return false;
|
||||
|
||||
const { encryptKey, decryptKey } = await importEncryptionKeyPairFromBase64(
|
||||
clientKeys.encryptKeyBase64,
|
||||
clientKeys.decryptKeyBase64,
|
||||
);
|
||||
const { signKey, verifyKey } = await importSigningKeyPairFromBase64(
|
||||
clientKeys.signKeyBase64,
|
||||
clientKeys.verifyKeyBase64,
|
||||
);
|
||||
|
||||
keyring.setClientKeys({
|
||||
encryptKey,
|
||||
decryptKey: await makeRSAKeyNonextractable(decryptKey),
|
||||
signKey: await makeRSAKeyNonextractable(signKey),
|
||||
verifyKey,
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
export const generateInitialMasterKey = async (encryptKey: CryptoKey) => {
|
||||
const { masterKey } = await generateMasterKey();
|
||||
return {
|
||||
masterKey: await makeAESKeyNonextractable(masterKey),
|
||||
masterKeyWrapped: await wrapMasterKey(masterKey, encryptKey),
|
||||
};
|
||||
};
|
||||
|
||||
export const generateInitialHmacSecret = async (masterKey: CryptoKey) => {
|
||||
const { hmacSecret } = await generateHmacSecret();
|
||||
return {
|
||||
hmacSecretWrapped: await wrapHmacSecret(hmacSecret, masterKey),
|
||||
};
|
||||
};
|
||||
|
||||
export const generatePublicKeyFingerprint = async (encryptKey: CryptoKey, 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))
|
||||
.map((byte) => byte.toString(16).padStart(2, "0"))
|
||||
.join("")
|
||||
.toUpperCase()
|
||||
.match(/.{1,4}/g)!
|
||||
.join(" ");
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from "./generation";
|
||||
export * from "./keyring.svelte";
|
||||
export * from "./serialization";
|
||||
export * from "./sync";
|
||||
export * from "./types";
|
||||
@@ -0,0 +1,113 @@
|
||||
import {
|
||||
getClientKey,
|
||||
getHmacSecrets,
|
||||
getMasterKeys,
|
||||
storeClientKey,
|
||||
storeHmacSecrets,
|
||||
storeMasterKeys,
|
||||
} from "#lib/client/storage/indexedDB/index.js";
|
||||
import type { ClientKeys, HmacSecret, MasterKey } from "./types";
|
||||
|
||||
const findLatestActive = <T extends { version: number; state: string }>(
|
||||
entries: ReadonlyMap<number, T>,
|
||||
) => {
|
||||
return entries
|
||||
.values()
|
||||
.filter(({ state }) => state === "active")
|
||||
.reduce<T | undefined>(
|
||||
(latest, entry) => ((latest?.version ?? 0) > entry.version ? latest : entry),
|
||||
undefined,
|
||||
);
|
||||
};
|
||||
|
||||
// Holds the keys of this device and the user. Keys are persisted in IndexedDB.
|
||||
class Keyring {
|
||||
clientKeys = $state.raw<ClientKeys | null>(null);
|
||||
masterKeys = $state.raw<ReadonlyMap<number, MasterKey>>(new Map());
|
||||
hmacSecrets = $state.raw<ReadonlyMap<number, HmacSecret>>(new Map());
|
||||
|
||||
async load() {
|
||||
const [encryptKey, decryptKey, signKey, verifyKey, masterKeys, hmacSecrets] = await Promise.all(
|
||||
[
|
||||
getClientKey("encrypt"),
|
||||
getClientKey("decrypt"),
|
||||
getClientKey("sign"),
|
||||
getClientKey("verify"),
|
||||
getMasterKeys(),
|
||||
getHmacSecrets(),
|
||||
],
|
||||
);
|
||||
if (encryptKey && decryptKey && signKey && verifyKey) {
|
||||
this.clientKeys = { encryptKey, decryptKey, signKey, verifyKey };
|
||||
}
|
||||
this.masterKeys = new Map(masterKeys.map((masterKey) => [masterKey.version, masterKey]));
|
||||
this.hmacSecrets = new Map(hmacSecrets.map((hmacSecret) => [hmacSecret.version, hmacSecret]));
|
||||
}
|
||||
|
||||
// Client keys are kept in memory only until they are registered and persisted
|
||||
setClientKeys(clientKeys: ClientKeys) {
|
||||
this.clientKeys = clientKeys;
|
||||
}
|
||||
|
||||
async persistClientKeys() {
|
||||
const clientKeys = this.requireClientKeys();
|
||||
await Promise.all([
|
||||
storeClientKey(clientKeys.encryptKey, "encrypt"),
|
||||
storeClientKey(clientKeys.decryptKey, "decrypt"),
|
||||
storeClientKey(clientKeys.signKey, "sign"),
|
||||
storeClientKey(clientKeys.verifyKey, "verify"),
|
||||
]);
|
||||
}
|
||||
|
||||
async setMasterKeys(masterKeys: MasterKey[]) {
|
||||
await storeMasterKeys(masterKeys);
|
||||
this.masterKeys = new Map(masterKeys.map((masterKey) => [masterKey.version, masterKey]));
|
||||
}
|
||||
|
||||
async setHmacSecrets(hmacSecrets: HmacSecret[]) {
|
||||
await storeHmacSecrets(hmacSecrets);
|
||||
this.hmacSecrets = new Map(hmacSecrets.map((hmacSecret) => [hmacSecret.version, hmacSecret]));
|
||||
}
|
||||
|
||||
get hasMasterKeys() {
|
||||
return this.masterKeys.size > 0;
|
||||
}
|
||||
|
||||
get hasHmacSecrets() {
|
||||
return this.hmacSecrets.size > 0;
|
||||
}
|
||||
|
||||
requireClientKeys() {
|
||||
if (!this.clientKeys) {
|
||||
throw new Error("Client keys not found");
|
||||
}
|
||||
return this.clientKeys;
|
||||
}
|
||||
|
||||
getMasterKey(version: number) {
|
||||
const masterKey = this.masterKeys.get(version);
|
||||
if (!masterKey) {
|
||||
throw new Error(`Master key (version ${version}) not found`);
|
||||
}
|
||||
return masterKey;
|
||||
}
|
||||
|
||||
// The master key used to encrypt new data
|
||||
get activeMasterKey() {
|
||||
const masterKey = findLatestActive(this.masterKeys);
|
||||
if (!masterKey) {
|
||||
throw new Error("Active master key not found");
|
||||
}
|
||||
return masterKey;
|
||||
}
|
||||
|
||||
get activeHmacSecret() {
|
||||
const hmacSecret = findLatestActive(this.hmacSecrets);
|
||||
if (!hmacSecret) {
|
||||
throw new Error("Active HMAC secret not found");
|
||||
}
|
||||
return hmacSecret;
|
||||
}
|
||||
}
|
||||
|
||||
export const keyring = new Keyring();
|
||||
@@ -0,0 +1,54 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const SerializedClientKeysSchema = z.intersection(
|
||||
z.object({
|
||||
generator: z.literal("ArkVault"),
|
||||
exportedAt: z.iso.datetime(),
|
||||
}),
|
||||
z.object({
|
||||
version: z.literal(1),
|
||||
encryptKey: z.base64().nonempty(),
|
||||
decryptKey: z.base64().nonempty(),
|
||||
signKey: z.base64().nonempty(),
|
||||
verifyKey: z.base64().nonempty(),
|
||||
}),
|
||||
);
|
||||
|
||||
type SerializedClientKeys = z.infer<typeof SerializedClientKeysSchema>;
|
||||
|
||||
type DeserializedClientKeys = {
|
||||
encryptKeyBase64: string;
|
||||
decryptKeyBase64: string;
|
||||
signKeyBase64: string;
|
||||
verifyKeyBase64: string;
|
||||
};
|
||||
|
||||
export const serializeClientKeys = ({
|
||||
encryptKeyBase64,
|
||||
decryptKeyBase64,
|
||||
signKeyBase64,
|
||||
verifyKeyBase64,
|
||||
}: DeserializedClientKeys) => {
|
||||
return JSON.stringify({
|
||||
version: 1,
|
||||
generator: "ArkVault",
|
||||
exportedAt: new Date().toISOString(),
|
||||
encryptKey: encryptKeyBase64,
|
||||
decryptKey: decryptKeyBase64,
|
||||
signKey: signKeyBase64,
|
||||
verifyKey: verifyKeyBase64,
|
||||
} satisfies SerializedClientKeys);
|
||||
};
|
||||
|
||||
export const deserializeClientKeys = (serialized: string) => {
|
||||
const zodRes = SerializedClientKeysSchema.safeParse(JSON.parse(serialized));
|
||||
if (zodRes.success) {
|
||||
return {
|
||||
encryptKeyBase64: zodRes.data.encryptKey,
|
||||
decryptKeyBase64: zodRes.data.decryptKey,
|
||||
signKeyBase64: zodRes.data.signKey,
|
||||
verifyKeyBase64: zodRes.data.verifyKey,
|
||||
} satisfies DeserializedClientKeys;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
@@ -0,0 +1,182 @@
|
||||
import {
|
||||
decryptChallenge,
|
||||
encodeToBase64,
|
||||
exportRSAKeyToBase64,
|
||||
signMasterKeyWrapped,
|
||||
signMessageRSA,
|
||||
unwrapHmacSecret,
|
||||
unwrapMasterKey,
|
||||
verifyMasterKeyWrapped,
|
||||
} from "#lib/shared/crypto/index.js";
|
||||
import { isTRPCClientError, trpc } from "#trpc/client.js";
|
||||
import { keyring } from "./keyring.svelte";
|
||||
import type { ClientKeys } from "./types";
|
||||
|
||||
// Synchronizes keys with the server
|
||||
|
||||
const exportPublicKeys = async ({ encryptKey, verifyKey }: ClientKeys) => ({
|
||||
encPubKey: await exportRSAKeyToBase64(encryptKey),
|
||||
sigPubKey: await exportRSAKeyToBase64(verifyKey),
|
||||
});
|
||||
|
||||
const answerChallenge = async (challenge: string, { decryptKey, signKey }: ClientKeys) => {
|
||||
const answer = await decryptChallenge(challenge, decryptKey);
|
||||
return encodeToBase64(await signMessageRSA(answer, signKey));
|
||||
};
|
||||
|
||||
export const requestSessionUpgrade = async (force = false) => {
|
||||
const clientKeys = keyring.requireClientKeys();
|
||||
|
||||
let id, challenge;
|
||||
try {
|
||||
({ id, challenge } = await trpc.auth.upgrade.mutate(await exportPublicKeys(clientKeys)));
|
||||
} catch (e) {
|
||||
if (isTRPCClientError(e) && e.data?.code === "FORBIDDEN") {
|
||||
return [false, "Unregistered client"] as const;
|
||||
}
|
||||
return [false] as const;
|
||||
}
|
||||
|
||||
try {
|
||||
await trpc.auth.verifyUpgrade.mutate({
|
||||
id,
|
||||
answerSig: await answerChallenge(challenge, clientKeys),
|
||||
force,
|
||||
});
|
||||
} catch (e) {
|
||||
if (isTRPCClientError(e) && e.data?.code === "CONFLICT") {
|
||||
return [false, "Already logged in"] as const;
|
||||
}
|
||||
return [false] as const;
|
||||
}
|
||||
|
||||
return [true] as const;
|
||||
};
|
||||
|
||||
export const requestClientRegistration = async () => {
|
||||
const clientKeys = keyring.requireClientKeys();
|
||||
|
||||
try {
|
||||
const { id, challenge } = await trpc.client.register.mutate(await exportPublicKeys(clientKeys));
|
||||
await trpc.client.verify.mutate({
|
||||
id,
|
||||
answerSig: await answerChallenge(challenge, clientKeys),
|
||||
});
|
||||
return true;
|
||||
} catch {
|
||||
// TODO: Error Handling
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const requestClientRegistrationAndSessionUpgrade = async (force: boolean) => {
|
||||
const [res, error] = await requestSessionUpgrade(force);
|
||||
if (error === undefined) return [res] as const;
|
||||
|
||||
if (error === "Unregistered client" && !(await requestClientRegistration())) {
|
||||
return [false] as const;
|
||||
} else if (error === "Already logged in") {
|
||||
return [false, force ? undefined : error] as const;
|
||||
}
|
||||
|
||||
return [(await requestSessionUpgrade())[0]] as const;
|
||||
};
|
||||
|
||||
export const registerInitialMasterKeyAndHmacSecret = async (
|
||||
masterKeyWrapped: string,
|
||||
hmacSecretWrapped: string,
|
||||
) => {
|
||||
const { signKey } = keyring.requireClientKeys();
|
||||
|
||||
try {
|
||||
await trpc.mek.registerInitial.mutate({
|
||||
mek: masterKeyWrapped,
|
||||
mekSig: await signMasterKeyWrapped(masterKeyWrapped, 1, signKey),
|
||||
});
|
||||
} catch (e) {
|
||||
if (isTRPCClientError(e) && (e.data?.code === "FORBIDDEN" || e.data?.code === "CONFLICT")) {
|
||||
return true;
|
||||
}
|
||||
// TODO: Error Handling
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await trpc.hsk.registerInitial.mutate({
|
||||
mekVersion: 1,
|
||||
hsk: hmacSecretWrapped,
|
||||
});
|
||||
return true;
|
||||
} catch {
|
||||
// TODO: Error Handling
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const downloadHmacSecrets = async () => {
|
||||
let hmacSecretsWrapped;
|
||||
try {
|
||||
hmacSecretsWrapped = await trpc.hsk.list.query();
|
||||
} catch {
|
||||
// TODO: Error Handling
|
||||
return false;
|
||||
}
|
||||
|
||||
const hmacSecrets = await Promise.all(
|
||||
hmacSecretsWrapped.map(async ({ version, state, mekVersion, hsk: hmacSecretWrapped }) => {
|
||||
const { hmacSecret } = await unwrapHmacSecret(
|
||||
hmacSecretWrapped,
|
||||
keyring.getMasterKey(mekVersion).key,
|
||||
);
|
||||
return { version, state, secret: hmacSecret };
|
||||
}),
|
||||
);
|
||||
|
||||
await keyring.setHmacSecrets(hmacSecrets);
|
||||
return true;
|
||||
};
|
||||
|
||||
// Downloads the master keys (and HMAC secrets) once this client has been approved
|
||||
export const downloadMasterKeys = async () => {
|
||||
const { decryptKey, verifyKey } = keyring.requireClientKeys();
|
||||
|
||||
let masterKeysWrapped;
|
||||
try {
|
||||
masterKeysWrapped = await trpc.mek.list.query();
|
||||
} catch {
|
||||
// TODO: Error Handling
|
||||
return false;
|
||||
}
|
||||
|
||||
const masterKeys = await Promise.all(
|
||||
masterKeysWrapped.map(
|
||||
async ({ version, state, mek: masterKeyWrapped, mekSig: masterKeyWrappedSig }) => {
|
||||
const { masterKey } = await unwrapMasterKey(masterKeyWrapped, decryptKey);
|
||||
return {
|
||||
version,
|
||||
state,
|
||||
key: masterKey,
|
||||
isValid: await verifyMasterKeyWrapped(
|
||||
masterKeyWrapped,
|
||||
version,
|
||||
masterKeyWrappedSig,
|
||||
verifyKey,
|
||||
),
|
||||
};
|
||||
},
|
||||
),
|
||||
);
|
||||
if (masterKeys.length === 0 || !masterKeys.every(({ isValid }) => isValid)) return false;
|
||||
|
||||
await keyring.setMasterKeys(
|
||||
masterKeys.map(({ version, state, key }) => ({ version, state, key })),
|
||||
);
|
||||
await downloadHmacSecrets();
|
||||
return true;
|
||||
};
|
||||
|
||||
export const ensureHmacSecrets = async () => {
|
||||
if (!keyring.hasHmacSecrets && !(await downloadHmacSecrets())) {
|
||||
throw new Error("Failed to download HMAC secrets");
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
export interface ClientKeys {
|
||||
encryptKey: CryptoKey;
|
||||
decryptKey: CryptoKey;
|
||||
signKey: CryptoKey;
|
||||
verifyKey: CryptoKey;
|
||||
}
|
||||
|
||||
export interface MasterKey {
|
||||
version: number;
|
||||
state: "active" | "retired";
|
||||
key: CryptoKey;
|
||||
}
|
||||
|
||||
export interface HmacSecret {
|
||||
version: number;
|
||||
state: "active";
|
||||
secret: CryptoKey;
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import { encodeToBase64 } from "#lib/shared/crypto/index.js";
|
||||
|
||||
const scaleSize = (width: number, height: number, targetSize: number) => {
|
||||
if (width <= targetSize || height <= targetSize) {
|
||||
return { width, height };
|
||||
@@ -116,7 +114,3 @@ export const generateThumbnail = async (blob: Blob) => {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const getThumbnailUrl = (thumbnailBuffer: ArrayBuffer) => {
|
||||
return `data:image/webp;base64,${encodeToBase64(thumbnailBuffer)}`;
|
||||
};
|
||||
@@ -1,5 +1,11 @@
|
||||
import { limitFunction } from "p-limit";
|
||||
import { signMessageHmac } from "#lib/client/crypto/hmac.js";
|
||||
import {
|
||||
ensureHmacSecrets,
|
||||
keyring,
|
||||
type HmacSecret,
|
||||
type MasterKey,
|
||||
} from "#lib/client/keys/index.js";
|
||||
import { extractExifDateTime } from "#lib/client/media/exif.js";
|
||||
import { generateThumbnail } from "#lib/client/media/thumbnail.js";
|
||||
import { uploadBlob } from "#lib/client/transfer/chunkedUpload.js";
|
||||
@@ -10,7 +16,6 @@ import {
|
||||
encryptString,
|
||||
} from "#lib/shared/crypto/index.js";
|
||||
import { getChunkCount } from "#lib/shared/format/index.js";
|
||||
import type { MasterKey, HmacSecret } from "#lib/stores/index.js";
|
||||
import { Scheduler } from "#lib/utils/index.js";
|
||||
import { trpc } from "#trpc/client.js";
|
||||
|
||||
@@ -184,8 +189,6 @@ const requestFileUpload = limitFunction(
|
||||
export const uploadFile = async (
|
||||
file: File,
|
||||
parentId: "root" | number,
|
||||
masterKey: MasterKey,
|
||||
hmacSecret: HmacSecret,
|
||||
onDuplicate: () => Promise<boolean>,
|
||||
) => {
|
||||
uploadingFiles.push({
|
||||
@@ -198,6 +201,10 @@ export const uploadFile = async (
|
||||
|
||||
return await scheduler.schedule(file.size, async () => {
|
||||
try {
|
||||
await ensureHmacSecrets();
|
||||
const masterKey = keyring.activeMasterKey;
|
||||
const hmacSecret = keyring.activeHmacSecret;
|
||||
|
||||
const { fileSigned } = await requestDuplicateFileScan(state, file, hmacSecret, onDuplicate);
|
||||
|
||||
if (!fileSigned) {
|
||||
|
||||
Reference in new issue
Block a user