This commit is contained in:
Manav Rathi
2024-11-20 15:02:34 +05:30
parent fac2b34045
commit 7f80ef1879
6 changed files with 128 additions and 122 deletions

View File

@@ -2,6 +2,7 @@ import {
streamEncryptionChunkSize,
type B64EncryptionResult,
} from "@/base/crypto/libsodium";
import type { BytesOrB64 } from "@/base/crypto/types";
import { type CryptoWorker } from "@/base/crypto/worker";
import { ensureElectron } from "@/base/electron";
import { basename, nameAndExtension } from "@/base/file";
@@ -259,28 +260,20 @@ interface EncryptedFileStream {
chunkCount: number;
}
interface LocalFileAttributes<
T extends string | Uint8Array | EncryptedFileStream,
> {
encryptedData: T;
decryptionHeader: string;
}
interface EncryptedMetadata {
encryptedDataB64: string;
decryptionHeaderB64: string;
}
interface EncryptionResult<
T extends string | Uint8Array | EncryptedFileStream,
> {
file: LocalFileAttributes<T>;
key: string;
}
interface ProcessedFile {
file: LocalFileAttributes<Uint8Array | EncryptedFileStream>;
thumbnail: LocalFileAttributes<Uint8Array>;
file: {
encryptedData: Uint8Array | EncryptedFileStream;
decryptionHeader: string;
};
thumbnail: {
encryptedData: Uint8Array;
decryptionHeader: string;
};
metadata: EncryptedMetadata;
pubMagicMetadata: EncryptedMagicMetadata;
localID: number;
@@ -1354,30 +1347,35 @@ const encryptFile = async (
encryptionKey: string,
worker: CryptoWorker,
): Promise<EncryptedFile> => {
const { key: fileKey, file: encryptedFiledata } = await encryptFiledata(
file.fileStreamOrData,
worker,
);
const fileKey = await worker.generateBlobOrStreamKey();
const { fileStreamOrData, thumbnail, metadata, pubMagicMetadata, localID } =
file;
const encryptedFiledata =
fileStreamOrData instanceof Uint8Array
? await worker.encryptStreamBytes(fileStreamOrData, fileKey)
: await encryptFileStream(fileStreamOrData, fileKey, worker);
const encryptedThumbnail = await worker.encryptThumbnail(
file.thumbnail,
thumbnail,
fileKey,
);
const encryptedMetadata = await worker.encryptMetadataJSON({
jsonValue: file.metadata,
jsonValue: metadata,
keyB64: fileKey,
});
let encryptedPubMagicMetadata: EncryptedMagicMetadata;
if (file.pubMagicMetadata) {
if (pubMagicMetadata) {
const encryptedPubMagicMetadataData = await worker.encryptMetadataJSON({
jsonValue: file.pubMagicMetadata.data,
jsonValue: pubMagicMetadata.data,
keyB64: fileKey,
});
encryptedPubMagicMetadata = {
version: file.pubMagicMetadata.version,
count: file.pubMagicMetadata.count,
version: pubMagicMetadata.version,
count: pubMagicMetadata.count,
data: encryptedPubMagicMetadataData.encryptedDataB64,
header: encryptedPubMagicMetadataData.decryptionHeaderB64,
};
@@ -1391,34 +1389,26 @@ const encryptFile = async (
thumbnail: encryptedThumbnail,
metadata: encryptedMetadata,
pubMagicMetadata: encryptedPubMagicMetadata,
localID: file.localID,
localID: localID,
},
fileKey: encryptedKey,
};
return result;
};
const encryptFiledata = async (
fileStreamOrData: FileStream | Uint8Array,
worker: CryptoWorker,
): Promise<EncryptionResult<Uint8Array | EncryptedFileStream>> =>
fileStreamOrData instanceof Uint8Array
? await worker.encryptFile(fileStreamOrData)
: await encryptFileStream(fileStreamOrData, worker);
const encryptFileStream = async (
fileData: FileStream,
{ stream, chunkCount }: FileStream,
fileKey: BytesOrB64,
worker: CryptoWorker,
) => {
const { stream, chunkCount } = fileData;
const fileStreamReader = stream.getReader();
const { key, decryptionHeader, pushState } =
await worker.initChunkEncryption();
const { decryptionHeader, pushState } =
await worker.initChunkEncryption(fileKey);
const ref = { pullCount: 1 };
const encryptedFileStream = new ReadableStream({
async pull(controller) {
const { value } = await fileStreamReader.read();
const encryptedFileChunk = await worker.encryptFileChunk(
const encryptedFileChunk = await worker.encryptStreamChunk(
value,
pushState,
ref.pullCount === chunkCount,
@@ -1431,11 +1421,8 @@ const encryptFileStream = async (
},
});
return {
key,
file: {
decryptionHeader,
encryptedData: { stream: encryptedFileStream, chunkCount },
},
decryptionHeader,
encryptedData: { stream: encryptedFileStream, chunkCount },
};
};

View File

@@ -2,6 +2,10 @@
import * as libsodium from "./libsodium";
import type { BytesOrB64, EncryptedBlob, EncryptedFile } from "./types";
export const _generateBoxKey = libsodium.generateBoxKey;
export const _generateBlobOrStreamKey = libsodium.generateBlobOrStreamKey;
export const _encryptBoxB64 = libsodium.encryptBoxB64;
export const _encryptBlob = libsodium.encryptBlob;
@@ -19,6 +23,12 @@ export const _encryptThumbnail = async (
};
};
export const _encryptStreamBytes = libsodium.encryptStreamBytes;
export const _initChunkEncryption = libsodium.initChunkEncryption;
export const _encryptStreamChunk = libsodium.encryptStreamChunk;
export const _encryptMetadataJSON_New = (jsonValue: unknown, key: BytesOrB64) =>
_encryptBlobB64(new TextEncoder().encode(JSON.stringify(jsonValue)), key);

View File

@@ -49,7 +49,6 @@ import { ComlinkWorker } from "@/base/worker/comlink-worker";
import { assertionFailed } from "../assert";
import { inWorker } from "../env";
import * as ei from "./ente-impl";
import * as libsodium from "./libsodium";
import type { BytesOrB64, EncryptedBlob, EncryptedBox } from "./types";
import type { CryptoWorker } from "./worker";
@@ -89,16 +88,22 @@ const assertInWorker = <T>(x: T): T => {
};
/**
* Return a new randomly generated 256-bit key suitable for use with the *Box
* encryption functions.
* Return a new randomly generated 256-bit key (as a base64 string) suitable for
* use with the *Box encryption functions.
*/
export const generateNewBoxKey = libsodium.generateNewBoxKey;
export const generateBoxKey = () =>
inWorker()
? ei._generateBoxKey()
: sharedCryptoWorker().then((w) => w.generateBoxKey());
/**
* Return a new randomly generated 256-bit key suitable for use with the *Blob
* or *Stream encryption functions.
* Return a new randomly generated 256-bit key (as a base64 string) suitable for
* use with the *Blob or *Stream encryption functions.
*/
export const generateNewBlobOrStreamKey = libsodium.generateNewBlobOrStreamKey;
export const generateBlobOrStreamKey = () =>
inWorker()
? ei._generateBlobOrStreamKey()
: sharedCryptoWorker().then((w) => w.generateBlobOrStreamKey());
/**
* Encrypt the given data, returning a box containing the encrypted data and a
@@ -144,18 +149,6 @@ export const encryptBlobB64 = (data: BytesOrB64, key: BytesOrB64) =>
? ei._encryptBlobB64(data, key)
: sharedCryptoWorker().then((w) => w._encryptBlobB64(data, key));
/**
* Encrypt the thumbnail for a file.
*
* 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 = (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.
@@ -228,12 +221,6 @@ export const decryptBlobB64 = (blob: EncryptedBlob, key: BytesOrB64) =>
? 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}.
*

View File

@@ -17,6 +17,7 @@ import type {
EncryptedBlobBytes,
EncryptedBox,
EncryptedBoxB64,
EncryptedFile,
} from "./types";
/**
@@ -128,23 +129,25 @@ const bytes = async (bob: BytesOrB64) =>
typeof bob == "string" ? fromB64(bob) : bob;
/**
* Generate a key for use with the *Box encryption functions.
* Generate a new key for use with the *Box encryption functions, and return its
* base64 string representation.
*
* This returns a new randomly generated 256-bit key suitable for being used
* with libsodium's secretbox APIs.
*/
export const generateNewBoxKey = async () => {
export const generateBoxKey = async () => {
await sodium.ready;
return toB64(sodium.crypto_secretbox_keygen());
};
/**
* Generate a key for use with the *Blob or *Stream encryption functions.
* Generate a new key for use with the *Blob or *Stream encryption functions,
* and return its base64 string representation.
*
* This returns a new randomly generated 256-bit key suitable for being used
* with libsodium's secretstream APIs.
*/
export const generateNewBlobOrStreamKey = async () => {
export const generateBlobOrStreamKey = async () => {
await sodium.ready;
return toB64(sodium.crypto_secretstream_xchacha20poly1305_keygen());
};
@@ -284,9 +287,9 @@ export const encryptBlob = async (
): Promise<EncryptedBlobBytes> => {
await sodium.ready;
const uintkey = await bytes(key);
const keyBytes = await bytes(key);
const initPushResult =
sodium.crypto_secretstream_xchacha20poly1305_init_push(uintkey);
sodium.crypto_secretstream_xchacha20poly1305_init_push(keyBytes);
const [pushState, header] = [initPushResult.state, initPushResult.header];
const pushResult = sodium.crypto_secretstream_xchacha20poly1305_push(
@@ -340,21 +343,22 @@ export const streamEncryptionChunkSize = 4 * 1024 * 1024;
*
* @param data The data to encrypt.
*
* @returns The encrypted data, the decryption header as {@link Uint8Array}s,
* and the newly generated key that was used for encryption.
* @returns The encrypted bytes ({@link Uint8Array}) and the decryption header
* (as a base64 string).
*
* - See: [Note: 3 forms of encryption (Box | Blob | Stream)].
*
* - See: https://doc.libsodium.org/secret-key_cryptography/secretstream
*/
export const encryptChaCha = async (data: Uint8Array) => {
export const encryptStreamBytes = async (
data: Uint8Array,
key: BytesOrB64,
): Promise<EncryptedFile> => {
await sodium.ready;
const uintkey: Uint8Array =
sodium.crypto_secretstream_xchacha20poly1305_keygen();
const keyBytes = await bytes(key);
const initPushResult =
sodium.crypto_secretstream_xchacha20poly1305_init_push(uintkey);
sodium.crypto_secretstream_xchacha20poly1305_init_push(keyBytes);
const [pushState, header] = [initPushResult.state, initPushResult.header];
let bytesRead = 0;
let tag = sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
@@ -379,45 +383,74 @@ export const encryptChaCha = async (data: Uint8Array) => {
encryptedChunks.push(pushResult);
}
return {
key: await toB64(uintkey),
file: {
encryptedData: mergeUint8Arrays(encryptedChunks),
decryptionHeader: await toB64(header),
},
encryptedData: mergeUint8Arrays(encryptedChunks),
decryptionHeader: await toB64(header),
};
};
export async function initChunkEncryption() {
/**
* Initialize libsodium's secretstream APIs for encrypting
* {@link streamEncryptionChunkSize} chunks. Subsequently, each chunk can be
* encrypted using {@link encryptStreamChunk}.
*
* Use {@link initChunkDecryption} to initialize the decryption routine, and
* {@link decryptStreamChunk} to decrypt the individual chunks.
*
* See also: {@link encryptStreamBytes} which also does chunked encryption but
* encrypts all the chunks in a single call.
*
* @param key The key to use for encryption.
*
* @returns The decryption header (as a base64 string) which should be preserved
* and used during decryption, and an opaque "push state" that should be passed
* to subsequent calls to {@link encryptStreamChunk} along with the chunks's
* contents.
*/
export const initChunkEncryption = async (key: BytesOrB64) => {
await sodium.ready;
const key = sodium.crypto_secretstream_xchacha20poly1305_keygen();
const initPushResult =
sodium.crypto_secretstream_xchacha20poly1305_init_push(key);
const [pushState, header] = [initPushResult.state, initPushResult.header];
const keyBytes = await bytes(key);
const { state, header } =
sodium.crypto_secretstream_xchacha20poly1305_init_push(keyBytes);
return {
key: await toB64(key),
decryptionHeader: await toB64(header),
pushState,
pushState: state,
};
}
};
export async function encryptFileChunk(
/**
* Encrypt an individual chunk using libsodium's secretstream APIs.
*
* This function is not meant to be standalone, but is instead called in tandem
* with {@link initChunkEncryption} for encrypting data after breaking it into
* chunks.
*
* @param data The chunk's data as bytes ({@link Uint8Array}).
*
* @param pushState The state for this instantiation of chunked encryption. This
* should be treated as opaque libsodium state that should be passed to all
* calls to {@link encryptStreamChunk} that are paired with a particular
* {@link initChunkEncryption}.
*
* @param isFinalChunk `true` if this is the last chunk in the sequence.
*
* @returns The encrypted chunk.
*/
export const encryptStreamChunk = async (
data: Uint8Array,
pushState: sodium.StateAddress,
isFinalChunk: boolean,
) {
) => {
await sodium.ready;
const tag = isFinalChunk
? sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL
: sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE;
const pushResult = sodium.crypto_secretstream_xchacha20poly1305_push(
return sodium.crypto_secretstream_xchacha20poly1305_push(
pushState,
data,
null,
tag,
);
return pushResult;
}
};
/**
* Decrypt the result of {@link encryptBoxB64} and return the decrypted bytes.

View File

@@ -13,9 +13,14 @@ import * as libsodium from "./libsodium";
* Note: Keep these methods logic free. They are meant to be trivial proxies.
*/
export class CryptoWorker {
generateBoxKey = ei._generateBoxKey;
generateBlobOrStreamKey = ei._generateBlobOrStreamKey;
encryptBoxB64 = ei._encryptBoxB64;
encryptThumbnail = ei._encryptThumbnail;
_encryptBlobB64 = ei._encryptBlobB64;
encryptStreamBytes = ei._encryptStreamBytes;
initChunkEncryption = ei._initChunkEncryption;
encryptStreamChunk = ei._encryptStreamChunk;
encryptMetadataJSON_New = ei._encryptMetadataJSON_New;
encryptMetadataJSON = ei._encryptMetadataJSON;
decryptBox = ei._decryptBox;
@@ -32,22 +37,6 @@ export class CryptoWorker {
return libsodium.decryptChaCha(fileData, header, key);
}
async encryptFile(fileData: Uint8Array) {
return libsodium.encryptChaCha(fileData);
}
async encryptFileChunk(
data: Uint8Array,
pushState: StateAddress,
isFinalChunk: boolean,
) {
return libsodium.encryptFileChunk(data, pushState, isFinalChunk);
}
async initChunkEncryption() {
return libsodium.initChunkEncryption();
}
async initChunkDecryption(header: Uint8Array, key: Uint8Array) {
return libsodium.initChunkDecryption(header, key);
}

View File

@@ -2,7 +2,7 @@ import {
decryptBoxB64,
encryptBlobB64,
encryptBoxB64,
generateNewBlobOrStreamKey,
generateBlobOrStreamKey,
} from "@/base/crypto";
import { nullishToEmpty, nullToUndefined } from "@/utils/transform";
import { z } from "zod";
@@ -258,16 +258,16 @@ const getOrCreateEntityKeyB64 = async (
// As a sanity check, genarate the key but immediately encrypt it as if it
// were fetched from remote and then try to decrypt it before doing anything
// with it.
const generated = await generateNewEncryptedEntityKey(masterKey);
const generated = await generateEncryptedEntityKey(masterKey);
const result = decryptEntityKey(generated, masterKey);
await postUserEntityKey(type, generated);
await saveRemoteUserEntityKey(type, generated);
return result;
};
const generateNewEncryptedEntityKey = async (masterKey: Uint8Array) => {
const generateEncryptedEntityKey = async (masterKey: Uint8Array) => {
const { encryptedData, nonce } = await encryptBoxB64(
await generateNewBlobOrStreamKey(),
await generateBlobOrStreamKey(),
masterKey,
);
// Remote calls it the header, but it really is the nonce.