[web] Better standardize the crypto nomenclature used in code (#2620)
This commit is contained in:
@@ -169,7 +169,7 @@ const beginPasskeyRegistration = async (token: string) => {
|
||||
// binary data.
|
||||
//
|
||||
// Binary data in the returned `PublicKeyCredentialCreationOptions` are
|
||||
// serialized as a "URLEncodedBase64", which is a URL-encoded Base64 string
|
||||
// serialized as a "URLEncodedBase64", which is a URL-encoded base64 string
|
||||
// without any padding. The library is following the WebAuthn recommendation
|
||||
// when it does this:
|
||||
//
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import log from "@/base/log";
|
||||
import { apiURL } from "@/base/origins";
|
||||
import { ensureString } from "@/utils/ensure";
|
||||
import ComlinkCryptoWorker from "@ente/shared/crypto";
|
||||
import { ApiError, CustomError } from "@ente/shared/error";
|
||||
import HTTPService from "@ente/shared/network/HTTPService";
|
||||
@@ -34,7 +35,10 @@ export const getAuthCodes = async (): Promise<Code[]> => {
|
||||
entity.header,
|
||||
authenticatorKey,
|
||||
);
|
||||
return codeFromURIString(entity.id, decryptedCode);
|
||||
return codeFromURIString(
|
||||
entity.id,
|
||||
ensureString(decryptedCode),
|
||||
);
|
||||
} catch (e) {
|
||||
log.error(`Failed to parse codeID ${entity.id}`, e);
|
||||
return undefined;
|
||||
|
||||
@@ -212,6 +212,7 @@ const decryptEnteFile = async (
|
||||
if (magicMetadata?.data) {
|
||||
fileMagicMetadata = {
|
||||
...encryptedFile.magicMetadata,
|
||||
// @ts-expect-error TODO: Need to use zod here.
|
||||
data: await worker.decryptMetadata(
|
||||
magicMetadata.data,
|
||||
magicMetadata.header,
|
||||
@@ -222,6 +223,7 @@ const decryptEnteFile = async (
|
||||
if (pubMagicMetadata?.data) {
|
||||
filePubMagicMetadata = {
|
||||
...pubMagicMetadata,
|
||||
// @ts-expect-error TODO: Need to use zod here.
|
||||
data: await worker.decryptMetadata(
|
||||
pubMagicMetadata.data,
|
||||
pubMagicMetadata.header,
|
||||
@@ -237,9 +239,11 @@ const decryptEnteFile = async (
|
||||
pubMagicMetadata: filePubMagicMetadata,
|
||||
};
|
||||
if (file.pubMagicMetadata?.data.editedTime) {
|
||||
// @ts-expect-error TODO: Need to use zod here.
|
||||
file.metadata.creationTime = file.pubMagicMetadata.data.editedTime;
|
||||
}
|
||||
if (file.pubMagicMetadata?.data.editedName) {
|
||||
// @ts-expect-error TODO: Need to use zod here.
|
||||
file.metadata.title = file.pubMagicMetadata.data.editedName;
|
||||
}
|
||||
// @ts-expect-error TODO: The core types need to be updated to allow the
|
||||
|
||||
@@ -426,7 +426,7 @@ const createCollection = async (
|
||||
let encryptedMagicMetadata: EncryptedMagicMetadata;
|
||||
if (magicMetadataProps) {
|
||||
const magicMetadata = await updateMagicMetadata(magicMetadataProps);
|
||||
const { file: encryptedMagicMetadataProps } =
|
||||
const encryptedMagicMetadataProps =
|
||||
await cryptoWorker.encryptMetadata(
|
||||
magicMetadataProps,
|
||||
collectionKey,
|
||||
@@ -434,8 +434,8 @@ const createCollection = async (
|
||||
|
||||
encryptedMagicMetadata = {
|
||||
...magicMetadata,
|
||||
data: encryptedMagicMetadataProps.encryptedData,
|
||||
header: encryptedMagicMetadataProps.decryptionHeader,
|
||||
data: encryptedMagicMetadataProps.encryptedDataB64,
|
||||
header: encryptedMagicMetadataProps.decryptionHeaderB64,
|
||||
};
|
||||
}
|
||||
const newCollection: EncryptedCollection = {
|
||||
@@ -799,18 +799,19 @@ export const updateCollectionMagicMetadata = async (
|
||||
|
||||
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||
|
||||
const { file: encryptedMagicMetadata } = await cryptoWorker.encryptMetadata(
|
||||
updatedMagicMetadata.data,
|
||||
collection.key,
|
||||
);
|
||||
const { encryptedDataB64, decryptionHeaderB64 } =
|
||||
await cryptoWorker.encryptMetadata(
|
||||
updatedMagicMetadata.data,
|
||||
collection.key,
|
||||
);
|
||||
|
||||
const reqBody: UpdateMagicMetadataRequest = {
|
||||
id: collection.id,
|
||||
magicMetadata: {
|
||||
version: updatedMagicMetadata.version,
|
||||
count: updatedMagicMetadata.count,
|
||||
data: encryptedMagicMetadata.encryptedData,
|
||||
header: encryptedMagicMetadata.decryptionHeader,
|
||||
data: encryptedDataB64,
|
||||
header: decryptionHeaderB64,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -843,18 +844,19 @@ export const updateSharedCollectionMagicMetadata = async (
|
||||
|
||||
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||
|
||||
const { file: encryptedMagicMetadata } = await cryptoWorker.encryptMetadata(
|
||||
updatedMagicMetadata.data,
|
||||
collection.key,
|
||||
);
|
||||
const { encryptedDataB64, decryptionHeaderB64 } =
|
||||
await cryptoWorker.encryptMetadata(
|
||||
updatedMagicMetadata.data,
|
||||
collection.key,
|
||||
);
|
||||
|
||||
const reqBody: UpdateMagicMetadataRequest = {
|
||||
id: collection.id,
|
||||
magicMetadata: {
|
||||
version: updatedMagicMetadata.version,
|
||||
count: updatedMagicMetadata.count,
|
||||
data: encryptedMagicMetadata.encryptedData,
|
||||
header: encryptedMagicMetadata.decryptionHeader,
|
||||
data: encryptedDataB64,
|
||||
header: decryptionHeaderB64,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -887,18 +889,19 @@ export const updatePublicCollectionMagicMetadata = async (
|
||||
|
||||
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||
|
||||
const { file: encryptedMagicMetadata } = await cryptoWorker.encryptMetadata(
|
||||
updatedPublicMagicMetadata.data,
|
||||
collection.key,
|
||||
);
|
||||
const { encryptedDataB64, decryptionHeaderB64 } =
|
||||
await cryptoWorker.encryptMetadata(
|
||||
updatedPublicMagicMetadata.data,
|
||||
collection.key,
|
||||
);
|
||||
|
||||
const reqBody: UpdateMagicMetadataRequest = {
|
||||
id: collection.id,
|
||||
magicMetadata: {
|
||||
version: updatedPublicMagicMetadata.version,
|
||||
count: updatedPublicMagicMetadata.count,
|
||||
data: encryptedMagicMetadata.encryptedData,
|
||||
header: encryptedMagicMetadata.decryptionHeader,
|
||||
data: encryptedDataB64,
|
||||
header: decryptionHeaderB64,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -143,6 +143,7 @@ const syncEntity = async <T>(type: EntityType): Promise<Entity<T>> => {
|
||||
}
|
||||
|
||||
const entityKey = await getEntityKey(type);
|
||||
// @ts-expect-error TODO: Need to use zod here.
|
||||
const newDecryptedEntities: Array<Entity<T>> = await Promise.all(
|
||||
response.diff.map(async (entity: EncryptedEntity) => {
|
||||
if (entity.isDeleted) {
|
||||
|
||||
@@ -191,7 +191,7 @@ export const updateFileMagicMetadata = async (
|
||||
file,
|
||||
updatedMagicMetadata,
|
||||
} of fileWithUpdatedMagicMetadataList) {
|
||||
const { file: encryptedMagicMetadata } =
|
||||
const { encryptedDataB64, decryptionHeaderB64 } =
|
||||
await cryptoWorker.encryptMetadata(
|
||||
updatedMagicMetadata.data,
|
||||
file.key,
|
||||
@@ -201,8 +201,8 @@ export const updateFileMagicMetadata = async (
|
||||
magicMetadata: {
|
||||
version: updatedMagicMetadata.version,
|
||||
count: updatedMagicMetadata.count,
|
||||
data: encryptedMagicMetadata.encryptedData,
|
||||
header: encryptedMagicMetadata.decryptionHeader,
|
||||
data: encryptedDataB64,
|
||||
header: decryptionHeaderB64,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -238,7 +238,7 @@ export const updateFilePublicMagicMetadata = async (
|
||||
file,
|
||||
updatedPublicMagicMetadata: updatePublicMagicMetadata,
|
||||
} of fileWithUpdatedPublicMagicMetadataList) {
|
||||
const { file: encryptedPubMagicMetadata } =
|
||||
const { encryptedDataB64, decryptionHeaderB64 } =
|
||||
await cryptoWorker.encryptMetadata(
|
||||
updatePublicMagicMetadata.data,
|
||||
file.key,
|
||||
@@ -248,8 +248,8 @@ export const updateFilePublicMagicMetadata = async (
|
||||
magicMetadata: {
|
||||
version: updatePublicMagicMetadata.version,
|
||||
count: updatePublicMagicMetadata.count,
|
||||
data: encryptedPubMagicMetadata.encryptedData,
|
||||
header: encryptedPubMagicMetadata.decryptionHeader,
|
||||
data: encryptedDataB64,
|
||||
header: decryptionHeaderB64,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -245,6 +245,11 @@ interface LocalFileAttributes<
|
||||
decryptionHeader: string;
|
||||
}
|
||||
|
||||
interface EncryptedMetadata {
|
||||
encryptedDataB64: string;
|
||||
decryptionHeaderB64: string;
|
||||
}
|
||||
|
||||
interface EncryptionResult<
|
||||
T extends string | Uint8Array | EncryptedFileStream,
|
||||
> {
|
||||
@@ -255,7 +260,7 @@ interface EncryptionResult<
|
||||
interface ProcessedFile {
|
||||
file: LocalFileAttributes<Uint8Array | EncryptedFileStream>;
|
||||
thumbnail: LocalFileAttributes<Uint8Array>;
|
||||
metadata: LocalFileAttributes<string>;
|
||||
metadata: EncryptedMetadata;
|
||||
pubMagicMetadata: EncryptedMagicMetadata;
|
||||
localID: number;
|
||||
}
|
||||
@@ -1119,25 +1124,31 @@ const encryptFile = async (
|
||||
worker,
|
||||
);
|
||||
|
||||
const { file: encryptedThumbnail } = await worker.encryptThumbnail(
|
||||
file.thumbnail,
|
||||
fileKey,
|
||||
);
|
||||
const {
|
||||
encryptedData: thumbEncryptedData,
|
||||
decryptionHeaderB64: thumbDecryptionHeader,
|
||||
} = await worker.encryptThumbnail(file.thumbnail, fileKey);
|
||||
const encryptedThumbnail = {
|
||||
encryptedData: thumbEncryptedData,
|
||||
decryptionHeader: thumbDecryptionHeader,
|
||||
};
|
||||
|
||||
const { file: encryptedMetadata } = await worker.encryptMetadata(
|
||||
const encryptedMetadata = await worker.encryptMetadata(
|
||||
file.metadata,
|
||||
fileKey,
|
||||
);
|
||||
|
||||
let encryptedPubMagicMetadata: EncryptedMagicMetadata;
|
||||
if (file.pubMagicMetadata) {
|
||||
const { file: encryptedPubMagicMetadataData } =
|
||||
await worker.encryptMetadata(file.pubMagicMetadata.data, fileKey);
|
||||
const encryptedPubMagicMetadataData = await worker.encryptMetadata(
|
||||
file.pubMagicMetadata.data,
|
||||
fileKey,
|
||||
);
|
||||
encryptedPubMagicMetadata = {
|
||||
version: file.pubMagicMetadata.version,
|
||||
count: file.pubMagicMetadata.count,
|
||||
data: encryptedPubMagicMetadataData.encryptedData,
|
||||
header: encryptedPubMagicMetadataData.decryptionHeader,
|
||||
data: encryptedPubMagicMetadataData.encryptedDataB64,
|
||||
header: encryptedPubMagicMetadataData.decryptionHeaderB64,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1267,7 +1278,10 @@ const uploadToBucket = async (
|
||||
decryptionHeader: file.thumbnail.decryptionHeader,
|
||||
objectKey: thumbnailObjectKey,
|
||||
},
|
||||
metadata: file.metadata,
|
||||
metadata: {
|
||||
encryptedData: file.metadata.encryptedDataB64,
|
||||
decryptionHeader: file.metadata.decryptionHeaderB64,
|
||||
},
|
||||
pubMagicMetadata: file.pubMagicMetadata,
|
||||
};
|
||||
return backupedFile;
|
||||
|
||||
@@ -177,6 +177,7 @@ export async function decryptFile(
|
||||
return {
|
||||
...restFileProps,
|
||||
key: fileKey,
|
||||
// @ts-expect-error TODO: Need to use zod here.
|
||||
metadata: fileMetadata,
|
||||
magicMetadata: fileMagicMetadata,
|
||||
pubMagicMetadata: filePubMagicMetadata,
|
||||
|
||||
@@ -56,6 +56,7 @@ export async function updateMagicMetadata<T>(
|
||||
}
|
||||
|
||||
if (typeof originalMagicMetadata?.data === "string") {
|
||||
// @ts-expect-error TODO: Need to use zod here.
|
||||
originalMagicMetadata.data = await cryptoWorker.decryptMetadata(
|
||||
originalMagicMetadata.data,
|
||||
originalMagicMetadata.header,
|
||||
|
||||
@@ -1,56 +1,197 @@
|
||||
/**
|
||||
* @file Higher level functions that use the ontology of Ente's types
|
||||
*
|
||||
* These are thin wrappers over the (thin-) wrappers in internal/libsodium.ts.
|
||||
* The main difference is that these functions don't talk in terms of the crypto
|
||||
* algorithms, but rather in terms the higher-level Ente specific goal we are
|
||||
* trying to accomplish.
|
||||
* [Note: Crypto code hierarchy]
|
||||
*
|
||||
* The functions in this file (base/crypto/ente.ts) are are thin wrappers over
|
||||
* the (thin-) wrappers in internal/libsodium.ts. The main difference is that
|
||||
* these functions don't talk in terms of the crypto algorithms, but rather in
|
||||
* terms the higher-level Ente specific goal we are trying to accomplish.
|
||||
*
|
||||
* Some of these are also exposed via the web worker in
|
||||
* internal/crypto.worker.ts. The web worker variants should be used when we
|
||||
* need to perform these operations from the main thread, so that the UI remains
|
||||
* responsive while the potentially CPU-intensive encryption etc happens.
|
||||
*
|
||||
* 1. ente.ts or crypto.worker.ts (high level, Ente specific).
|
||||
* 2. internal/libsodium.ts (wrappers over libsodium)
|
||||
* 3. libsodium (JS bindings).
|
||||
*/
|
||||
import * as libsodium from "@ente/shared/crypto/internal/libsodium";
|
||||
|
||||
/**
|
||||
* Encrypt arbitrary metadata associated with a file using the file's key.
|
||||
* Encrypt arbitrary data associated with an Ente object (file, collection,
|
||||
* entity) using the object's key.
|
||||
*
|
||||
* @param metadata The metadata (bytes) to encrypt.
|
||||
* Use {@link decryptAssociatedData} to decrypt the result.
|
||||
*
|
||||
* @param keyB64 Base64 encoded string containing the encryption key (this'll
|
||||
* generally be the file's key).
|
||||
* See {@link encryptChaChaOneShot} for the implementation details.
|
||||
*
|
||||
* @returns Base64 encoded strings containing the encrypted data and the
|
||||
* decryption header.
|
||||
* @param data A {@link Uint8Array} containing the bytes to encrypt.
|
||||
*
|
||||
* @param keyB64 base64 string containing the encryption key. This is expected
|
||||
* to the key of the object with which {@link data} is associated. For example,
|
||||
* if this is data associated with a file, then this will be the file's key.
|
||||
*
|
||||
* @returns The encrypted data and the (base64 encoded) decryption header.
|
||||
*/
|
||||
export const encryptFileMetadata = async (
|
||||
metadata: Uint8Array,
|
||||
export const encryptAssociatedData = libsodium.encryptChaChaOneShot;
|
||||
|
||||
/**
|
||||
* Encrypt the thumbnail for a file.
|
||||
*
|
||||
* This is just an alias for {@link encryptAssociatedData}.
|
||||
*
|
||||
* @param data The thumbnail's data.
|
||||
*
|
||||
* @param keyB64 The key associated with the file whose thumbnail this is.
|
||||
*
|
||||
* @returns The encrypted thumbnail, and the associated decryption header
|
||||
* (base64 encoded).
|
||||
*/
|
||||
export const encryptThumbnail = encryptAssociatedData;
|
||||
|
||||
/**
|
||||
* Encrypted the embedding associated with a file using the file's key.
|
||||
*
|
||||
* This as a variant of {@link encryptAssociatedData} tailored for
|
||||
* encrypting the embeddings (a.k.a. derived data) associated with a file. In
|
||||
* particular, it returns the encrypted data in the result as a base64 string
|
||||
* instead of its bytes.
|
||||
*
|
||||
* Use {@link decryptFileEmbedding} to decrypt the result.
|
||||
*/
|
||||
export const encryptFileEmbedding = async (
|
||||
data: Uint8Array,
|
||||
keyB64: string,
|
||||
) => {
|
||||
const { file } = await libsodium.encryptChaChaOneShot(metadata, keyB64);
|
||||
const { encryptedData, decryptionHeaderB64 } = await encryptAssociatedData(
|
||||
data,
|
||||
keyB64,
|
||||
);
|
||||
return {
|
||||
encryptedMetadataB64: await libsodium.toB64(file.encryptedData),
|
||||
decryptionHeaderB64: file.decryptionHeader,
|
||||
encryptedDataB64: await libsodium.toB64(encryptedData),
|
||||
decryptionHeaderB64,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Decrypt arbitrary metadata associated with a file using the file's key.
|
||||
* Encrypt the metadata associated with an Ente object (file, collection or
|
||||
* entity) using the object's key.
|
||||
*
|
||||
* @param encryptedMetadataB64 Base64 encoded string containing the encrypted
|
||||
* data.
|
||||
* 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.
|
||||
*
|
||||
* @param headerB64 Base64 encoded string containing the decryption header
|
||||
* 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.
|
||||
*
|
||||
* Use {@link decryptMetadata} to decrypt the result.
|
||||
*
|
||||
* @param metadata The JSON value to encrypt. It can be an arbitrary JSON value,
|
||||
* but since TypeScript currently doesn't have a native JSON type, it is typed
|
||||
* as an unknown.
|
||||
*
|
||||
* @returns The encrypted data and decryption header, both as base64 strings.
|
||||
*/
|
||||
export const encryptMetadata = async (metadata: unknown, keyB64: string) => {
|
||||
const encodedMetadata = new TextEncoder().encode(JSON.stringify(metadata));
|
||||
|
||||
const { encryptedData, decryptionHeaderB64 } = await encryptAssociatedData(
|
||||
encodedMetadata,
|
||||
keyB64,
|
||||
);
|
||||
return {
|
||||
encryptedDataB64: await libsodium.toB64(encryptedData),
|
||||
decryptionHeaderB64,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 decryptChaChaOneShot2} for the implementation details.
|
||||
*
|
||||
* @param encryptedData A {@link Uint8Array} containing the bytes to decrypt.
|
||||
*
|
||||
* @param headerB64 A base64 string containing the decryption header that was
|
||||
* produced during encryption.
|
||||
*
|
||||
* @param keyB64 Base64 encoded string containing the encryption key (this'll
|
||||
* generally be the file's key).
|
||||
* @param keyB64 A base64 string containing the encryption key. This is expected
|
||||
* to be the key of the object to which {@link encryptedDataB64} is associated.
|
||||
*
|
||||
* @returns The decrypted metadata bytes.
|
||||
* @returns The decrypted bytes.
|
||||
*/
|
||||
export const decryptFileMetadata = async (
|
||||
encryptedMetadataB64: string,
|
||||
export const decryptAssociatedData = libsodium.decryptChaChaOneShot;
|
||||
|
||||
/**
|
||||
* Decrypt the thumbnail for a file.
|
||||
*
|
||||
* This is just an alias for {@link decryptAssociatedData}.
|
||||
*/
|
||||
export const decryptThumbnail = decryptAssociatedData;
|
||||
|
||||
/**
|
||||
* Decrypt the embedding associated with a file using the file's key.
|
||||
*
|
||||
* This is the sibling of {@link encryptFileEmbedding}.
|
||||
*
|
||||
* @param encryptedDataB64 A base64 string containing the encrypted embedding.
|
||||
*
|
||||
* @param headerB64 A base64 string containing the decryption header produced
|
||||
* during encryption.
|
||||
*
|
||||
* @param keyB64 A base64 string containing the encryption key. This is expected
|
||||
* to be the key of the file with which {@link encryptedDataB64} is associated.
|
||||
*
|
||||
* @returns The decrypted metadata JSON object.
|
||||
*/
|
||||
export const decryptFileEmbedding = async (
|
||||
encryptedDataB64: string,
|
||||
decryptionHeaderB64: string,
|
||||
keyB64: string,
|
||||
) =>
|
||||
libsodium.decryptChaChaOneShot(
|
||||
await libsodium.fromB64(encryptedMetadataB64),
|
||||
await libsodium.fromB64(decryptionHeaderB64),
|
||||
decryptAssociatedData(
|
||||
await libsodium.fromB64(encryptedDataB64),
|
||||
decryptionHeaderB64,
|
||||
keyB64,
|
||||
);
|
||||
|
||||
/**
|
||||
* Decrypt the metadata associated with an Ente object (file, collection or
|
||||
* entity) using the object's key.
|
||||
*
|
||||
* This is the sibling of {@link decryptMetadata}.
|
||||
*
|
||||
* @param encryptedDataB64 base64 encoded string containing the encrypted data.
|
||||
*
|
||||
* @param headerB64 base64 encoded string containing the decryption header
|
||||
* produced during encryption.
|
||||
*
|
||||
* @param keyB64 base64 encoded string containing the encryption key. This is
|
||||
* expected to be the key of the object with which {@link encryptedDataB64} is
|
||||
* associated.
|
||||
*
|
||||
* @returns The decrypted JSON value. Since TypeScript does not have a native
|
||||
* JSON type, we need to return it as an `unknown`.
|
||||
*/
|
||||
|
||||
export const decryptMetadata = async (
|
||||
encryptedDataB64: string,
|
||||
decryptionHeaderB64: string,
|
||||
keyB64: string,
|
||||
) =>
|
||||
JSON.parse(
|
||||
new TextDecoder().decode(
|
||||
await decryptAssociatedData(
|
||||
await libsodium.fromB64(encryptedDataB64),
|
||||
decryptionHeaderB64,
|
||||
keyB64,
|
||||
),
|
||||
),
|
||||
) as unknown;
|
||||
|
||||
@@ -127,7 +127,7 @@ class DownloadManagerImpl {
|
||||
const encrypted = await downloadClient.downloadThumbnail(file);
|
||||
const decrypted = await cryptoWorker.decryptThumbnail(
|
||||
encrypted,
|
||||
await cryptoWorker.fromB64(file.thumbnail.decryptionHeader),
|
||||
file.thumbnail.decryptionHeader,
|
||||
file.key,
|
||||
);
|
||||
return decrypted;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { decryptFileMetadata, encryptFileMetadata } from "@/base/crypto/ente";
|
||||
import { decryptFileEmbedding, encryptFileEmbedding } from "@/base/crypto/ente";
|
||||
import { authenticatedRequestHeaders, ensureOk } from "@/base/http";
|
||||
import log from "@/base/log";
|
||||
import { apiURL } from "@/base/origins";
|
||||
@@ -195,7 +195,7 @@ export const fetchDerivedData = async (
|
||||
}
|
||||
|
||||
try {
|
||||
const decryptedBytes = await decryptFileMetadata(
|
||||
const decryptedBytes = await decryptFileEmbedding(
|
||||
remoteEmbedding.encryptedEmbedding,
|
||||
remoteEmbedding.decryptionHeader,
|
||||
file.key,
|
||||
@@ -293,15 +293,15 @@ const putEmbedding = async (
|
||||
model: EmbeddingModel,
|
||||
embedding: Uint8Array,
|
||||
) => {
|
||||
const { encryptedMetadataB64, decryptionHeaderB64 } =
|
||||
await encryptFileMetadata(embedding, enteFile.key);
|
||||
const { encryptedDataB64, decryptionHeaderB64 } =
|
||||
await encryptFileEmbedding(embedding, enteFile.key);
|
||||
|
||||
const res = await fetch(await apiURL("/embeddings"), {
|
||||
method: "PUT",
|
||||
headers: await authenticatedRequestHeaders(),
|
||||
body: JSON.stringify({
|
||||
fileID: enteFile.id,
|
||||
encryptedEmbedding: encryptedMetadataB64,
|
||||
encryptedEmbedding: encryptedDataB64,
|
||||
decryptionHeader: decryptionHeaderB64,
|
||||
model,
|
||||
}),
|
||||
|
||||
@@ -1,44 +1,38 @@
|
||||
import * as ente from "@/base/crypto/ente";
|
||||
import * as libsodium from "@ente/shared/crypto/internal/libsodium";
|
||||
import * as Comlink from "comlink";
|
||||
import type { StateAddress } from "libsodium-wrappers";
|
||||
|
||||
const textDecoder = new TextDecoder();
|
||||
const textEncoder = new TextEncoder();
|
||||
|
||||
/**
|
||||
* A web worker that exposes some of the functions defined in either the Ente
|
||||
* specific layer (base/crypto/ente.ts) or the internal libsodium layer
|
||||
* (internal/libsodium.ts).
|
||||
*
|
||||
* Running these in a web worker allows us to use potentially CPU-intensive
|
||||
* crypto operations from the main thread without stalling the UI.
|
||||
*
|
||||
* See: [Note: Crypto code hierarchy].
|
||||
*
|
||||
* Note: Keep these methods logic free. They should just act as trivial proxies.
|
||||
*/
|
||||
export class DedicatedCryptoWorker {
|
||||
async decryptMetadata(
|
||||
encryptedMetadata: string,
|
||||
header: string,
|
||||
key: string,
|
||||
) {
|
||||
const encodedMetadata = await libsodium.decryptChaChaOneShot(
|
||||
await libsodium.fromB64(encryptedMetadata),
|
||||
await libsodium.fromB64(header),
|
||||
key,
|
||||
);
|
||||
return JSON.parse(textDecoder.decode(encodedMetadata));
|
||||
}
|
||||
|
||||
async decryptThumbnail(
|
||||
fileData: Uint8Array,
|
||||
header: Uint8Array,
|
||||
key: string,
|
||||
encryptedData: Uint8Array,
|
||||
headerB64: string,
|
||||
keyB64: string,
|
||||
) {
|
||||
return libsodium.decryptChaChaOneShot(fileData, header, key);
|
||||
return ente.decryptThumbnail(encryptedData, headerB64, keyB64);
|
||||
}
|
||||
|
||||
async decryptEmbedding(
|
||||
encryptedEmbedding: string,
|
||||
header: string,
|
||||
key: string,
|
||||
async decryptMetadata(
|
||||
encryptedDataB64: string,
|
||||
decryptionHeaderB64: string,
|
||||
keyB64: string,
|
||||
) {
|
||||
const encodedEmbedding = await libsodium.decryptChaChaOneShot(
|
||||
await libsodium.fromB64(encryptedEmbedding),
|
||||
await libsodium.fromB64(header),
|
||||
key,
|
||||
);
|
||||
return Float32Array.from(
|
||||
JSON.parse(textDecoder.decode(encodedEmbedding)),
|
||||
return ente.decryptMetadata(
|
||||
encryptedDataB64,
|
||||
decryptionHeaderB64,
|
||||
keyB64,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -46,41 +40,12 @@ export class DedicatedCryptoWorker {
|
||||
return libsodium.decryptChaCha(fileData, header, key);
|
||||
}
|
||||
|
||||
async encryptMetadata(metadata: Object, key: string) {
|
||||
const encodedMetadata = textEncoder.encode(JSON.stringify(metadata));
|
||||
|
||||
const { file: encryptedMetadata } =
|
||||
await libsodium.encryptChaChaOneShot(encodedMetadata, key);
|
||||
const { encryptedData, ...other } = encryptedMetadata;
|
||||
return {
|
||||
file: {
|
||||
encryptedData: await libsodium.toB64(encryptedData),
|
||||
...other,
|
||||
},
|
||||
key,
|
||||
};
|
||||
async encryptThumbnail(data: Uint8Array, keyB64: string) {
|
||||
return ente.encryptThumbnail(data, keyB64);
|
||||
}
|
||||
|
||||
async encryptThumbnail(fileData: Uint8Array, key: string) {
|
||||
return libsodium.encryptChaChaOneShot(fileData, key);
|
||||
}
|
||||
|
||||
async encryptEmbedding(embedding: Float32Array, key: string) {
|
||||
const encodedEmbedding = textEncoder.encode(
|
||||
JSON.stringify(Array.from(embedding)),
|
||||
);
|
||||
const { file: encryptEmbedding } = await libsodium.encryptChaChaOneShot(
|
||||
encodedEmbedding,
|
||||
key,
|
||||
);
|
||||
const { encryptedData, ...other } = encryptEmbedding;
|
||||
return {
|
||||
file: {
|
||||
encryptedData: await libsodium.toB64(encryptedData),
|
||||
...other,
|
||||
},
|
||||
key,
|
||||
};
|
||||
async encryptMetadata(metadata: unknown, keyB64: string) {
|
||||
return ente.encryptMetadata(metadata, keyB64);
|
||||
}
|
||||
|
||||
async encryptFile(fileData: Uint8Array) {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { CustomError } from "@ente/shared/error";
|
||||
import sodium, { type StateAddress } from "libsodium-wrappers";
|
||||
|
||||
/**
|
||||
* Convert a {@link Uint8Array} to a Base64 encoded string.
|
||||
* Convert bytes ({@link Uint8Array}) to a base64 string.
|
||||
*
|
||||
* See also {@link toB64URLSafe} and {@link toB64URLSafeNoPadding}.
|
||||
*/
|
||||
@@ -21,7 +21,7 @@ export const toB64 = async (input: Uint8Array) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a Base64 encoded string to a {@link Uint8Array}.
|
||||
* Convert a base64 string to bytes ({@link Uint8Array}).
|
||||
*
|
||||
* This is the converse of {@link toBase64}.
|
||||
*/
|
||||
@@ -31,7 +31,7 @@ export const fromB64 = async (input: string) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a {@link Uint8Array} to a URL-safe Base64 encoded string.
|
||||
* Convert bytes ({@link Uint8Array}) to a URL-safe base64 string.
|
||||
*
|
||||
* See also {@link toB64URLSafe} and {@link toB64URLSafeNoPadding}.
|
||||
*/
|
||||
@@ -41,7 +41,7 @@ export const toB64URLSafe = async (input: Uint8Array) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a {@link Uint8Array} to a unpadded URL-safe Base64 encoded string.
|
||||
* Convert bytes ({@link Uint8Array}) to a unpadded URL-safe base64 string.
|
||||
*
|
||||
* This differs from {@link toB64URLSafe} in that it does not append any
|
||||
* trailing padding character(s) "=" to make the resultant string's length be an
|
||||
@@ -62,7 +62,7 @@ export const toB64URLSafeNoPadding = async (input: Uint8Array) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a unpadded URL-safe Base64 encoded string to a {@link Uint8Array}.
|
||||
* Convert a unpadded URL-safe base64 string to bytes ({@link Uint8Array}).
|
||||
*
|
||||
* This is the converse of {@link toB64URLSafeNoPadding}, and does not expect
|
||||
* its input string's length to be a an integer multiple of 4.
|
||||
@@ -110,10 +110,56 @@ export async function fromHex(input: string) {
|
||||
return await toB64(sodium.from_hex(input));
|
||||
}
|
||||
|
||||
export async function encryptChaChaOneShot(data: Uint8Array, key: string) {
|
||||
/**
|
||||
* Encrypt the given {@link data} using the given (base64 encoded) key.
|
||||
*
|
||||
* Use {@link decryptChaChaOneShot} to decrypt the result.
|
||||
*
|
||||
* [Note: Salsa and ChaCha]
|
||||
*
|
||||
* This uses the same stream encryption algorithm (XChaCha20 stream cipher with
|
||||
* Poly1305 MAC authentication) that we use for encrypting other streams, in
|
||||
* particular the actual file's contents.
|
||||
*
|
||||
* The difference here is that this function does a one shot instead of a
|
||||
* streaming encryption. This is only meant to be used for relatively small
|
||||
* amounts of data (few MBs).
|
||||
*
|
||||
* See: https://doc.libsodium.org/secret-key_cryptography/secretstream
|
||||
*
|
||||
* Libsodium also provides the `crypto_secretbox_easy` APIs for one shot
|
||||
* encryption, which we do use in other places where we need to one shot
|
||||
* encryption of independent bits of data.
|
||||
*
|
||||
* These secretbox APIs use XSalsa20 with Poly1305. XSalsa20 is a minor variant
|
||||
* (predecessor in fact) of XChaCha20.
|
||||
*
|
||||
* See: https://doc.libsodium.org/secret-key_cryptography/secretbox
|
||||
*
|
||||
* The difference here is that this function is meant to used for data
|
||||
* associated with a file (or some other Ente object, like a collection or an
|
||||
* entity). There is no technical reason to do it that way, just this way all
|
||||
* data associated with a file, including its actual contents, use the same
|
||||
* underlying (streaming) libsodium APIs. In other cases, where we have free
|
||||
* standing independent data, we continue using the secretbox APIs for one shot
|
||||
* encryption and decryption.
|
||||
*
|
||||
* @param data A {@link Uint8Array} containing the bytes that we want to
|
||||
* encrypt.
|
||||
*
|
||||
* @param keyB64 A base64 string containing the encryption key.
|
||||
*
|
||||
* @returns The encrypted data (bytes) and decryption header pair (base64
|
||||
* encoded string). Both these values are needed to decrypt the data. The header
|
||||
* does not need to be secret.
|
||||
*/
|
||||
export const encryptChaChaOneShot = async (
|
||||
data: Uint8Array,
|
||||
keyB64: string,
|
||||
) => {
|
||||
await sodium.ready;
|
||||
|
||||
const uintkey: Uint8Array = await fromB64(key);
|
||||
const uintkey: Uint8Array = await fromB64(keyB64);
|
||||
const initPushResult =
|
||||
sodium.crypto_secretstream_xchacha20poly1305_init_push(uintkey);
|
||||
const [pushState, header] = [initPushResult.state, initPushResult.header];
|
||||
@@ -125,13 +171,10 @@ export async function encryptChaChaOneShot(data: Uint8Array, key: string) {
|
||||
sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL,
|
||||
);
|
||||
return {
|
||||
key: await toB64(uintkey),
|
||||
file: {
|
||||
encryptedData: pushResult,
|
||||
decryptionHeader: await toB64(header),
|
||||
},
|
||||
encryptedData: pushResult,
|
||||
decryptionHeaderB64: await toB64(header),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const ENCRYPTION_CHUNK_SIZE = 4 * 1024 * 1024;
|
||||
|
||||
@@ -207,23 +250,38 @@ export async function encryptFileChunk(
|
||||
return pushResult;
|
||||
}
|
||||
|
||||
export async function decryptChaChaOneShot(
|
||||
data: Uint8Array,
|
||||
header: Uint8Array,
|
||||
key: string,
|
||||
) {
|
||||
/**
|
||||
* Decrypt the result of {@link encryptChaChaOneShot}.
|
||||
*
|
||||
* @param encryptedData A {@link Uint8Array} containing the bytes to decrypt.
|
||||
*
|
||||
* @param header A base64 string containing the bytes of the decryption header
|
||||
* that was produced during encryption.
|
||||
*
|
||||
* @param keyB64 The base64 string containing the key that was used to encrypt
|
||||
* the data.
|
||||
*
|
||||
* @returns The decrypted bytes.
|
||||
*
|
||||
* @returns The decrypted metadata bytes.
|
||||
*/
|
||||
export const decryptChaChaOneShot = async (
|
||||
encryptedData: Uint8Array,
|
||||
headerB64: string,
|
||||
keyB64: string,
|
||||
) => {
|
||||
await sodium.ready;
|
||||
const pullState = sodium.crypto_secretstream_xchacha20poly1305_init_pull(
|
||||
header,
|
||||
await fromB64(key),
|
||||
await fromB64(headerB64),
|
||||
await fromB64(keyB64),
|
||||
);
|
||||
const pullResult = sodium.crypto_secretstream_xchacha20poly1305_pull(
|
||||
pullState,
|
||||
data,
|
||||
encryptedData,
|
||||
null,
|
||||
);
|
||||
return pullResult.message;
|
||||
}
|
||||
};
|
||||
|
||||
export const decryptChaCha = async (
|
||||
data: Uint8Array,
|
||||
@@ -453,7 +511,7 @@ export async function generateSaltToDeriveKey() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new public/private keypair, and return their Base64
|
||||
* Generate a new public/private keypair, and return their base64
|
||||
* representations.
|
||||
*/
|
||||
export const generateKeyPair = async () => {
|
||||
|
||||
Reference in New Issue
Block a user