Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
feffab9
refactor: `StoredCredential` stores `ciphersuite` not `signature_scheme`
coriolinus Nov 19, 2025
ce87314
chore: add `v7` wasm migration
coriolinus Nov 19, 2025
9ae3871
build: add dev-dependencies to keystore for a new test
coriolinus Dec 1, 2025
f3334b5
chore: add a sqlite migration test
coriolinus Dec 1, 2025
20c9501
chore: add `v18`, `v19` sqlite migrations + meta-migration
coriolinus Dec 1, 2025
2429a0d
chore: add `KeyPackage::make_ref`
coriolinus Nov 7, 2025
26f333d
feat: add ciphersuite to credential
coriolinus Nov 7, 2025
c85259b
chore: adjust tests for ciphersuite in credential
coriolinus Nov 7, 2025
7606b08
feat: add `generate_keypackage` according to new api
coriolinus Nov 7, 2025
a682a73
feat: add `getKeypackages()`
coriolinus Nov 7, 2025
49a4f64
chore: add `Session::load_keypackage`
coriolinus Nov 19, 2025
be15355
feat: add `TransactionContext::remove_keypackage`
coriolinus Nov 19, 2025
0a29434
feat: add `TransactionContext::remove_keypackages_for`
coriolinus Nov 19, 2025
e5f5429
chore: use new api in `Session::remove_credential`
coriolinus Nov 19, 2025
10a4936
chore(ffi): adjust usage to new CC api
coriolinus Nov 19, 2025
f777f26
chore(ffi): push new APIs to the FFI layer
coriolinus Nov 19, 2025
d6ff28c
chore: ensure everything builds
coriolinus Nov 20, 2025
43161ab
chore: ensure there is no unreachable pub item
coriolinus Nov 20, 2025
06a52a1
chore: make `KEYPACKAGE_DEFAULT_LIFETIME` public
coriolinus Nov 20, 2025
d696666
refactor: `Keypackage` is a struct of its own
coriolinus Nov 20, 2025
c0cf846
chore: cause to build cleanly on all targets
coriolinus Nov 20, 2025
dfb8790
chore: rm intra-doc links from ffi docs
coriolinus Nov 20, 2025
a001433
chore: reduce visibility to eliminate unreachable public item
coriolinus Nov 20, 2025
bbb3088
chore(ts): propagate new keypackage api
coriolinus Nov 20, 2025
7dafa5e
test(ts): add tests for new keypackage api
coriolinus Nov 20, 2025
166c056
chore(ffi): expose `SignatureScheme` over FFI
coriolinus Nov 21, 2025
f664fa9
chore(ffi): make certain conversions safely infallible
coriolinus Nov 21, 2025
0bc6114
chore: use correct procedure for identifying keypackages for a creden…
coriolinus Nov 21, 2025
e097e08
chore: cause uniffi to use a default when keypackage lifetime unspeci…
coriolinus Nov 21, 2025
8887842
test(jvm): add tests for new keypackage api
coriolinus Nov 21, 2025
76b6bb1
test(swift): add tests for new keypackage api
coriolinus Nov 21, 2025
b742863
chore: add some tsdoc `@param` notations
coriolinus Nov 21, 2025
fc25147
chore: reexport some more ts types for documentation
coriolinus Nov 21, 2025
7c94503
feat: add utility methods to `KeyPackageExt`
coriolinus Nov 24, 2025
9ea95b7
feat: make keypackage refs fat
coriolinus Nov 24, 2025
83e5704
chore: rm old `client_keypackages` method
coriolinus Nov 24, 2025
98053e2
chore: rm old `deleteStaleKeypackages` method
coriolinus Nov 24, 2025
d909bd7
chore: rm old `client_valid_keypackages_count` method
coriolinus Nov 24, 2025
937af61
chore: rm old `prune_keypackages` method
coriolinus Nov 24, 2025
569845f
chore: rm old `generate_one_keypackage_from_credential` method
coriolinus Nov 24, 2025
07c8cf9
chore: transaction context uses proper keypackage, ref structs
coriolinus Nov 24, 2025
ead8fa1
chore: CC tests compile with new keypackage api
coriolinus Nov 24, 2025
75aacba
chore(ffi): update to new keypackage api
coriolinus Nov 25, 2025
a39d68f
chore(interop): update to new keypackage api
coriolinus Nov 25, 2025
b38a15b
chore(docs): ensure intra-doc links work
coriolinus Nov 25, 2025
dcb24f5
chore: fix clippy lints
coriolinus Nov 25, 2025
2c66c3f
chore(benches): refactor initialization for better credential handling
coriolinus Nov 25, 2025
c6928f6
chore: fix a test which was failing due to changed semantics
coriolinus Nov 25, 2025
d47e27b
chore: fix a test which was failing due to changed semantics
coriolinus Nov 25, 2025
44f5f2e
chore(ts): remove removed methods
coriolinus Nov 25, 2025
c4d03ff
chore(ts): adjust tests to use only new keypackage api
coriolinus Nov 25, 2025
e8aedee
chore(ts): update docs to not mention removed methods
coriolinus Nov 25, 2025
007a941
chore(jvm): adapt tests to new keypackage api
coriolinus Nov 26, 2025
cf89d42
chore(swift): adjust tests to the new api
coriolinus Nov 27, 2025
366969e
chore(interop/js): adjust to new api
coriolinus Nov 27, 2025
beab6cd
chore: `cargo +nightly fmt`
coriolinus Dec 1, 2025
326ba95
chore(js): copy bun tests into browser context
coriolinus Dec 1, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions crypto-ffi/bindings/js/src/CoreCrypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@ export {
type AcmeDirectory,
ProteusAutoPrekeyBundle,
BufferedDecryptedMessage,
Keypackage,
KeypackageRef,
type KeypackageInterface,
type KeypackageRefInterface,
SignatureScheme,
} from "./autogenerated/core_crypto_ffi";

import * as core_crypto_ffi from "./autogenerated/core_crypto_ffi";
Expand Down
109 changes: 55 additions & 54 deletions crypto-ffi/bindings/js/src/CoreCryptoContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
CustomConfiguration,
WireIdentity,
ConversationId,
KeyPackage,
Keypackage,
Welcome,
type SecretKeyInterface,
type ExternalSenderKeyInterface,
Expand All @@ -14,6 +14,8 @@ import {
DecryptedMessage,
type CredentialRefInterface,
type CredentialInterface,
type KeypackageInterface,
type KeypackageRefInterface,
} from "./autogenerated/core_crypto_ffi";
import * as CoreCryptoFfiTypes from "./autogenerated/core_crypto_ffi";

Expand Down Expand Up @@ -280,45 +282,6 @@ export class CoreCryptoContext {
);
}

/**
*
* @param ciphersuite - of the KeyPackages to count
* @param credentialType - of the KeyPackages to count
* @returns The amount of valid, non-expired KeyPackages that are persisted in the backing storage
*/
async clientValidKeypackagesCount(
ciphersuite: Ciphersuite,
credentialType: CredentialType
): Promise<number> {
const kpCount = await CoreCryptoError.asyncMapErr(
this.#ctx.clientValidKeypackagesCount(ciphersuite, credentialType)
);
return safeBigintToNumber(kpCount);
}

/**
* Fetches a requested amount of keypackages
*
* @param ciphersuite - of the KeyPackages to generate
* @param credentialType - of the KeyPackages to generate
* @param amountRequested - The amount of keypackages requested
* @returns An array of length `amountRequested` containing TLS-serialized KeyPackages
*/
async clientKeypackages(
ciphersuite: Ciphersuite,
credentialType: CredentialType,
amountRequested: number
): Promise<Array<ArrayBuffer>> {
const kps = await CoreCryptoError.asyncMapErr(
this.#ctx.clientKeypackages(
ciphersuite,
credentialType,
amountRequested
)
);
return kps.map((kp) => kp.copyBytes());
}

/**
* Adds new clients to a conversation, assuming the current client has the right to add new clients to the conversation.
*
Expand All @@ -333,7 +296,7 @@ export class CoreCryptoContext {
conversationId: ConversationId,
keyPackages: ArrayBuffer[]
): Promise<NewCrlDistributionPoints> {
const kps = keyPackages.map((bytes) => new KeyPackage(bytes));
const kps = keyPackages.map((bytes) => new Keypackage(bytes));
return await CoreCryptoError.asyncMapErr(
this.#ctx.addClientsToConversation(conversationId, kps)
);
Expand Down Expand Up @@ -878,9 +841,9 @@ export class CoreCryptoContext {
*
* # Expected actions to perform after this function (in this order)
* 1. Rotate credentials for each conversation using {@link CoreCryptoContext.e2eiRotate}
* 2. Generate new key packages with {@link CoreCryptoContext.clientKeypackages}
* 2. Generate new key packages with {@link CoreCryptoContext.generateKeypackage}
* 3. Use these to replace the stale ones the in the backend
* 4. Delete the stale ones locally using {@link CoreCryptoContext.deleteStaleKeyPackages}
* 4. Delete the stale ones locally.
* * This is the last step because you might still need the old key packages to avoid
* an orphan welcome message
*
Expand All @@ -897,17 +860,6 @@ export class CoreCryptoContext {
);
}

/**
* Deletes all key packages whose credential does not match the most recently
* saved x509 credential and the provided signature scheme.
* @param ciphersuite
*/
async deleteStaleKeyPackages(ciphersuite: Ciphersuite): Promise<void> {
return await CoreCryptoError.asyncMapErr(
this.#ctx.deleteStaleKeyPackages(ciphersuite)
);
}

/**
* Allows persisting an active enrollment (for example while redirecting the user during OAuth) in order to resume
* it later with {@link e2eiEnrollmentStashPop}
Expand Down Expand Up @@ -1050,4 +1002,53 @@ export class CoreCryptoContext {
)
);
}

/**
* Generate a `KeyPackage` from the referenced credential.
*
* Makes no attempt to look up or prune existing keypackges.
*
* @param credentialRef references the credential to be used as the foundation for this keypackage
* @param lifetime how long this keypackage is valid for. If unset, approx 3 months. Expressed in milliseconds.
*/
async generateKeypackage(
credentialRef: CredentialRefInterface,
lifetime?: number
): Promise<KeypackageInterface> {
const mappedLifetime = lifetime !== null ? lifetime : undefined;
return await CoreCryptoError.asyncMapErr(
this.#ctx.generateKeypackage(credentialRef, mappedLifetime)
);
}

/**
* Get a reference to each `KeyPackage` in the database.
*/
async getKeypackages(): Promise<Array<KeypackageRefInterface>> {
return await CoreCryptoError.asyncMapErr(this.#ctx.getKeypackages());
}

/**
* Remove a `KeyPackage` from the database.
*
* @param kpRef references the keypackage to be removed.
*/
async removeKeypackage(kpRef: KeypackageRefInterface): Promise<void> {
return await CoreCryptoError.asyncMapErr(
this.#ctx.removeKeypackage(kpRef)
);
}

/**
* Remove all `KeyPackage`s associated with this credential ref.
*
* @param credentialRef references the credential for which all keypackages should be removed.
*/
async removeKeypackagesFor(
credentialRef: CredentialRefInterface
): Promise<void> {
return await CoreCryptoError.asyncMapErr(
this.#ctx.removeKeypackagesFor(credentialRef)
);
}
}
166 changes: 166 additions & 0 deletions crypto-ffi/bindings/js/test/bun/keyPackage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { ccInit, setup, teardown } from "./utils";
import { afterEach, test, beforeEach, describe, expect } from "bun:test";
import { ciphersuiteDefault, ClientId } from "../../src/CoreCrypto";
import {
Ciphersuite,
credentialBasic,
Keypackage,
} from "../../src/autogenerated/core_crypto_ffi";

beforeEach(async () => {
await setup();
});

afterEach(async () => {
await teardown();
});

describe("key package", () => {
test("can be created", async () => {
const clientId = new ClientId(
Buffer.from("any random client id here").buffer
);
const credential = credentialBasic(ciphersuiteDefault(), clientId);

const cc = await ccInit(clientId);

const credentialRef = await cc.transaction(async (ctx) => {
return await ctx.addCredential(credential);
});

const keyPackage = await cc.transaction(async (ctx) => {
return await ctx.generateKeypackage(credentialRef);
});

expect(keyPackage).toBeDefined();
});

test("can be serialized", async () => {
const clientId = new ClientId(
Buffer.from("any random client id here").buffer
);
const credential = credentialBasic(ciphersuiteDefault(), clientId);

const cc = await ccInit(clientId);

const credentialRef = await cc.transaction(async (ctx) => {
return await ctx.addCredential(credential);
});

const keyPackage = await cc.transaction(async (ctx) => {
return await ctx.generateKeypackage(credentialRef);
});

const bytes = new Uint8Array(keyPackage.serialize());

expect(bytes).toBeDefined();
expect(bytes).not.toBeEmpty();

// roundtrip
const kp2 = new Keypackage(bytes.buffer);
const bytes2 = new Uint8Array(kp2.serialize());

expect(bytes2).toEqual(bytes);
});

test("can be retrieved in bulk", async () => {
const clientId = new ClientId(
Buffer.from("any random client id here").buffer
);
const credential = credentialBasic(ciphersuiteDefault(), clientId);

const cc = await ccInit(clientId);

const credentialRef = await cc.transaction(async (ctx) => {
return await ctx.addCredential(credential);
});

await cc.transaction(async (ctx) => {
await ctx.generateKeypackage(credentialRef);
});

const keyPackages = await cc.transaction(async (ctx) => {
return await ctx.getKeypackages();
});

expect(keyPackages).toBeDefined();
expect(keyPackages).toBeArrayOfSize(1);
expect(keyPackages[0]).toBeDefined();
});

test("can be removed", async () => {
const clientId = new ClientId(
Buffer.from("any random client id here").buffer
);
const credential = credentialBasic(ciphersuiteDefault(), clientId);

const cc = await ccInit(clientId);

const credentialRef = await cc.transaction(async (ctx) => {
return await ctx.addCredential(credential);
});

// add a kp which will not be removed, so we have one left over
await cc.transaction(async (ctx) => {
await ctx.generateKeypackage(credentialRef);
});

// add a kp which will be removed
const keyPackage = await cc.transaction(async (ctx) => {
return await ctx.generateKeypackage(credentialRef);
});

// now remove the keypackage
await cc.transaction(async (ctx) => {
await ctx.removeKeypackage(keyPackage.ref());
});

const keyPackages = await cc.transaction(async (ctx) => {
return await ctx.getKeypackages();
});

expect(keyPackages).toBeDefined();
expect(keyPackages).toBeArrayOfSize(1);
});

test("can be removed by credentialref", async () => {
const clientId = new ClientId(
Buffer.from("any random client id here").buffer
);
const credential1 = credentialBasic(
Ciphersuite.Mls128Dhkemx25519Aes128gcmSha256Ed25519,
clientId
);
const credential2 = credentialBasic(
Ciphersuite.Mls128Dhkemp256Aes128gcmSha256P256,
clientId
);
const cc = await ccInit(clientId);

await cc.transaction(async (ctx) => {
const cref1 = await ctx.addCredential(credential1);
const cref2 = await ctx.addCredential(credential2);

// we're going to generate keypackages for both credentials,
// then remove those packages for credential 2, leaving behind those for credential 1
const KEYPACKAGES_PER_CREDENTIAL = 2;
for (const cref of [cref1, cref2]) {
for (let i = 0; i < KEYPACKAGES_PER_CREDENTIAL; i++) {
await ctx.generateKeypackage(cref);
}
}

const kpsBeforeRemoval = await ctx.getKeypackages();
// 2 credentials with the same n keypackages each
expect(kpsBeforeRemoval).toBeArrayOfSize(
KEYPACKAGES_PER_CREDENTIAL * 2
);

// now remove all keypackages for one of the credentials
await ctx.removeKeypackagesFor(cref1);

const kps = await ctx.getKeypackages();
expect(kps).toBeArrayOfSize(KEYPACKAGES_PER_CREDENTIAL);
});
});
});
12 changes: 8 additions & 4 deletions crypto-ffi/bindings/js/test/bun/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,11 +153,15 @@ export async function invite(
cc2: CoreCrypto,
conversationId: ConversationId
): Promise<GroupInfoBundle> {
const [kp] = await cc2.transaction((ctx) =>
ctx.clientKeypackages(DEFAULT_CIPHERSUITE, CredentialType.Basic, 1)
);
const kp = await cc2.transaction(async (ctx) => {
const [credentialRef] = await ctx.findCredentials({
ciphersuite: DEFAULT_CIPHERSUITE,
credentialType: CredentialType.Basic,
});
return await ctx.generateKeypackage(credentialRef!);
});
await cc1.transaction((ctx) =>
ctx.addClientsToConversation(conversationId, [kp!])
ctx.addClientsToConversation(conversationId, [kp.serialize()])
);
const { groupInfo, welcome } =
await DELIVERY_SERVICE.getLatestCommitBundle();
Expand Down
Loading
Loading