[web] Better standardize the crypto nomenclature used in code (#2620)

This commit is contained in:
Manav Rathi
2024-08-05 15:44:49 +05:30
committed by GitHub
14 changed files with 352 additions and 160 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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