[web] Iterate on the cluster related crypto (#2737)
This commit is contained in:
@@ -45,9 +45,9 @@ import { convertToJPEG, generateImageThumbnail } from "./services/image";
|
||||
import { logout } from "./services/logout";
|
||||
import { createMLWorker } from "./services/ml";
|
||||
import {
|
||||
encryptionKey,
|
||||
lastShownChangelogVersion,
|
||||
saveEncryptionKey,
|
||||
masterKeyB64,
|
||||
saveMasterKeyB64,
|
||||
setLastShownChangelogVersion,
|
||||
} from "./services/store";
|
||||
import {
|
||||
@@ -103,10 +103,10 @@ export const attachIPCHandlers = () => {
|
||||
|
||||
ipcMain.handle("selectDirectory", () => selectDirectory());
|
||||
|
||||
ipcMain.handle("encryptionKey", () => encryptionKey());
|
||||
ipcMain.handle("masterKeyB64", () => masterKeyB64());
|
||||
|
||||
ipcMain.handle("saveEncryptionKey", (_, encryptionKey: string) =>
|
||||
saveEncryptionKey(encryptionKey),
|
||||
ipcMain.handle("saveMasterKeyB64", (_, masterKeyB64: string) =>
|
||||
saveMasterKeyB64(masterKeyB64),
|
||||
);
|
||||
|
||||
ipcMain.handle("lastShownChangelogVersion", () =>
|
||||
|
||||
@@ -24,13 +24,13 @@ export const clearStores = () => {
|
||||
* On macOS, `safeStorage` stores our data under a Keychain entry named
|
||||
* "<app-name> Safe Storage". In our case, "ente Safe Storage".
|
||||
*/
|
||||
export const saveEncryptionKey = (encryptionKey: string) => {
|
||||
const encryptedKey = safeStorage.encryptString(encryptionKey);
|
||||
export const saveMasterKeyB64 = (masterKeyB64: string) => {
|
||||
const encryptedKey = safeStorage.encryptString(masterKeyB64);
|
||||
const b64EncryptedKey = Buffer.from(encryptedKey).toString("base64");
|
||||
safeStorageStore.set("encryptionKey", b64EncryptedKey);
|
||||
};
|
||||
|
||||
export const encryptionKey = (): string | undefined => {
|
||||
export const masterKeyB64 = (): string | undefined => {
|
||||
const b64EncryptedKey = safeStorageStore.get("encryptionKey");
|
||||
if (!b64EncryptedKey) return undefined;
|
||||
const keyBuffer = Buffer.from(b64EncryptedKey, "base64");
|
||||
|
||||
@@ -103,10 +103,10 @@ const logout = () => {
|
||||
return ipcRenderer.invoke("logout");
|
||||
};
|
||||
|
||||
const encryptionKey = () => ipcRenderer.invoke("encryptionKey");
|
||||
const masterKeyB64 = () => ipcRenderer.invoke("masterKeyB64");
|
||||
|
||||
const saveEncryptionKey = (encryptionKey: string) =>
|
||||
ipcRenderer.invoke("saveEncryptionKey", encryptionKey);
|
||||
const saveMasterKeyB64 = (masterKeyB64: string) =>
|
||||
ipcRenderer.invoke("saveMasterKeyB64", masterKeyB64);
|
||||
|
||||
const lastShownChangelogVersion = () =>
|
||||
ipcRenderer.invoke("lastShownChangelogVersion");
|
||||
@@ -342,8 +342,8 @@ contextBridge.exposeInMainWorld("electron", {
|
||||
openLogDirectory,
|
||||
selectDirectory,
|
||||
logout,
|
||||
encryptionKey,
|
||||
saveEncryptionKey,
|
||||
masterKeyB64,
|
||||
saveMasterKeyB64,
|
||||
lastShownChangelogVersion,
|
||||
setLastShownChangelogVersion,
|
||||
onMainWindowFocus,
|
||||
|
||||
@@ -73,9 +73,9 @@ export default function LandingPage() {
|
||||
const electron = globalThis.electron;
|
||||
if (!key && electron) {
|
||||
try {
|
||||
key = await electron.encryptionKey();
|
||||
key = await electron.masterKeyB64();
|
||||
} catch (e) {
|
||||
log.error("Failed to get encryption key from electron", e);
|
||||
log.error("Failed to read master key from safe storage", e);
|
||||
}
|
||||
if (key) {
|
||||
await saveKeyInSessionStore(
|
||||
|
||||
@@ -1119,11 +1119,8 @@ const encryptFile = async (
|
||||
|
||||
const {
|
||||
encryptedData: thumbEncryptedData,
|
||||
decryptionHeaderB64: thumbDecryptionHeader,
|
||||
} = await worker.encryptThumbnail({
|
||||
data: file.thumbnail,
|
||||
keyB64: fileKey,
|
||||
});
|
||||
decryptionHeader: thumbDecryptionHeader,
|
||||
} = await worker.encryptThumbnail(file.thumbnail, fileKey);
|
||||
const encryptedThumbnail = {
|
||||
encryptedData: thumbEncryptedData,
|
||||
decryptionHeader: thumbDecryptionHeader,
|
||||
|
||||
@@ -125,9 +125,9 @@ const Page: React.FC<PageProps> = ({ appContext }) => {
|
||||
const electron = globalThis.electron;
|
||||
if (!key && electron) {
|
||||
try {
|
||||
key = await electron.encryptionKey();
|
||||
key = await electron.masterKeyB64();
|
||||
} catch (e) {
|
||||
log.error("Failed to get encryption key from electron", e);
|
||||
log.error("Failed to read master key from safe storage", e);
|
||||
}
|
||||
if (key) {
|
||||
await saveKeyInSessionStore(
|
||||
|
||||
@@ -1,77 +1,66 @@
|
||||
/** Careful when adding add other imports! */
|
||||
import * as libsodium from "./libsodium";
|
||||
import type {
|
||||
DecryptBlobB64,
|
||||
DecryptBoxB64,
|
||||
DecryptBoxBytes,
|
||||
EncryptB64,
|
||||
EncryptBytes,
|
||||
EncryptedBlobB64,
|
||||
EncryptedBlobBytes,
|
||||
EncryptJSON,
|
||||
} from "./types";
|
||||
import type { BytesOrB64, EncryptedBlob } from "./types";
|
||||
|
||||
const EncryptB64ToBytes = async ({
|
||||
dataB64,
|
||||
keyB64,
|
||||
}: EncryptB64): Promise<EncryptBytes> => ({
|
||||
data: await libsodium.fromB64(dataB64),
|
||||
keyB64,
|
||||
});
|
||||
export const _encryptBoxB64 = libsodium.encryptBoxB64;
|
||||
|
||||
const EncryptedBlobBytesToB64 = async ({
|
||||
encryptedData,
|
||||
decryptionHeaderB64,
|
||||
}: EncryptedBlobBytes): Promise<EncryptedBlobB64> => ({
|
||||
encryptedDataB64: await libsodium.toB64(encryptedData),
|
||||
decryptionHeaderB64,
|
||||
});
|
||||
export const _encryptBlob = libsodium.encryptBlob;
|
||||
|
||||
export const _encryptBoxB64 = (r: EncryptB64) =>
|
||||
EncryptB64ToBytes(r).then((rb) => libsodium.encryptBox(rb));
|
||||
export const _encryptBlobB64 = libsodium.encryptBlobB64;
|
||||
|
||||
export const _encryptAssociatedData = libsodium.encryptBlob;
|
||||
export const _encryptThumbnail = async (data: BytesOrB64, key: BytesOrB64) => {
|
||||
const { encryptedData, decryptionHeader } = await _encryptBlob(data, key);
|
||||
return {
|
||||
encryptedData,
|
||||
decryptionHeader: await libsodium.toB64(decryptionHeader),
|
||||
};
|
||||
};
|
||||
|
||||
export const _encryptThumbnail = _encryptAssociatedData;
|
||||
export const _encryptMetadataJSON_New = (jsonValue: unknown, key: BytesOrB64) =>
|
||||
_encryptBlobB64(new TextEncoder().encode(JSON.stringify(jsonValue)), key);
|
||||
|
||||
export const _encryptAssociatedDataB64 = (r: EncryptBytes) =>
|
||||
_encryptAssociatedData(r).then(EncryptedBlobBytesToB64);
|
||||
// Deprecated, translates to the old API for now.
|
||||
export const _encryptMetadataJSON = async (r: {
|
||||
jsonValue: unknown;
|
||||
keyB64: string;
|
||||
}) => {
|
||||
const { encryptedData, decryptionHeader } = await _encryptMetadataJSON_New(
|
||||
r.jsonValue,
|
||||
r.keyB64,
|
||||
);
|
||||
return {
|
||||
encryptedDataB64: encryptedData,
|
||||
decryptionHeaderB64: decryptionHeader,
|
||||
};
|
||||
};
|
||||
|
||||
export const _encryptMetadataJSON = ({ jsonValue, keyB64 }: EncryptJSON) =>
|
||||
_encryptAssociatedDataB64({
|
||||
data: new TextEncoder().encode(JSON.stringify(jsonValue)),
|
||||
keyB64,
|
||||
});
|
||||
export const _decryptBox = libsodium.decryptBox;
|
||||
|
||||
const DecryptBoxB64ToBytes = async ({
|
||||
encryptedDataB64,
|
||||
nonceB64,
|
||||
keyB64,
|
||||
}: DecryptBoxB64): Promise<DecryptBoxBytes> => ({
|
||||
encryptedData: await libsodium.fromB64(encryptedDataB64),
|
||||
nonceB64,
|
||||
keyB64,
|
||||
});
|
||||
export const _decryptBoxB64 = libsodium.decryptBoxB64;
|
||||
|
||||
export const _decryptBoxB64 = (r: DecryptBoxB64) =>
|
||||
DecryptBoxB64ToBytes(r).then((rb) => libsodium.decryptBox(rb));
|
||||
export const _decryptBlob = libsodium.decryptBlob;
|
||||
|
||||
export const _decryptAssociatedData = libsodium.decryptBlob;
|
||||
export const _decryptBlobB64 = libsodium.decryptBlobB64;
|
||||
|
||||
export const _decryptThumbnail = _decryptAssociatedData;
|
||||
export const _decryptThumbnail = _decryptBlob;
|
||||
|
||||
export const _decryptAssociatedDataB64 = async ({
|
||||
encryptedDataB64,
|
||||
decryptionHeaderB64,
|
||||
keyB64,
|
||||
}: DecryptBlobB64) =>
|
||||
await _decryptAssociatedData({
|
||||
encryptedData: await libsodium.fromB64(encryptedDataB64),
|
||||
decryptionHeaderB64,
|
||||
keyB64,
|
||||
});
|
||||
|
||||
export const _decryptMetadataJSON = async (r: DecryptBlobB64) =>
|
||||
export const _decryptMetadataJSON_New = async (
|
||||
blob: EncryptedBlob,
|
||||
key: BytesOrB64,
|
||||
) =>
|
||||
JSON.parse(
|
||||
new TextDecoder().decode(await _decryptAssociatedDataB64(r)),
|
||||
new TextDecoder().decode(await _decryptBlob(blob, key)),
|
||||
) as unknown;
|
||||
|
||||
export const _decryptMetadataJSON = async (r: {
|
||||
encryptedDataB64: string;
|
||||
decryptionHeaderB64: string;
|
||||
keyB64: string;
|
||||
}) =>
|
||||
_decryptMetadataJSON_New(
|
||||
{
|
||||
encryptedData: r.encryptedDataB64,
|
||||
decryptionHeader: r.decryptionHeaderB64,
|
||||
},
|
||||
r.keyB64,
|
||||
);
|
||||
|
||||
@@ -51,22 +51,15 @@ import { sharedCryptoWorker } from ".";
|
||||
import { assertionFailed } from "../assert";
|
||||
import { inWorker } from "../env";
|
||||
import * as ei from "./ente-impl";
|
||||
import type {
|
||||
DecryptBlobB64,
|
||||
DecryptBlobBytes,
|
||||
DecryptBoxB64,
|
||||
EncryptB64,
|
||||
EncryptBytes,
|
||||
EncryptJSON,
|
||||
} from "./types";
|
||||
import type { BytesOrB64, EncryptedBlob, EncryptedBox } from "./types";
|
||||
|
||||
/**
|
||||
* Some of these functions have not yet been needed on the main thread, and for
|
||||
* these we don't have a corresponding sharedCryptoWorker method.
|
||||
*
|
||||
* This assertion will let us know when we need to implement them (this'll
|
||||
* This assertion will let us know when we need to implement them. This will
|
||||
* gracefully degrade in production: the functionality will work, just that the
|
||||
* crypto will happen on the main thread itself).
|
||||
* crypto operations will happen on the main thread itself.
|
||||
*/
|
||||
const assertInWorker = <T>(x: T): T => {
|
||||
if (!inWorker()) assertionFailed("Currently only usable in a web worker");
|
||||
@@ -74,123 +67,158 @@ const assertInWorker = <T>(x: T): T => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Encrypt arbitrary data using the given key and a randomly generated nonce.
|
||||
* Encrypt the given data, returning a box containing the encrypted data and a
|
||||
* randomly generated nonce that was used during encryption.
|
||||
*
|
||||
* Both the encrypted data and the nonce are returned as base64 strings.
|
||||
*
|
||||
* Use {@link decryptBoxB64} to decrypt the result.
|
||||
*
|
||||
* ee {@link encryptBox} for the implementation details.
|
||||
*
|
||||
* > The suffix "Box" comes from the fact that it uses the so called secretbox
|
||||
* > APIs provided by libsodium under the hood.
|
||||
* >
|
||||
* > See: [Note: 3 forms of encryption (Box | Blob | Stream)]
|
||||
*/
|
||||
export const encryptBoxB64 = (r: EncryptB64) =>
|
||||
export const encryptBoxB64 = (data: BytesOrB64, key: BytesOrB64) =>
|
||||
inWorker()
|
||||
? ei._encryptBoxB64(r)
|
||||
: sharedCryptoWorker().then((w) => w.encryptBoxB64(r));
|
||||
? ei._encryptBoxB64(data, key)
|
||||
: sharedCryptoWorker().then((w) => w.encryptBoxB64(data, key));
|
||||
|
||||
/**
|
||||
* Encrypt arbitrary data associated with an Ente object (file, collection,
|
||||
* entity) using the object's key.
|
||||
* Encrypt the given data, returning a blob containing the encrypted data and a
|
||||
* decryption header.
|
||||
*
|
||||
* Use {@link decryptAssociatedData} to decrypt the result.
|
||||
* This function is usually used to encrypt data associated with an Ente object
|
||||
* (file, collection, entity) using the object's key.
|
||||
*
|
||||
* See {@link encryptBlob} for the implementation details.
|
||||
* Use {@link decryptBlob} to decrypt the result.
|
||||
*
|
||||
* > The suffix "Blob" comes from our convention of naming functions that use
|
||||
* > the secretstream APIs in one-shot mode.
|
||||
* >
|
||||
* > See: [Note: 3 forms of encryption (Box | Blob | Stream)]
|
||||
*/
|
||||
export const encryptAssociatedData = (r: EncryptBytes) =>
|
||||
assertInWorker(ei._encryptAssociatedData(r));
|
||||
export const encryptBlob = (data: BytesOrB64, key: BytesOrB64) =>
|
||||
assertInWorker(ei._encryptBlob(data, key));
|
||||
|
||||
/**
|
||||
* A variant of {@link encryptBlob} that returns the result components as base64
|
||||
* strings.
|
||||
*/
|
||||
export const encryptBlobB64 = (data: BytesOrB64, key: BytesOrB64) =>
|
||||
assertInWorker(ei._encryptBlobB64(data, key));
|
||||
|
||||
/**
|
||||
* Encrypt the thumbnail for a file.
|
||||
*
|
||||
* This is just an alias for {@link encryptAssociatedData}.
|
||||
* This is midway variant of {@link encryptBlob} and {@link encryptBlobB64} that
|
||||
* returns the decryption header as a base64 string, but leaves the data
|
||||
* unchanged.
|
||||
*
|
||||
* Use {@link decryptThumbnail} to decrypt the result.
|
||||
*/
|
||||
export const encryptThumbnail = (r: EncryptBytes) =>
|
||||
assertInWorker(ei._encryptThumbnail(r));
|
||||
|
||||
/**
|
||||
* A variant of {@link encryptAssociatedData} that returns the encrypted data as
|
||||
* a base64 string instead of returning its bytes.
|
||||
*
|
||||
* Use {@link decryptAssociatedDataB64} to decrypt the result.
|
||||
*/
|
||||
export const encryptAssociatedDataB64 = (r: EncryptBytes) =>
|
||||
assertInWorker(ei._encryptAssociatedDataB64(r));
|
||||
export const encryptThumbnail = (data: BytesOrB64, key: BytesOrB64) =>
|
||||
assertInWorker(ei._encryptThumbnail(data, key));
|
||||
|
||||
/**
|
||||
* Encrypt the JSON metadata associated with an Ente object (file, collection or
|
||||
* entity) using the object's key.
|
||||
*
|
||||
* This is a variant of {@link encryptAssociatedData} tailored for encrypting
|
||||
* any arbitrary metadata associated with an Ente object. For example, it is
|
||||
* used for encrypting the various metadata fields (See: [Note: Metadatum])
|
||||
* associated with a file, using that file's key.
|
||||
* This is a variant of {@link encryptBlobB64} tailored for encrypting any
|
||||
* arbitrary metadata associated with an Ente object. For example, it is used
|
||||
* for encrypting the various metadata fields associated with a file, using that
|
||||
* file's key.
|
||||
*
|
||||
* Instead of raw bytes, it takes as input an arbitrary JSON object which it
|
||||
* encodes into a string, and encrypts that. And instead of returning the raw
|
||||
* encrypted bytes, it returns their base64 string representation.
|
||||
* encodes into a string, and encrypts that.
|
||||
*
|
||||
* Use {@link decryptMetadataJSON} to decrypt the result.
|
||||
*
|
||||
* @param jsonValue The JSON value to encrypt. This can be an arbitrary JSON
|
||||
* value, but since TypeScript currently doesn't have a native JSON type, it is
|
||||
* typed as {@link unknown}.
|
||||
*
|
||||
* @param key The encryption key.
|
||||
*/
|
||||
export const encryptMetadataJSON = async (r: EncryptJSON) =>
|
||||
export const encryptMetadataJSON_New = (jsonValue: unknown, key: BytesOrB64) =>
|
||||
inWorker()
|
||||
? ei._encryptMetadataJSON_New(jsonValue, key)
|
||||
: sharedCryptoWorker().then((w) =>
|
||||
w.encryptMetadataJSON_New(jsonValue, key),
|
||||
);
|
||||
|
||||
/**
|
||||
* Deprecated, use {@link encryptMetadataJSON_New} instead.
|
||||
*/
|
||||
export const encryptMetadataJSON = async (r: {
|
||||
jsonValue: unknown;
|
||||
keyB64: string;
|
||||
}) =>
|
||||
inWorker()
|
||||
? ei._encryptMetadataJSON(r)
|
||||
: sharedCryptoWorker().then((w) => w.encryptMetadataJSON(r));
|
||||
|
||||
/**
|
||||
* Decrypt arbitrary data, provided as a base64 string, using the given key and
|
||||
* the provided nonce.
|
||||
*
|
||||
* This is the sibling of {@link encryptBoxB64}.
|
||||
*
|
||||
* See {@link decryptBox} for the implementation details.
|
||||
* Decrypt a box encrypted using {@link encryptBoxB64}.
|
||||
*/
|
||||
export const decryptBoxB64 = (r: DecryptBoxB64) =>
|
||||
export const decryptBox = (box: EncryptedBox, key: BytesOrB64) =>
|
||||
inWorker()
|
||||
? ei._decryptBoxB64(r)
|
||||
: sharedCryptoWorker().then((w) => w.decryptBoxB64(r));
|
||||
? ei._decryptBox(box, key)
|
||||
: sharedCryptoWorker().then((w) => w.decryptBox(box, key));
|
||||
|
||||
/**
|
||||
* Decrypt arbitrary data associated with an Ente object (file, collection or
|
||||
* entity) using the object's key.
|
||||
*
|
||||
* This is the sibling of {@link encryptAssociatedData}.
|
||||
*
|
||||
* See {@link decryptBlob} for the implementation details.
|
||||
* Variant of {@link decryptBox} that returns the result as a base64 string.
|
||||
*/
|
||||
export const decryptAssociatedData = (r: DecryptBlobBytes) =>
|
||||
assertInWorker(ei._decryptAssociatedData(r));
|
||||
|
||||
/**
|
||||
* Decrypt the thumbnail for a file.
|
||||
*
|
||||
* This is the sibling of {@link encryptThumbnail}.
|
||||
*/
|
||||
export const decryptThumbnail = (r: DecryptBlobBytes) =>
|
||||
assertInWorker(ei._decryptThumbnail(r));
|
||||
|
||||
/**
|
||||
* A variant of {@link decryptAssociatedData} that expects the encrypted data as
|
||||
* a base64 encoded string.
|
||||
*
|
||||
* This is the sibling of {@link encryptAssociatedDataB64}.
|
||||
*/
|
||||
export const decryptAssociatedDataB64 = (r: DecryptBlobB64) =>
|
||||
export const decryptBoxB64 = (box: EncryptedBox, key: BytesOrB64) =>
|
||||
inWorker()
|
||||
? ei._decryptAssociatedDataB64(r)
|
||||
: sharedCryptoWorker().then((w) => w.decryptAssociatedDataB64(r));
|
||||
? ei._decryptBoxB64(box, key)
|
||||
: sharedCryptoWorker().then((w) => w.decryptBoxB64(box, key));
|
||||
|
||||
/**
|
||||
* Decrypt the metadata JSON associated with an Ente object.
|
||||
*
|
||||
* This is the sibling of {@link encryptMetadataJSON}.
|
||||
* Decrypt a blob encrypted using either {@link encryptBlob} or
|
||||
* {@link encryptBlobB64}.
|
||||
*/
|
||||
export const decryptBlob = (blob: EncryptedBlob, key: BytesOrB64) =>
|
||||
assertInWorker(ei._decryptBlob(blob, key));
|
||||
|
||||
/**
|
||||
* A variant of {@link decryptBlob} that returns the result as a base64 string.
|
||||
*/
|
||||
export const decryptBlobB64 = (blob: EncryptedBlob, key: BytesOrB64) =>
|
||||
inWorker()
|
||||
? ei._decryptBlobB64(blob, key)
|
||||
: sharedCryptoWorker().then((w) => w.decryptBlobB64(blob, key));
|
||||
|
||||
/**
|
||||
* Decrypt the thumbnail encrypted using {@link encryptThumbnail}.
|
||||
*/
|
||||
export const decryptThumbnail = (blob: EncryptedBlob, key: BytesOrB64) =>
|
||||
assertInWorker(ei._decryptThumbnail(blob, key));
|
||||
|
||||
/**
|
||||
* Decrypt the metadata JSON encrypted using {@link encryptMetadataJSON}.
|
||||
*
|
||||
* @returns The decrypted JSON value. Since TypeScript does not have a native
|
||||
* JSON type, we need to return it as an `unknown`.
|
||||
*/
|
||||
export const decryptMetadataJSON = (r: DecryptBlobB64) =>
|
||||
export const decryptMetadataJSON_New = (
|
||||
blob: EncryptedBlob,
|
||||
key: BytesOrB64,
|
||||
) =>
|
||||
inWorker()
|
||||
? ei._decryptMetadataJSON_New(blob, key)
|
||||
: sharedCryptoWorker().then((w) =>
|
||||
w.decryptMetadataJSON_New(blob, key),
|
||||
);
|
||||
|
||||
/**
|
||||
* Deprecated, retains the old API.
|
||||
*/
|
||||
export const decryptMetadataJSON = (r: {
|
||||
encryptedDataB64: string;
|
||||
decryptionHeaderB64: string;
|
||||
keyB64: string;
|
||||
}) =>
|
||||
inWorker()
|
||||
? ei._decryptMetadataJSON(r)
|
||||
: sharedCryptoWorker().then((w) => w.decryptMetadataJSON(r));
|
||||
|
||||
@@ -12,11 +12,12 @@ import { mergeUint8Arrays } from "@/utils/array";
|
||||
import { CustomError } from "@ente/shared/error";
|
||||
import sodium, { type StateAddress } from "libsodium-wrappers";
|
||||
import type {
|
||||
DecryptBlobBytes,
|
||||
DecryptBoxBytes,
|
||||
EncryptBytes,
|
||||
BytesOrB64,
|
||||
EncryptedBlob,
|
||||
EncryptedBlobB64,
|
||||
EncryptedBlobBytes,
|
||||
EncryptedBoxBytes,
|
||||
EncryptedBox,
|
||||
EncryptedBoxB64,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
@@ -120,7 +121,24 @@ export async function fromHex(input: string) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt the given data using the provided base64 encoded key.
|
||||
* If the provided {@link bob} ("Bytes or B64 string") is already a
|
||||
* {@link Uint8Array}, return it unchanged, otherwise convert the base64 string
|
||||
* into bytes and return those.
|
||||
*/
|
||||
const bytes = async (bob: BytesOrB64) =>
|
||||
typeof bob == "string" ? fromB64(bob) : bob;
|
||||
|
||||
/**
|
||||
* Encrypt the given data using libsodium's secretbox APIs, using a randomly
|
||||
* generated nonce.
|
||||
*
|
||||
* Use {@link decryptBox} to decrypt the result.
|
||||
*
|
||||
* @param data The data to encrypt.
|
||||
*
|
||||
* @param key The key to use for encryption.
|
||||
*
|
||||
* @returns The encrypted data and the generated nonce, both as base64 strings.
|
||||
*
|
||||
* [Note: 3 forms of encryption (Box | Blob | Stream)]
|
||||
*
|
||||
@@ -205,50 +223,73 @@ export async function fromHex(input: string) {
|
||||
*
|
||||
* 3. Box returns a "nonce", while Blob returns a "header".
|
||||
*/
|
||||
export const encryptBox = async ({
|
||||
data,
|
||||
keyB64,
|
||||
}: EncryptBytes): Promise<EncryptedBoxBytes> => {
|
||||
export const encryptBoxB64 = async (
|
||||
data: BytesOrB64,
|
||||
key: BytesOrB64,
|
||||
): Promise<EncryptedBoxB64> => {
|
||||
await sodium.ready;
|
||||
const nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES);
|
||||
const encryptedData = sodium.crypto_secretbox_easy(
|
||||
data,
|
||||
nonce,
|
||||
await fromB64(keyB64),
|
||||
await bytes(data),
|
||||
await bytes(nonce),
|
||||
await bytes(key),
|
||||
);
|
||||
return { encryptedData, nonceB64: await toB64(nonce) };
|
||||
return {
|
||||
encryptedData: await toB64(encryptedData),
|
||||
nonce: await toB64(nonce),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Encrypt the given data using secretstream APIs in one-shot mode, using the
|
||||
* given base64 encoded key.
|
||||
* Encrypt the given data using libsodium's secretstream APIs in one-shot mode.
|
||||
*
|
||||
* Use {@link decryptBlob} to decrypt the result.
|
||||
*
|
||||
* See: [Note: 3 forms of encryption (Box | Blob | Stream)].
|
||||
* @param data The data to encrypt.
|
||||
*
|
||||
* See: https://doc.libsodium.org/secret-key_cryptography/secretstream
|
||||
* @param key The key to use for encryption.
|
||||
*
|
||||
* @returns The encrypted data and the decryption header as {@link Uint8Array}s.
|
||||
*
|
||||
* - See: [Note: 3 forms of encryption (Box | Blob | Stream)].
|
||||
*
|
||||
* - See: https://doc.libsodium.org/secret-key_cryptography/secretstream
|
||||
*/
|
||||
export const encryptBlob = async ({
|
||||
data,
|
||||
keyB64,
|
||||
}: EncryptBytes): Promise<EncryptedBlobBytes> => {
|
||||
export const encryptBlob = async (
|
||||
data: BytesOrB64,
|
||||
key: BytesOrB64,
|
||||
): Promise<EncryptedBlobBytes> => {
|
||||
await sodium.ready;
|
||||
|
||||
const uintkey: Uint8Array = await fromB64(keyB64);
|
||||
const uintkey = await bytes(key);
|
||||
const initPushResult =
|
||||
sodium.crypto_secretstream_xchacha20poly1305_init_push(uintkey);
|
||||
const [pushState, header] = [initPushResult.state, initPushResult.header];
|
||||
|
||||
const pushResult = sodium.crypto_secretstream_xchacha20poly1305_push(
|
||||
pushState,
|
||||
data,
|
||||
await bytes(data),
|
||||
null,
|
||||
sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL,
|
||||
);
|
||||
return {
|
||||
encryptedData: pushResult,
|
||||
decryptionHeaderB64: await toB64(header),
|
||||
decryptionHeader: header,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* A variant of {@link encryptBlob} that returns the both the encrypted data and
|
||||
* decryption header as base64 strings.
|
||||
*/
|
||||
export const encryptBlobB64 = async (
|
||||
data: BytesOrB64,
|
||||
key: BytesOrB64,
|
||||
): Promise<EncryptedBlobB64> => {
|
||||
const { encryptedData, decryptionHeader } = await encryptBlob(data, key);
|
||||
return {
|
||||
encryptedData: await toB64(encryptedData),
|
||||
decryptionHeader: await toB64(decryptionHeader),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -327,42 +368,56 @@ export async function encryptFileChunk(
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt the result of {@link encryptBox}.
|
||||
* Decrypt the result of {@link encryptBoxB64}.
|
||||
*/
|
||||
export const decryptBox = async ({
|
||||
encryptedData,
|
||||
nonceB64,
|
||||
keyB64,
|
||||
}: DecryptBoxBytes): Promise<Uint8Array> => {
|
||||
export const decryptBox = async (
|
||||
{ encryptedData, nonce }: EncryptedBox,
|
||||
key: BytesOrB64,
|
||||
): Promise<Uint8Array> => {
|
||||
await sodium.ready;
|
||||
return sodium.crypto_secretbox_open_easy(
|
||||
encryptedData,
|
||||
await fromB64(nonceB64),
|
||||
await fromB64(keyB64),
|
||||
await bytes(encryptedData),
|
||||
await bytes(nonce),
|
||||
await bytes(key),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Decrypt the result of {@link encryptBlob}.
|
||||
* Variant of {@link decryptBox} that returns the data as a base64 string.
|
||||
*/
|
||||
export const decryptBlob = async ({
|
||||
encryptedData,
|
||||
decryptionHeaderB64,
|
||||
keyB64,
|
||||
}: DecryptBlobBytes): Promise<Uint8Array> => {
|
||||
export const decryptBoxB64 = (
|
||||
box: EncryptedBox,
|
||||
key: BytesOrB64,
|
||||
): Promise<string> => decryptBox(box, key).then(toB64);
|
||||
|
||||
/**
|
||||
* Decrypt the result of {@link encryptBlob} or {@link encryptBlobB64}.
|
||||
*/
|
||||
export const decryptBlob = async (
|
||||
{ encryptedData, decryptionHeader }: EncryptedBlob,
|
||||
key: BytesOrB64,
|
||||
): Promise<Uint8Array> => {
|
||||
await sodium.ready;
|
||||
const pullState = sodium.crypto_secretstream_xchacha20poly1305_init_pull(
|
||||
await fromB64(decryptionHeaderB64),
|
||||
await fromB64(keyB64),
|
||||
await bytes(decryptionHeader),
|
||||
await bytes(key),
|
||||
);
|
||||
const pullResult = sodium.crypto_secretstream_xchacha20poly1305_pull(
|
||||
pullState,
|
||||
encryptedData,
|
||||
await bytes(encryptedData),
|
||||
null,
|
||||
);
|
||||
return pullResult.message;
|
||||
};
|
||||
|
||||
/**
|
||||
* A variant of {@link decryptBlob} that returns the result as a base64 string.
|
||||
*/
|
||||
export const decryptBlobB64 = (
|
||||
blob: EncryptedBlob,
|
||||
key: BytesOrB64,
|
||||
): Promise<string> => decryptBlob(blob, key).then(toB64);
|
||||
|
||||
/** Decrypt Stream, but merge the results. */
|
||||
export const decryptChaCha = async (
|
||||
data: Uint8Array,
|
||||
@@ -439,14 +494,14 @@ export interface B64EncryptionResult {
|
||||
nonce: string;
|
||||
}
|
||||
|
||||
/** Deprecated, use {@link encryptBoxB64} instead */
|
||||
export async function encryptToB64(data: string, keyB64: string) {
|
||||
await sodium.ready;
|
||||
const encrypted = await encryptBox({ data: await fromB64(data), keyB64 });
|
||||
|
||||
const encrypted = await encryptBoxB64(data, keyB64);
|
||||
return {
|
||||
encryptedData: await toB64(encrypted.encryptedData),
|
||||
encryptedData: encrypted.encryptedData,
|
||||
key: keyB64,
|
||||
nonce: encrypted.nonceB64,
|
||||
nonce: encrypted.nonce,
|
||||
} as B64EncryptionResult;
|
||||
}
|
||||
|
||||
@@ -461,35 +516,23 @@ export async function encryptUTF8(data: string, key: string) {
|
||||
return await encryptToB64(b64Data, key);
|
||||
}
|
||||
|
||||
/** Deprecated */
|
||||
/** Deprecated, use {@link decryptBoxB64} instead. */
|
||||
export async function decryptB64(
|
||||
data: string,
|
||||
nonceB64: string,
|
||||
encryptedData: string,
|
||||
nonce: string,
|
||||
keyB64: string,
|
||||
) {
|
||||
await sodium.ready;
|
||||
const decrypted = await decryptBox({
|
||||
encryptedData: await fromB64(data),
|
||||
nonceB64,
|
||||
keyB64,
|
||||
});
|
||||
|
||||
return await toB64(decrypted);
|
||||
return decryptBoxB64({ encryptedData, nonce }, keyB64);
|
||||
}
|
||||
|
||||
/** Deprecated */
|
||||
export async function decryptToUTF8(
|
||||
data: string,
|
||||
nonceB64: string,
|
||||
encryptedData: string,
|
||||
nonce: string,
|
||||
keyB64: string,
|
||||
) {
|
||||
await sodium.ready;
|
||||
const decrypted = await decryptBox({
|
||||
encryptedData: await fromB64(data),
|
||||
nonceB64,
|
||||
keyB64,
|
||||
});
|
||||
|
||||
const decrypted = await decryptBox({ encryptedData, nonce }, keyB64);
|
||||
return sodium.to_string(decrypted);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,220 +1,107 @@
|
||||
/**
|
||||
* An encryption request with the data to encrypt provided as bytes.
|
||||
* Data provided either as bytes ({@link Uint8Array}) or their base64 string
|
||||
* representation.
|
||||
*/
|
||||
export interface EncryptBytes {
|
||||
/**
|
||||
* A {@link Uint8Array} containing the bytes to encrypt.
|
||||
*/
|
||||
data: Uint8Array;
|
||||
/**
|
||||
* A base64 string containing the encryption key.
|
||||
*/
|
||||
keyB64: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A variant of {@link EncryptBytes} with the data as base64 encoded string.
|
||||
*/
|
||||
export interface EncryptB64 {
|
||||
/**
|
||||
* A base64 string containing the data to encrypt.
|
||||
*/
|
||||
dataB64: string;
|
||||
/**
|
||||
* A base64 string containing the encryption key.
|
||||
*/
|
||||
keyB64: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A variant of {@link EncryptBytes} with the data as a JSON value.
|
||||
*/
|
||||
export interface EncryptJSON {
|
||||
/**
|
||||
* The JSON value to encrypt.
|
||||
*
|
||||
* This can be an arbitrary JSON value, but since TypeScript currently
|
||||
* doesn't have a native JSON type, it is typed as {@link unknown}.
|
||||
*/
|
||||
jsonValue: unknown;
|
||||
/**
|
||||
* A base64 string containing the encryption key.
|
||||
*/
|
||||
keyB64: string;
|
||||
}
|
||||
export type BytesOrB64 = Uint8Array | string;
|
||||
|
||||
/**
|
||||
* The result of encryption using the secretbox APIs.
|
||||
*
|
||||
* It contains the encrypted data (bytes) and nonce (base64 encoded string)
|
||||
* pair. Both these values are needed to decrypt the data. The nonce does not
|
||||
* need to be secret.
|
||||
* It contains an encrypted data and a randomly generated nonce that was used
|
||||
* during encryption. Both these values are needed to decrypt the data. The
|
||||
* nonce does not need to be secret.
|
||||
*
|
||||
* See: [Note: 3 forms of encryption (Box | Blob | Stream)].
|
||||
*/
|
||||
export interface EncryptedBoxBytes {
|
||||
export interface EncryptedBox {
|
||||
/**
|
||||
* A {@link Uint8Array} containing the encrypted data.
|
||||
* The data to decrypt.
|
||||
*/
|
||||
encryptedData: Uint8Array;
|
||||
encryptedData: BytesOrB64;
|
||||
/**
|
||||
* A base64 string containing the nonce used during encryption.
|
||||
* The nonce that was used during encryption.
|
||||
*
|
||||
* A randomly generated nonce for this encryption. It does not need to be
|
||||
* confidential, but it will be required to decrypt the data.
|
||||
* The nonce is required to decrypt the data, but it does not need to be
|
||||
* kept secret.
|
||||
*/
|
||||
nonceB64: string;
|
||||
nonce: BytesOrB64;
|
||||
}
|
||||
|
||||
export interface EncryptedBoxB64 {
|
||||
/**
|
||||
* The encrypted data as a base64 string.
|
||||
*/
|
||||
encryptedData: string;
|
||||
/**
|
||||
* The nonce that was used during encryption, as a base64 string.
|
||||
*
|
||||
* The nonce is required to decrypt the data, but it does not need to be
|
||||
* kept secret.
|
||||
*/
|
||||
nonce: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A variant of {@link EncryptedBoxBytes} with the encrypted data encoded as a
|
||||
* base64 string.
|
||||
*/
|
||||
export interface EncryptedBox64 {
|
||||
/**
|
||||
* A base64 string containing the encrypted data.
|
||||
*/
|
||||
encryptedDataB64: string;
|
||||
/**
|
||||
* A base64 string containing the nonce used during encryption.
|
||||
*
|
||||
* A randomly generated nonce for this encryption. It does not need to be
|
||||
* confidential, but it will be required to decrypt the data.
|
||||
*/
|
||||
nonceB64: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of encryption using the secretstream APIs used in one-shot mode.
|
||||
* The result of encryption using the secretstream APIs in one-shot mode.
|
||||
*
|
||||
* It contains the encrypted data (bytes) and decryption header (base64 encoded
|
||||
* string) pair. Both these values are needed to decrypt the data. The header
|
||||
* does not need to be secret.
|
||||
* It contains an encrypted data and a header that should be provided during
|
||||
* decryption. The header does not need to be secret.
|
||||
*
|
||||
* See: [Note: 3 forms of encryption (Box | Blob | Stream)].
|
||||
*
|
||||
* This type is a combination of {@link EncryptedBlobBytes} and
|
||||
* {@link EncryptedBlobB64} which allows the decryption routines to accept
|
||||
* either the bytes or the base64 variants produced by the encryption routines.
|
||||
*/
|
||||
export interface EncryptedBlob {
|
||||
/**
|
||||
* The encrypted data.
|
||||
*/
|
||||
encryptedData: BytesOrB64;
|
||||
/**
|
||||
* The decryption header.
|
||||
*
|
||||
* While the exact contents of the header are libsodium's internal details,
|
||||
* it effectively contains a random nonce generated by libsodium. It does
|
||||
* not need to be secret, but it is required to decrypt the data.
|
||||
*/
|
||||
decryptionHeader: BytesOrB64;
|
||||
}
|
||||
|
||||
/**
|
||||
* A variant of {@link EncryptedBlob} that has the encrypted data and header
|
||||
* as bytes ({@link Uint8Array}s).
|
||||
*/
|
||||
export interface EncryptedBlobBytes {
|
||||
/**
|
||||
* A {@link Uint8Array} containing the encrypted data.
|
||||
* The encrypted data.
|
||||
*/
|
||||
encryptedData: Uint8Array;
|
||||
/**
|
||||
* A base64 string containing the decryption header.
|
||||
* The decryption header.
|
||||
*
|
||||
* The header contains a random nonce and other libsodium specific metadata.
|
||||
* It does not need to be secret, but it is required to decrypt the data.
|
||||
* While the exact contents of the header are libsodium's internal details,
|
||||
* it effectively contains a random nonce generated by libsodium. It does
|
||||
* not need to be secret, but it is required to decrypt the data.
|
||||
*/
|
||||
decryptionHeaderB64: string;
|
||||
decryptionHeader: Uint8Array;
|
||||
}
|
||||
|
||||
/**
|
||||
* A variant of {@link EncryptedBlobBytes} with the encrypted data encoded as a
|
||||
* base64 string.
|
||||
* A variant of {@link EncryptedBlob} that has the encrypted data and header
|
||||
* as base64 strings.
|
||||
*/
|
||||
export interface EncryptedBlobB64 {
|
||||
/**
|
||||
* A base64 string containing the encrypted data.
|
||||
* The encrypted data as a base64 string.
|
||||
*/
|
||||
encryptedDataB64: string;
|
||||
encryptedData: string;
|
||||
/**
|
||||
* A base64 string containing the decryption header.
|
||||
*
|
||||
* The header contains a random nonce and other libsodium specific metadata.
|
||||
* It does not need to be secret, but it is required to decrypt the data.
|
||||
* While the exact contents of the header are libsodium's internal details,
|
||||
* it effectively contains a random nonce generated by libsodium. It does
|
||||
* not need to be secret, but it is required to decrypt the data.
|
||||
*/
|
||||
decryptionHeaderB64: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A decryption request to decrypt data encrypted using the secretbox APIs. The
|
||||
* encrypted Box's data is provided as bytes.
|
||||
*
|
||||
* See: [Note: 3 forms of encryption (Box | Blob | Stream)].
|
||||
*/
|
||||
export interface DecryptBoxBytes {
|
||||
/**
|
||||
* A {@link Uint8Array} containing the bytes to decrypt.
|
||||
*/
|
||||
encryptedData: Uint8Array;
|
||||
/**
|
||||
* A base64 string containing the nonce that was used during encryption.
|
||||
*
|
||||
* The nonce is required to decrypt the data, but it does not need to be
|
||||
* kept secret.
|
||||
*/
|
||||
nonceB64: string;
|
||||
/**
|
||||
* A base64 string containing the encryption key.
|
||||
*/
|
||||
keyB64: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A variant of {@link DecryptBoxBytes} with the encrypted Blob's data as a
|
||||
* base64 encoded string.
|
||||
*/
|
||||
export interface DecryptBoxB64 {
|
||||
/**
|
||||
* A base64 string containing the data to decrypt.
|
||||
*/
|
||||
encryptedDataB64: string;
|
||||
/**
|
||||
* A base64 string containing the nonce that was used during encryption.
|
||||
*
|
||||
* The nonce is required to decrypt the data, but it does not need to be
|
||||
* kept secret.
|
||||
*/
|
||||
nonceB64: string;
|
||||
/**
|
||||
* A base64 string containing the encryption key.
|
||||
*/
|
||||
keyB64: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A decryption request to decrypt data encrypted using the secretstream APIs in
|
||||
* one-shot mode. The encrypted Blob's data is provided as bytes.
|
||||
*
|
||||
* See: [Note: 3 forms of encryption (Box | Blob | Stream)].
|
||||
*/
|
||||
export interface DecryptBlobBytes {
|
||||
/**
|
||||
* A {@link Uint8Array} containing the bytes to decrypt.
|
||||
*/
|
||||
encryptedData: Uint8Array;
|
||||
/**
|
||||
* A base64 string containing the decryption header that was produced during
|
||||
* encryption.
|
||||
*
|
||||
* The header contains a random nonce and other libsodium metadata. It does
|
||||
* not need to be kept secret.
|
||||
*/
|
||||
decryptionHeaderB64: string;
|
||||
/**
|
||||
* A base64 string containing the encryption key.
|
||||
*/
|
||||
keyB64: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A variant of {@link DecryptBlobBytes} with the encrypted Blob's data as a
|
||||
* base64 encoded string.
|
||||
*/
|
||||
export interface DecryptBlobB64 {
|
||||
/**
|
||||
* A base64 string containing the data to decrypt.
|
||||
*/
|
||||
encryptedDataB64: string;
|
||||
/**
|
||||
* A base64 string containing the decryption header that was produced during
|
||||
* encryption.
|
||||
*
|
||||
* The header contains a random nonce and other libsodium metadata. It does
|
||||
* not need to be kept secret.
|
||||
*/
|
||||
decryptionHeaderB64: string;
|
||||
/**
|
||||
* A base64 string containing the encryption key.
|
||||
*/
|
||||
keyB64: string;
|
||||
decryptionHeader: string;
|
||||
}
|
||||
|
||||
@@ -14,10 +14,13 @@ import * as libsodium from "./libsodium";
|
||||
export class CryptoWorker {
|
||||
encryptBoxB64 = ei._encryptBoxB64;
|
||||
encryptThumbnail = ei._encryptThumbnail;
|
||||
encryptMetadataJSON_New = ei._encryptMetadataJSON_New;
|
||||
encryptMetadataJSON = ei._encryptMetadataJSON;
|
||||
decryptBox = ei._decryptBox;
|
||||
decryptBoxB64 = ei._decryptBoxB64;
|
||||
decryptBlobB64 = ei._decryptBlobB64;
|
||||
decryptThumbnail = ei._decryptThumbnail;
|
||||
decryptAssociatedDataB64 = ei._decryptAssociatedDataB64;
|
||||
decryptMetadataJSON_New = ei._decryptMetadataJSON_New;
|
||||
decryptMetadataJSON = ei._decryptMetadataJSON;
|
||||
|
||||
// TODO: -- AUDIT BELOW --
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { sharedCryptoWorker } from "@/base/crypto";
|
||||
import { z } from "zod";
|
||||
import { decryptBox } from "./crypto/ente";
|
||||
import { toB64 } from "./crypto/libsodium";
|
||||
|
||||
/**
|
||||
* Return the base64 encoded user's encryption key from session storage.
|
||||
* Return the user's master key (as a base64 string) from session storage.
|
||||
*
|
||||
* Precondition: The user should be logged in.
|
||||
*/
|
||||
export const usersEncryptionKeyB64 = async () => {
|
||||
export const masterKeyFromSession = async () => {
|
||||
// TODO: Same value as the deprecated SESSION_KEYS.ENCRYPTION_KEY.
|
||||
const value = sessionStorage.getItem("encryptionKey");
|
||||
if (!value) {
|
||||
throw new Error(
|
||||
"The user's encryption key was not found in session storage. Likely they are not logged in.",
|
||||
"The user's master key was not found in session storage. Likely they are not logged in.",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,10 +20,15 @@ export const usersEncryptionKeyB64 = async () => {
|
||||
JSON.parse(value),
|
||||
);
|
||||
|
||||
const cryptoWorker = await sharedCryptoWorker();
|
||||
return cryptoWorker.decryptB64(encryptedData, nonce, key);
|
||||
return decryptBox({ encryptedData, nonce }, key);
|
||||
};
|
||||
|
||||
/**
|
||||
* Variant of {@link masterKeyFromSession} that returns the master key as a
|
||||
* base64 string.
|
||||
*/
|
||||
export const masterKeyB64FromSession = () => masterKeyFromSession().then(toB64);
|
||||
|
||||
// TODO: Same as B64EncryptionResult. Revisit.
|
||||
const EncryptionKeyAttributes = z.object({
|
||||
encryptedData: z.string(),
|
||||
|
||||
@@ -69,18 +69,22 @@ export interface Electron {
|
||||
logout: () => Promise<void>;
|
||||
|
||||
/**
|
||||
* Return the previously saved encryption key from persistent safe storage.
|
||||
* Return the previously saved user's master key from the persistent safe
|
||||
* storage accessible to the desktop app.
|
||||
*
|
||||
* If no such key is found, return `undefined`.
|
||||
* The key is returned as a base64 encoded string.
|
||||
*
|
||||
* See also: {@link saveEncryptionKey}.
|
||||
* If the key is not found, return `undefined`.
|
||||
*
|
||||
* See also: {@link saveMasterKeyB64}.
|
||||
*/
|
||||
encryptionKey: () => Promise<string | undefined>;
|
||||
masterKeyB64: () => Promise<string | undefined>;
|
||||
|
||||
/**
|
||||
* Save the given {@link encryptionKey} into persistent safe storage.
|
||||
* Save the given {@link masterKeyB64} (encoded as a base64 string) to the
|
||||
* persistent safe storage accessible to the desktop app.
|
||||
*/
|
||||
saveEncryptionKey: (encryptionKey: string) => Promise<void>;
|
||||
saveMasterKeyB64: (masterKeyB64: string) => Promise<void>;
|
||||
|
||||
/**
|
||||
* Set or clear the callback {@link cb} to invoke whenever the app comes
|
||||
|
||||
@@ -123,13 +123,12 @@ class DownloadManagerImpl {
|
||||
private downloadThumb = async (file: EnteFile) => {
|
||||
const { downloadClient, cryptoWorker } = this.ensureInitialized();
|
||||
|
||||
const encrypted = await downloadClient.downloadThumbnail(file);
|
||||
const decrypted = await cryptoWorker.decryptThumbnail({
|
||||
encryptedData: encrypted,
|
||||
decryptionHeaderB64: file.thumbnail.decryptionHeader,
|
||||
keyB64: file.key,
|
||||
});
|
||||
return decrypted;
|
||||
const encryptedData = await downloadClient.downloadThumbnail(file);
|
||||
const decryptionHeader = file.thumbnail.decryptionHeader;
|
||||
return cryptoWorker.decryptThumbnail(
|
||||
{ encryptedData, decryptionHeader },
|
||||
file.key,
|
||||
);
|
||||
};
|
||||
|
||||
async getThumbnail(file: EnteFile, localOnly = false) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { encryptAssociatedDataB64 } from "@/base/crypto/ente";
|
||||
import { encryptBlobB64 } from "@/base/crypto/ente";
|
||||
import { authenticatedRequestHeaders, ensureOk } from "@/base/http";
|
||||
import { apiURL } from "@/base/origins";
|
||||
import type { EnteFile } from "@/new/photos/types/file";
|
||||
@@ -94,8 +94,10 @@ export const putFileData = async (
|
||||
type: FileDataType,
|
||||
data: Uint8Array,
|
||||
) => {
|
||||
const { encryptedDataB64, decryptionHeaderB64 } =
|
||||
await encryptAssociatedDataB64({ data: data, keyB64: enteFile.key });
|
||||
const { encryptedData, decryptionHeader } = await encryptBlobB64(
|
||||
data,
|
||||
enteFile.key,
|
||||
);
|
||||
|
||||
const res = await fetch(await apiURL("/files/data"), {
|
||||
method: "PUT",
|
||||
@@ -103,8 +105,8 @@ export const putFileData = async (
|
||||
body: JSON.stringify({
|
||||
fileID: enteFile.id,
|
||||
type,
|
||||
encryptedData: encryptedDataB64,
|
||||
decryptionHeader: decryptionHeaderB64,
|
||||
encryptedData,
|
||||
decryptionHeader,
|
||||
}),
|
||||
});
|
||||
ensureOk(res);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { decryptAssociatedDataB64 } from "@/base/crypto/ente";
|
||||
import { decryptBlob } from "@/base/crypto/ente";
|
||||
import log from "@/base/log";
|
||||
import type { EnteFile } from "@/new/photos/types/file";
|
||||
import { nullToUndefined } from "@/utils/transform";
|
||||
@@ -172,11 +172,12 @@ export const fetchMLData = async (
|
||||
}
|
||||
|
||||
try {
|
||||
const decryptedBytes = await decryptAssociatedDataB64({
|
||||
encryptedDataB64: remoteFileData.encryptedData,
|
||||
decryptionHeaderB64: remoteFileData.decryptionHeader,
|
||||
keyB64: file.key,
|
||||
});
|
||||
// TODO: This line is included in the photos app which currently
|
||||
// doesn't have strict mode enabled, and where it causes a spurious
|
||||
// error, so we unfortunately need to turn off typechecking.
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment, @typescript-eslint/prefer-ts-expect-error
|
||||
// @ts-ignore
|
||||
const decryptedBytes = await decryptBlob(remoteFileData, file.key);
|
||||
const jsonString = await gunzip(decryptedBytes);
|
||||
result.set(fileID, remoteMLDataFromJSONString(jsonString));
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { sharedCryptoWorker } from "@/base/crypto";
|
||||
import { decryptAssociatedDataB64 } from "@/base/crypto/ente";
|
||||
import { decryptBlob, decryptBoxB64 } from "@/base/crypto/ente";
|
||||
import { authenticatedRequestHeaders, ensureOk, HTTPError } from "@/base/http";
|
||||
import { getKV, getKVN, setKV } from "@/base/kv";
|
||||
import { apiURL } from "@/base/origins";
|
||||
import { usersEncryptionKeyB64 } from "@/base/session-store";
|
||||
import { masterKeyFromSession } from "@/base/session-store";
|
||||
import { ensure } from "@/utils/ensure";
|
||||
import { nullToUndefined } from "@/utils/transform";
|
||||
import { z } from "zod";
|
||||
import { gunzip } from "./gzip";
|
||||
@@ -52,32 +52,35 @@ export type EntityType =
|
||||
const defaultDiffLimit = 500;
|
||||
|
||||
/**
|
||||
* A generic user entity.
|
||||
* An entry in the user entity diff.
|
||||
*
|
||||
* This is an intermediate step, usually what we really want is a version
|
||||
* of this with the {@link data} parsed to the specific type of JSON object
|
||||
* expected to be associated with this entity type.
|
||||
* Each change either contains the latest data associated with a particular user
|
||||
* entity that has been created or updated, or indicates that the corresponding
|
||||
* entity has been deleted.
|
||||
*/
|
||||
interface UserEntity {
|
||||
interface UserEntityChange {
|
||||
/**
|
||||
* A UUID or nanoid for the entity.
|
||||
* A UUID or nanoid of the entity.
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Arbitrary data associated with the entity. The format of this data is
|
||||
* specific to each entity type.
|
||||
* Arbitrary (decrypted) data associated with the entity. The format of this
|
||||
* data is specific to each entity type.
|
||||
*
|
||||
* This will not be present for entities that have been deleted on remote.
|
||||
*/
|
||||
data: Uint8Array | undefined;
|
||||
/**
|
||||
* Epoch microseconds denoting when this entity was created or last updated.
|
||||
* Epoch microseconds denoting when this entity was last changed (created or
|
||||
* updated or deleted).
|
||||
*/
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
/** Zod schema for {@link RemoteUserEntity} */
|
||||
const RemoteUserEntity = z.object({
|
||||
/**
|
||||
* Zod schema for a item in the user entity diff.
|
||||
*/
|
||||
const RemoteUserEntityChange = z.object({
|
||||
id: z.string(),
|
||||
/**
|
||||
* Base64 string containing the encrypted contents of the entity.
|
||||
@@ -95,12 +98,9 @@ const RemoteUserEntity = z.object({
|
||||
updatedAt: z.number(),
|
||||
});
|
||||
|
||||
/** An item in the user entity diff response we get from remote. */
|
||||
type RemoteUserEntity = z.infer<typeof RemoteUserEntity>;
|
||||
|
||||
/**
|
||||
* Fetch the next batch of user entities of the given type that have been
|
||||
* created or updated since the given time.
|
||||
* Fetch the next set of changes (upsert or deletion) to user entities of the
|
||||
* given type since the given time.
|
||||
*
|
||||
* @param type The type of the entities to fetch.
|
||||
*
|
||||
@@ -114,47 +114,28 @@ type RemoteUserEntity = z.infer<typeof RemoteUserEntity>;
|
||||
* [Note: Diff response will have at most one entry for an id]
|
||||
*
|
||||
* Unlike git diffs which track all changes, the diffs we get from remote are
|
||||
* guaranteed to contain only one entry (upsert or delete) for particular Ente
|
||||
* guaranteed to contain only one entry (upsert or delete) for a particular Ente
|
||||
* object. This holds true irrespective of the diff limit.
|
||||
*
|
||||
* For example, in the user entity diff response, it is guaranteed that there
|
||||
* will only be at max one entry for a particular entity id. The entry will have
|
||||
* no data to indicate that the corresponding entity was deleted. Otherwise,
|
||||
* when the data is present, it is taken as the creation of a new entity or the
|
||||
* updation of an existing one.
|
||||
* For example, in a user entity diff, it is guaranteed that there will only be
|
||||
* at max one entry for a particular entity id. The entry will have no data to
|
||||
* indicate that the corresponding entity was deleted. Otherwise, when the data
|
||||
* is present, it is taken as the creation of a new entity or the updation of an
|
||||
* existing one.
|
||||
*
|
||||
* This behaviour comes from how remote stores the underlying, e.g., entities. A
|
||||
* This behaviour comes from how remote stores the underlying, say, entities. A
|
||||
* diff returns just entities whose updation times greater than the provided
|
||||
* since time (limited to the given diff limit). So there will be at most one
|
||||
* row for a particular entity id. And if that entity has been deleted, then the
|
||||
* row will be a tombstone so data will be not be present.
|
||||
* row will be a tombstone, so data be absent.
|
||||
*/
|
||||
export const userEntityDiff = async (
|
||||
const userEntityDiff = async (
|
||||
type: EntityType,
|
||||
sinceTime: number,
|
||||
entityKeyB64: string,
|
||||
): Promise<UserEntity[]> => {
|
||||
const parse = async ({
|
||||
id,
|
||||
encryptedData,
|
||||
header,
|
||||
isDeleted,
|
||||
updatedAt,
|
||||
}: RemoteUserEntity) => ({
|
||||
id,
|
||||
data:
|
||||
encryptedData && header && !isDeleted
|
||||
? await decrypt(encryptedData, header)
|
||||
: undefined,
|
||||
updatedAt,
|
||||
});
|
||||
|
||||
const decrypt = (encryptedDataB64: string, decryptionHeaderB64: string) =>
|
||||
decryptAssociatedDataB64({
|
||||
encryptedDataB64,
|
||||
decryptionHeaderB64,
|
||||
keyB64: entityKeyB64,
|
||||
});
|
||||
): Promise<UserEntityChange[]> => {
|
||||
const decrypt = (encryptedData: string, decryptionHeader: string) =>
|
||||
decryptBlob({ encryptedData, decryptionHeader }, entityKeyB64);
|
||||
|
||||
const params = new URLSearchParams({
|
||||
type,
|
||||
@@ -166,45 +147,50 @@ export const userEntityDiff = async (
|
||||
headers: await authenticatedRequestHeaders(),
|
||||
});
|
||||
ensureOk(res);
|
||||
const entities = z
|
||||
.object({ diff: z.array(RemoteUserEntity) })
|
||||
const diff = z
|
||||
.object({ diff: z.array(RemoteUserEntityChange) })
|
||||
.parse(await res.json()).diff;
|
||||
return Promise.all(entities.map(parse));
|
||||
return Promise.all(
|
||||
diff.map(
|
||||
async ({ id, encryptedData, header, isDeleted, updatedAt }) => ({
|
||||
id,
|
||||
data: !isDeleted
|
||||
? await decrypt(ensure(encryptedData), ensure(header))
|
||||
: undefined,
|
||||
updatedAt,
|
||||
}),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the entity key that can be used to decrypt the encrypted contents of
|
||||
* user entities of the given {@link type}.
|
||||
*
|
||||
* 1. We'll see if we have the (encrypted) entity key present locally. If so,
|
||||
* we'll decrypt it using the user's master key and return it.
|
||||
* 1. See if we have the encrypted entity key present locally. If so, return
|
||||
* the entity key by decrypting it using with the user's master key.
|
||||
*
|
||||
* 2. Otherwise we'll fetch the entity key for that type from remote. If found,
|
||||
* we'll decrypte it using the user's master key and return it, also saving
|
||||
* it locally for future use.
|
||||
* 2. Otherwise fetch the encrypted entity key for that type from remote. If we
|
||||
* get one, obtain the entity key by decrypt the encrypted one using the
|
||||
* user's master key, save it locally for future use, and return it.
|
||||
*
|
||||
* 3. Otherwise we'll create a new one, save it locally and put it to remote.
|
||||
* 3. Otherwise generate a new entity key, encrypt it using the user's master
|
||||
* key, putting the encrypted one to remote and also saving it locally, and
|
||||
* return it.
|
||||
*
|
||||
* See also, [Note: User entity keys].
|
||||
*/
|
||||
const getOrCreateEntityKeyB64 = async (type: EntityType) => {
|
||||
const encryptionKeyB64 = await usersEncryptionKeyB64();
|
||||
const worker = await sharedCryptoWorker();
|
||||
|
||||
const decrypt = async ({ encryptedKey, header }: RemoteUserEntityKey) => {
|
||||
return worker.decryptB64(encryptedKey, header, encryptionKeyB64);
|
||||
};
|
||||
|
||||
// See if we already have it locally.
|
||||
const saved = await savedRemoteUserEntityKey(type);
|
||||
if (saved) return decrypt(saved);
|
||||
if (saved) return decryptEntityKey(saved);
|
||||
|
||||
// See if remote already has it.
|
||||
const existing = await getUserEntityKey(type);
|
||||
if (existing) {
|
||||
// Only save it if we can decrypt it to avoid corrupting our local state
|
||||
// in unforeseen circumstances.
|
||||
const result = decrypt(existing);
|
||||
const result = await decryptEntityKey(existing);
|
||||
await saveRemoteUserEntityKey(type, existing);
|
||||
return result;
|
||||
}
|
||||
@@ -243,15 +229,28 @@ const saveRemoteUserEntityKey = (
|
||||
entityKey: RemoteUserEntityKey,
|
||||
) => setKV(entityKeyKey(type), JSON.stringify(entityKey));
|
||||
|
||||
/**
|
||||
* Decrypt an encrypted entity key using the user's master key.
|
||||
*/
|
||||
const decryptEntityKey = async (remote: RemoteUserEntityKey) =>
|
||||
decryptBoxB64(
|
||||
{
|
||||
encryptedData: remote.encryptedKey,
|
||||
// Remote calls it the header, but it really is the nonce.
|
||||
nonce: remote.header,
|
||||
},
|
||||
await masterKeyFromSession(),
|
||||
);
|
||||
|
||||
/**
|
||||
* Fetch the encryption key for the given user entity {@link type} from remote.
|
||||
*
|
||||
* [Note: User entity keys]
|
||||
*
|
||||
* There is one encryption key (itself encrypted with the user's encryption key)
|
||||
* for each user entity type. If the key doesn't exist on remote, then the
|
||||
* client is expected to create one on the user's behalf. Remote will disallow
|
||||
* attempts to multiple keys for the same user entity type.
|
||||
* There is one encryption key (itself encrypted with the user's master key) for
|
||||
* each user entity type. If the key doesn't exist on remote, then the client is
|
||||
* expected to create one on the user's behalf. Remote will disallow attempts to
|
||||
* multiple keys for the same user entity type.
|
||||
*/
|
||||
const getUserEntityKey = async (
|
||||
type: EntityType,
|
||||
|
||||
@@ -108,7 +108,7 @@ export const saveKeyInSessionStore = async (
|
||||
setKey(keyType, sessionKeyAttributes);
|
||||
const electron = globalThis.electron;
|
||||
if (electron && !fromDesktop && keyType === SESSION_KEYS.ENCRYPTION_KEY) {
|
||||
electron.saveEncryptionKey(key);
|
||||
electron.saveMasterKeyB64(key);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user