[web] Iterate on the cluster related crypto (#2737)

This commit is contained in:
Manav Rathi
2024-08-17 22:18:30 +05:30
committed by GitHub
18 changed files with 470 additions and 512 deletions

View File

@@ -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", () =>

View File

@@ -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");

View File

@@ -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,

View File

@@ -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(

View File

@@ -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,

View File

@@ -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(

View File

@@ -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,
);

View File

@@ -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));

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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 --

View File

@@ -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(),

View File

@@ -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

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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);
}
};