Rework
This commit is contained in:
@@ -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 },
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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}.
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user