Green: unseal shared collection keys with the account keypair

decryptCollection now takes the full key material {masterKey,
publicKey, secretKey} and dispatches on keyDecryptionNonce: present
means an owned collection (secretbox under the master key), absent
means a shared collection (sealed box to our public key). Client
already held the keypair for unsealing the auth token, so it just
passes it through.
This commit is contained in:
2026-06-10 11:46:51 -07:00
parent 59e0aa7d47
commit 15d2effc2d
6 changed files with 53 additions and 11 deletions

View File

@@ -156,7 +156,15 @@ export class Client {
collections: RawCollection[];
}>("/collections/v2", { sinceTime: 0 });
return collections.map((raw) =>
decryptCollection(raw, this.masterKey, this.userID),
decryptCollection(
raw,
{
masterKey: this.masterKey,
publicKey: this.publicKey,
secretKey: this.secretKey,
},
this.userID,
),
);
}

View File

@@ -24,6 +24,7 @@ export type {
FileBlob,
FileMetadata,
FileType,
KeyMaterial,
Microseconds,
RawCollection,
RawEnteFile,

View File

@@ -1,10 +1,16 @@
import { decryptBlob, decryptBox, fromBase64 } from "../crypto/index.js";
import {
decryptBlob,
decryptBox,
decryptSealed,
fromBase64,
} from "../crypto/index.js";
import type {
Collection,
CollectionType,
EnteFile,
FileMetadata,
FileType,
KeyMaterial,
RawCollection,
RawEnteFile,
RawMagicMetadata,
@@ -30,14 +36,25 @@ const parseFileType = (n: number): FileType => FILE_TYPE_MAP[n] ?? "unknown";
export const decryptCollection = (
raw: RawCollection,
masterKey: Uint8Array,
keys: KeyMaterial,
currentUserID?: number,
): Collection => {
const key = decryptBox(
fromBase64(raw.encryptedKey),
fromBase64(raw.keyDecryptionNonce),
masterKey,
);
// Owned collections carry their key as a secretbox under our master
// key, with the nonce in keyDecryptionNonce. Collections shared with
// us carry it as an anonymous sealed box to our public key and have
// no keyDecryptionNonce at all (sealed boxes embed an ephemeral
// public key instead).
const key = raw.keyDecryptionNonce
? decryptBox(
fromBase64(raw.encryptedKey),
fromBase64(raw.keyDecryptionNonce),
keys.masterKey,
)
: decryptSealed(
fromBase64(raw.encryptedKey),
keys.publicKey,
keys.secretKey,
);
let name = "";
if (raw.encryptedName && raw.nameDecryptionNonce) {

View File

@@ -6,6 +6,7 @@ export type {
FileBlob,
FileMetadata,
FileType,
KeyMaterial,
Microseconds,
RawCollection,
RawEnteFile,

View File

@@ -50,13 +50,25 @@ export interface EnteFile {
updationTime: Microseconds;
}
// The key material a logged-in client holds, everything needed to decrypt
// any collection: the master key (secretbox for owned collection keys) and
// the X25519 keypair (sealed box for collection keys shared with us).
export interface KeyMaterial {
masterKey: Uint8Array;
publicKey: Uint8Array;
secretKey: Uint8Array;
}
// Raw shapes as they arrive from the Ente API, before decryption.
export interface RawCollection {
id: number;
owner: { id: number };
encryptedKey: string;
keyDecryptionNonce: string;
// Absent for collections shared with us: their encryptedKey is a
// sealed box to our public key, which embeds an ephemeral public key
// instead of using a nonce.
keyDecryptionNonce?: string;
encryptedName?: string;
nameDecryptionNonce?: string;
type: string;