Skip to content

Conversation

@debdutdeb
Copy link
Member

@debdutdeb debdutdeb commented Sep 15, 2025

Summary by CodeRabbit

  • New Features
    • Added Ed25519 key support, JSON signing/verification, canonical JSON and hashing utilities, base64 helpers, and PEM encoding.
  • Refactor
    • Modularized crypto package with consolidated root exports; removed legacy inline implementations.
  • Documentation
    • Introduced README covering package usage and public interfaces.
  • Tests
    • Added comprehensive tests for canonical JSON and signature flows; removed outdated message event tests.
  • Chores
    • Added a new runtime dependency for Ed25519.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 15, 2025

Walkthrough

Introduces a modular crypto package: adds Ed25519 key implementations, DER/RFC8410 PEM encoders, data-type and constant utilities, and JSON signing/verification helpers. Refactors index to a barrel of re-exports. Adds a dependency on @noble/ed25519. Updates tests accordingly and removes two core event tests.

Changes

Cohort / File(s) Summary
Dependency Update
packages/crypto/package.json
Adds runtime dependency @noble/ed25519@^3.0.0.
Documentation
packages/crypto/src/README.md
Adds package README describing structure, interfaces, and utilities.
Contracts
packages/crypto/src/contracts/key.ts
Adds VerifierKey and Signer interfaces and KeyId type alias.
DER & PEM for Ed25519
packages/crypto/src/der/index.ts, packages/crypto/src/rfc/8410/ed25519-pem.ts
Adds DER TLV builders and RFC 8410 PEM encoders for Ed25519 public/private keys.
Ed25519 Key Implementations
packages/crypto/src/keys/ed25519.ts
Adds Ed25519VerifierKeyImpl and Ed25519SigningKeyImpl (verify/sign using Node crypto; raw↔PEM conversions).
Utilities: Constants & Data Types
packages/crypto/src/utils/constants.ts, packages/crypto/src/utils/data-types.ts
Adds enum EncryptionValidAlgorithm, type guards, canonical JSON, hashing, base64, and binary conversion helpers.
Signing/Verification Utilities
packages/crypto/src/utils/keys.ts
Adds loaders for Ed25519 signer/verifier and signJson/verifyJsonSignature.
Barrel Refactor
packages/crypto/src/index.ts
Replaces inline implementations with re-exports from utils/* and contracts/*; removes previous helpers and enums.
Crypto Tests
packages/crypto/src/utils/utils.spec.ts, packages/crypto/src/index.spec.ts
Adds comprehensive tests for canonical JSON and signing/verification; updates imports to new modules.
Core Test Removals
packages/core/src/events/m.room.message.edit.spec.ts, packages/core/src/events/m.room.message.spec.ts
Removes two event-building/signing tests.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant App
  participant Utils as utils/keys
  participant Data as utils/data-types
  participant Signer as Signer (Ed25519)
  App->>Utils: signJson(jsonObj, signer)
  Utils->>Data: encodeCanonicalJson(jsonObj)
  Data-->>Utils: canonicalJson
  Utils->>Data: toBinaryData(canonicalJson)
  Data-->>Utils: bytes
  Utils->>Signer: sign(bytes)
  Signer-->>Utils: signature(bytes)
  Utils->>Data: toUnpaddedBase64(signature)
  Data-->>App: signature(base64)
Loading
sequenceDiagram
  autonumber
  participant App
  participant Utils as utils/keys
  participant Data as utils/data-types
  participant Verifier as VerifierKey (Ed25519)
  App->>Utils: verifyJsonSignature(jsonObj, signatureB64, verifier)
  Utils->>Data: encodeCanonicalJson(jsonObj)
  Data-->>Utils: canonicalJson
  Utils->>Data: toBinaryData(canonicalJson)
  Data-->>Utils: bytes
  Utils->>Data: fromBase64ToBytes(signatureB64)
  Data-->>Utils: signature(bytes)
  Utils->>Verifier: verify(bytes, signature)
  Verifier-->>App: success or throw
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

whiskers twitch, I sign the JSON stream,
bytes in a row, like carrots in a dream.
DER and PEM stitched neat and tight,
ed25519 hops—secure and light.
new keys sprout, old paths refactor—done!
I stamp my paw: verified. Ship it, run! 🥕🐇

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "feat: add crypto package" succinctly and accurately captures the primary change in this PR—adding a new crypto package with related modules and utilities. It is concise, uses conventional commit style, and conveys the main intent for teammates scanning history.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fdr108-crypto-package

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Nitpick comments (10)
packages/crypto/src/utils/constants.ts (1)

1-17: Types and enum look good; tiny optional tidy-up.

isValidAlgorithm is fine. Optional: precompute a Set for O(1) lookup if called frequently.

packages/crypto/src/README.md (1)

9-18: Docs reference non-existent paths/APIs.

  • Mentions signing-key.ts; the code adds contracts/key.ts and keys/ed25519.ts. Please align filenames and terminology (Signer/Verifier vs “SigningKey”).
  • Production note mentions “nacl/libsodium” but the implementation uses Node Crypto + noble; clarify runtime story.
packages/crypto/src/rfc/8410/ed25519-pem.ts (1)

34-37: Typos in comments.

  • “CTET” → “NOTE” or remove.
  • “subhjectPublicKey” → “subjectPublicKey”.

Also applies to: 81-83

packages/crypto/src/contracts/key.ts (1)

7-29: API surface is reasonable; export and immutability nits.

  • Consider export type KeyId = ... so consumers can type the id.
  • Consider readonly publicKey/privateKey: Uint8Array to discourage mutation.
packages/crypto/src/utils/keys.ts (1)

27-36: Signing flow is fine; keep base64 variant consistent.

You emit unpadded Base64. Ensure all decoders accept unpadded form across runtimes.

If needed, add optional padding in fromBase64ToBytes or normalize here before decoding.

packages/crypto/src/utils/utils.spec.ts (1)

13-22: Helper is fine; tiny nit.

Drop the non-null assertion; assert parts length instead.

- const seed = content.split(' ').pop()!;
+ const parts = content.trim().split(/\s+/);
+ if (parts.length < 3) throw new Error('Invalid key content');
+ const seed = parts[parts.length - 1];
packages/crypto/src/keys/ed25519.ts (2)

15-17: Nit: drop unnecessary “as const” on id

It over-narrows the type and is unnecessary for a string id.

-	public get id() {
-		return `${this.algorithm}:${this.version}` as const;
-	}
+	public get id() {
+		return `${this.algorithm}:${this.version}`;
+	}

62-70: Optional: consider key material hygiene

Private key bytes remain in memory after PEM conversion. Consider zeroizing the Uint8Array after use or accepting KeyObject to avoid raw PEM handling in memory.

packages/crypto/src/der/index.ts (1)

108-111: Comment accuracy: Ed25519 AlgorithmIdentifier parameters MUST be absent

RFC 8410 requires parameters to be absent (not NULL). Comment says “parameters for ed25519 is NULL”; keep it commented out but fix the note.

 export const algorithmIdentifierTlv = sequenceOrderedTlv([
 	oidTlv,
-	// nullTlv, // parameters for ed25519 is NULL
+	// Parameters for Ed25519 MUST be absent (RFC 8410 §3); do not include NULL.
 ]);
packages/crypto/src/utils/data-types.ts (1)

21-37: Verify TextEncoder/TextDecoder availability in Node build

Global TextEncoder/TextDecoder types aren’t always available unless lib DOM is enabled. If tsconfig doesn’t include DOM libs, import from 'node:util' or use Buffer-based conversions.

Example alternative:

-	if (typeof value === 'string') {
-		return new TextEncoder().encode(value);
-	}
+	if (typeof value === 'string') {
+		return Buffer.from(value, 'utf8');
+	}
...
-	return new TextDecoder().decode(value);
+	return Buffer.isBuffer(value)
+		? value.toString('utf8')
+		: Buffer.from(value as ArrayBuffer).toString('utf8');

Also applies to: 39-47

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5f43238 and d61a320.

📒 Files selected for processing (11)
  • packages/crypto/package.json (1 hunks)
  • packages/crypto/src/README.md (1 hunks)
  • packages/crypto/src/contracts/key.ts (1 hunks)
  • packages/crypto/src/der/index.ts (1 hunks)
  • packages/crypto/src/index.ts (1 hunks)
  • packages/crypto/src/keys/ed25519.ts (1 hunks)
  • packages/crypto/src/rfc/8410/ed25519-pem.ts (1 hunks)
  • packages/crypto/src/utils/constants.ts (1 hunks)
  • packages/crypto/src/utils/data-types.ts (1 hunks)
  • packages/crypto/src/utils/keys.ts (1 hunks)
  • packages/crypto/src/utils/utils.spec.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (7)
packages/crypto/src/utils/keys.ts (3)
packages/crypto/src/contracts/key.ts (2)
  • Signer (25-29)
  • VerifierKey (10-21)
packages/crypto/src/keys/ed25519.ts (2)
  • Ed25519SigningKeyImpl (47-71)
  • Ed25519VerifierKeyImpl (10-45)
packages/crypto/src/utils/data-types.ts (4)
  • encodeCanonicalJson (63-82)
  • toBinaryData (21-37)
  • toUnpaddedBase64 (49-60)
  • fromBase64ToBytes (84-86)
packages/crypto/src/contracts/key.ts (1)
packages/crypto/src/utils/constants.ts (2)
  • DataType (5-5)
  • SignatureType (7-7)
packages/crypto/src/utils/constants.ts (2)
packages/core/src/index.ts (1)
  • EncryptionValidAlgorithm (2-2)
packages/crypto/src/index.ts (1)
  • isValidAlgorithm (11-11)
packages/crypto/src/utils/utils.spec.ts (2)
packages/crypto/src/utils/data-types.ts (2)
  • fromBase64ToBytes (84-86)
  • encodeCanonicalJson (63-82)
packages/crypto/src/utils/keys.ts (4)
  • loadEd25519SignerFromSeed (11-18)
  • signJson (27-36)
  • verifyJsonSignature (39-49)
  • loadEd25519VerifierFromPublicKey (20-25)
packages/crypto/src/utils/data-types.ts (1)
packages/crypto/src/index.ts (7)
  • computeHashBuffer (3-3)
  • encodeCanonicalJson (5-5)
  • computeHashString (4-4)
  • toUnpaddedBase64 (2-2)
  • toBinaryData (6-6)
  • fromBinaryData (7-7)
  • fromBase64ToBytes (8-8)
packages/crypto/src/keys/ed25519.ts (2)
packages/crypto/src/contracts/key.ts (2)
  • VerifierKey (10-21)
  • Signer (25-29)
packages/crypto/src/rfc/8410/ed25519-pem.ts (2)
  • ed25519PublicKeyRawToPem (76-88)
  • ed25519PrivateKeyRawToPem (38-63)
packages/crypto/src/rfc/8410/ed25519-pem.ts (1)
packages/crypto/src/der/index.ts (5)
  • privateKeyVersionTlv (30-34)
  • algorithmIdentifierTlv (108-111)
  • octetStringTlv (89-106)
  • sequenceOrderedTlv (58-86)
  • bitStringTlv (114-126)
🔇 Additional comments (4)
packages/crypto/src/rfc/8410/ed25519-pem.ts (1)

38-63: PKCS#8 (RFC 5958/8410) structure: implementation looks correct.

Validations and double OCTET for privateKey (CurvePrivateKey inside privateKey) match RFC 8410 examples. Good.

Add a unit test that round-trips with Node/Bun crypto:

// pseudo-test
const privPem = ed25519PrivateKeyRawToPem(seed32);
const pubPem = ed25519PublicKeyRawToPem(pub32);
const keyObj = crypto.createPrivateKey(privPem);
const pubObj = crypto.createPublicKey(pubPem);
expect(keyObj.asymmetricKeyType).toBe('ed25519');
expect(pubObj.asymmetricKeyType).toBe('ed25519');
packages/crypto/src/utils/keys.ts (1)

38-49: Verifier path is correct modulo base64 padding concern.

LGTM otherwise.

packages/crypto/package.json (1)

18-20: No change required — @noble/ed25519@^3.0.0 supports keygenAsync. ed.keygenAsync()/ed.keygen() return { secretKey, publicKey }; ed.utils.randomPrivateKey() and ed.getPublicKey/getPublicKeyAsync are available, so the dependency in packages/crypto/package.json is compatible with the current usage.

packages/crypto/src/index.ts (1)

1-20: Barrel re-export looks good

Public surface is cleanly organized and consistent with the new modular structure.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (6)
packages/crypto/src/index.spec.ts (3)

3-8: Remove unused import (encodeCanonicalJson).

encodeCanonicalJson is imported but not used; this will fail linting/CI. Drop it.

-import { encodeCanonicalJson, fromBase64ToBytes } from './utils/data-types';
+import { fromBase64ToBytes } from './utils/data-types';

47-51: Brittle assertion against a fixed signature.

Hard-coding the exact base64 ties this test to implementation details of canonicalization. Keep if you intend to lock that exact canonical form; otherwise, prefer asserting that the produced signature verifies with the known public key in a separate assertion.


104-108: Assert verification outcome, not just absence of throw.

Currently no assertion on the return value; the test would pass even if verification returns false but doesn't throw.

Option A (if it returns boolean):

-    await verifyJsonSignature(json, signature, verifier);
+    const ok = await verifyJsonSignature(json, signature, verifier);
+    expect(ok).toBe(true);

Option B (if it throws on failure and returns void):

-    await verifyJsonSignature(json, signature, verifier);
+    await expect(verifyJsonSignature(json, signature, verifier)).resolves.toBeUndefined();

Please confirm the contract and apply the appropriate option.

packages/federation-sdk/src/services/key.service.ts (3)

19-22: Handle signing-key load failures (unhandled rejection) or remove unused state.

this.key is never used; at minimum, catch and log failures to avoid unhandled rejections.

-    this.configService.getSigningKey().then((key) => {
-      this.key = key;
-    });
+    this.configService
+      .getSigningKey()
+      .then((key) => {
+        this.key = key;
+      })
+      .catch((e) => {
+        this.logger.error('Failed to load signing key', e);
+      });

If unused, consider removing key until needed to reduce confusion.


210-229: Store only active verify_keys — confirm old keys policy.

You ignore old_verify_keys. If you need to validate historical events, you may want to persist those with expiredTs.

I can add an optional branch to persist old_verify_keys with their expired_ts if that’s required by your federation flows.


78-79: Nit: fix typos in comments.

“notmal” → “normal”.

-    // notmal http is enough
+    // normal http is enough
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d61a320 and 64d5617.

📒 Files selected for processing (2)
  • packages/crypto/src/index.spec.ts (3 hunks)
  • packages/federation-sdk/src/services/key.service.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
packages/federation-sdk/src/services/key.service.ts (2)
packages/crypto/src/contracts/key.ts (1)
  • Signer (25-29)
packages/core/src/server.ts (1)
  • ServerKey (3-20)
packages/crypto/src/index.spec.ts (1)
packages/core/src/utils/signJson.ts (2)
  • signJson (17-50)
  • verifyJsonSignature (115-134)
🔇 Additional comments (2)
packages/crypto/src/index.spec.ts (1)

45-47: Confirm seed decoding invariants (32 bytes, url-safe/unpadded) and add negative tests.

Ensure fromBase64ToBytes(seed) returns exactly 32 bytes and accepts unpadded/base64url inputs. In packages/crypto/src/index.spec.ts (around lines 45–47) add: a positive test asserting a valid seed (including unpadded/base64url) decodes to 32 bytes, and negative tests asserting malformed base64 and decoded-length != 32 are rejected/throw. Verified sample seed decodes to 32 bytes (std & urlsafe).

packages/federation-sdk/src/services/key.service.ts (1)

3-7: Import verifySignaturesFromRemote from @hs/core

verifySignaturesFromRemote is exported by @hs/core (not @hs/crypto) — add in packages/federation-sdk/src/services/key.service.ts, e.g.:
import { verifySignaturesFromRemote } from '@hs/core';

Likely an incorrect or invalid review comment.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (5)
packages/crypto/src/utils/utils.spec.ts (3)

25-29: Don't use async describe; move async setup to beforeAll.

describe blocks should be synchronous. Initialize signer via beforeAll for proper test lifecycle management.

Apply this diff:

-describe('Signing and verifying payloads', async () => {
-	const seedFileContent =
-		'ed25519 a_XRhW YjbSyfqQeGto+OFswt+XwtJUUooHXH5w+czSgawN63U';
-
-	const signer = await getSignerFromKeyContent(seedFileContent);
+describe('Signing and verifying payloads', () => {
+	const seedFileContent =
+		'ed25519 a_XRhW YjbSyfqQeGto+OFswt+XwtJUUooHXH5w+czSgawN63U';
+	let signer: Awaited<ReturnType<typeof getSignerFromKeyContent>>;
+	
+	beforeAll(async () => {
+		signer = await getSignerFromKeyContent(seedFileContent);
+	});

102-104: Fix Promise rejection assertion.

Use await expect(promise).rejects pattern for proper async rejection testing.

Apply this diff:

-		expect(verifyJsonSignature({}, signature, verifier)).rejects.toThrowError(
-			'Invalid signature',
-		);
+		await expect(verifyJsonSignature({}, signature, verifier)).rejects.toThrow(
+			'Invalid signature'
+		);

85-86: Fix Promise rejection assertion.

The current assertion syntax doesn't properly test Promise rejection. Use await expect(promise).rejects pattern instead.

Apply this diff:

-		expect(verifyJsonSignature({}, signature, signer)).resolves;
+		await expect(verifyJsonSignature({}, signature, signer)).resolves.toBeDefined();
packages/crypto/src/keys/ed25519.ts (2)

34-52: Fix: crypto.verify is synchronous; remove callback usage.

Node's crypto.verify is synchronous and returns a boolean directly. The callback signature doesn't exist and will fail at runtime.

Based on the retrieved learning, it appears there was confusion about the API. The Node.js documentation confirms that crypto.verify is synchronous when using the algorithm-less form.

Apply this diff:

-	public async verify(data: Uint8Array, signature: Uint8Array): Promise<void> {
-		return new Promise((resolve, reject) => {
-			crypto.verify(
-				null,
-				data,
-				this._publicKeyPem,
-				signature,
-				(err, verified) => {
-					if (err) {
-						reject(err);
-					} else if (verified) {
-						resolve();
-					} else {
-						reject(new Error('Invalid signature'));
-					}
-				},
-			);
-		});
-	}
+	public async verify(data: Uint8Array, signature: Uint8Array): Promise<void> {
+		const verified = crypto.verify(null, data, this._publicKeyPem, signature);
+		if (!verified) {
+			throw new Error('Invalid signature');
+		}
+	}

59-68: Fix: crypto.sign is synchronous; remove callback usage.

crypto.sign is synchronous and returns a Buffer directly. Convert to Uint8Array and return.

Apply this diff:

-	public async sign(data: Uint8Array) {
-		return new Promise<Uint8Array>((resolve, reject) => {
-			crypto.sign(null, data, this._privateKeyPem, (err, signature) => {
-				if (err) {
-					return reject(err);
-				}
-				return resolve(signature);
-			});
-		});
-	}
+	public async sign(data: Uint8Array): Promise<Uint8Array> {
+		const signature = crypto.sign(null, data, this._privateKeyPem);
+		return new Uint8Array(signature);
+	}
🧹 Nitpick comments (7)
packages/crypto/src/utils/utils.spec.ts (1)

10-12: Consider removing deprecated comments.

The comment suggests these are temporary notes that "won't have much point later". Consider removing them before merging.

packages/crypto/src/index.spec.ts (1)

108-108: Fix Promise resolution assertion.

The current assertion syntax doesn't properly test Promise resolution. Use await expect(promise).resolves pattern.

Apply this diff:

-		expect(verifyJsonSignature(json, signature, verifier)).resolves;
+		await expect(verifyJsonSignature(json, signature, verifier)).resolves.toBeDefined();
packages/crypto/src/keys/ed25519.ts (1)

70-70: Remove unnecessary non-null assertion.

The _privateKeyPem property is initialized in the constructor, so the ! assertion is unnecessary.

Apply this diff:

-	private _privateKeyPem!: string;
+	private _privateKeyPem: string;
packages/crypto/src/index.ts (4)

1-9: Solid barrel exports; minor naming polish for symmetry

LGTM overall. Consider adding a non‑breaking alias for fromBase64ToBytes to improve symmetry with the encoder names.

Apply this diff to add an alias without breaking existing imports:

 export {
   toUnpaddedBase64,
   computeHashBuffer,
   computeHashString,
   encodeCanonicalJson,
   toBinaryData,
   fromBinaryData,
-  fromBase64ToBytes,
+  fromBase64ToBytes,
+  // Alias for naming symmetry; keep original for BC
+  fromBase64ToBytes as base64ToBytes,
 } from './utils/data-types';

11-11: Disambiguate isValidAlgorithm or document scope

Name is a bit generic; if it only relates to Ed25519 (or a fixed set), either narrow the name (e.g., isValidEd25519Algorithm) or add a brief doc comment in utils/constants.ts clarifying its domain.


1-20: Add package export map and mark side‑effect‑free for better DX and tree‑shaking

Expose only intended entry points and discourage deep imports.

Add to packages/crypto/package.json:

{
  "name": "@rocketchat/crypto",
  "sideEffects": false,
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js"
    }
  }
}

13-13: Replace export * with type-only named re-exports

packages/crypto/src/contracts/key.ts only exports interfaces (VerifierKey, Signer) — replace the barrel line in packages/crypto/src/index.ts:

export * from './contracts/key';

with:

export type { VerifierKey, Signer } from './contracts/key';

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 64d5617 and bcd6d15.

📒 Files selected for processing (6)
  • packages/crypto/src/README.md (1 hunks)
  • packages/crypto/src/contracts/key.ts (1 hunks)
  • packages/crypto/src/index.spec.ts (3 hunks)
  • packages/crypto/src/index.ts (1 hunks)
  • packages/crypto/src/keys/ed25519.ts (1 hunks)
  • packages/crypto/src/utils/utils.spec.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-15T04:56:11.689Z
Learnt from: debdutdeb
PR: RocketChat/homeserver#190
File: packages/crypto/src/keys/ed25519.ts:34-52
Timestamp: 2025-09-15T04:56:11.689Z
Learning: Node.js crypto.verify function supports both synchronous and asynchronous (callback) modes. When a callback is provided as the fifth parameter, it uses libuv's threadpool for non-blocking operation.

Applied to files:

  • packages/crypto/src/keys/ed25519.ts
🧬 Code graph analysis (4)
packages/crypto/src/utils/utils.spec.ts (2)
packages/crypto/src/utils/data-types.ts (2)
  • fromBase64ToBytes (84-86)
  • encodeCanonicalJson (63-82)
packages/crypto/src/utils/keys.ts (4)
  • loadEd25519SignerFromSeed (11-18)
  • signJson (27-36)
  • verifyJsonSignature (39-49)
  • loadEd25519VerifierFromPublicKey (20-25)
packages/crypto/src/keys/ed25519.ts (2)
packages/crypto/src/contracts/key.ts (2)
  • VerifierKey (10-23)
  • Signer (27-32)
packages/crypto/src/rfc/8410/ed25519-pem.ts (2)
  • ed25519PublicKeyRawToPem (76-88)
  • ed25519PrivateKeyRawToPem (38-63)
packages/crypto/src/contracts/key.ts (1)
packages/crypto/src/utils/constants.ts (2)
  • DataType (5-5)
  • SignatureType (7-7)
packages/crypto/src/index.spec.ts (1)
packages/core/src/utils/signJson.ts (2)
  • signJson (17-50)
  • verifyJsonSignature (115-134)
🔇 Additional comments (3)
packages/crypto/src/contracts/key.ts (1)

1-33: LGTM! Well-structured key interfaces.

The interfaces clearly define the cryptographic key abstractions with proper inheritance structure and documentation.

packages/crypto/src/README.md (1)

1-49: Well-written documentation with clear explanations.

The README provides excellent documentation covering the package structure, interfaces, and utilities with clear explanations of the separation of concerns.

packages/crypto/src/index.ts (1)

15-20: Canonicalization confirmed: signJson and verifyJsonSignature both call encodeCanonicalJson

Both functions call encodeCanonicalJson(jsonObject) in packages/crypto/src/utils/keys.ts (signJson at line 31; verifyJsonSignature at line 44).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
packages/crypto/src/rfc/8410/ed25519-pem.ts (1)

16-18: PEM wrapping fix looks correct.

substring(i, i + 64) resolves the earlier line‑wrap bug. Good catch.

🧹 Nitpick comments (5)
packages/crypto/src/rfc/8410/ed25519-pem.ts (5)

34-36: Correct ASN.1 comment.

Typo and structure: it should reference CurvePrivateKey and OCTET STRING.

-   CTET
-   PrivateKey ::= OCTET { OCTET STRING }
+   CurvePrivateKey ::= OCTET STRING

71-74: RFC 8410: parameters must be ABSENT, not NULL (comment fix).

Align the comment with the implementation (you already omit NULL in algorithmIdentifierTlv).

-	  parameters NULL
+	  parameters ABSENT  -- per RFC 8410, parameters MUST be absent

82-83: Comment typo.

subhjectPublicKeysubjectPublicKey.


77-79: Confirm input size expectations (32 vs 64 byte keys).

Some libs hand around 64‑byte Ed25519 “secret keys” (seed+pub). Your check enforces 32 bytes (seed) per RFC 8410, which is correct. Verify callers won’t pass 64 bytes; if they might, slice the first 32 with a warning.


60-60: Make base64 encoding isomorphic (avoid Node-only Buffer)

Buffer.from(...).toString('base64') is Node-only and will break browser builds without a Buffer polyfill — add a small isomorphic helper and replace both call sites in packages/crypto/src/rfc/8410/ed25519-pem.ts (ed25519PrivateKeyRawToPem and ed25519PublicKeyRawToPem).

Example helper:

function bytesToBase64(data: Uint8Array): string {
	if (typeof Buffer !== 'undefined') return Buffer.from(data).toString('base64');
	if (typeof btoa !== 'undefined') {
		let s = '';
		for (let i = 0; i < data.length; i++) s += String.fromCharCode(data[i]);
		return btoa(s);
	}
	throw new Error('No base64 encoder available');
}

Example replacements:

-Buffer.from(oneAsymmetricKey).toString('base64')
+bytesToBase64(oneAsymmetricKey)
-Buffer.from(spki).toString('base64')
+bytesToBase64(spki)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bcd6d15 and 716481f.

📒 Files selected for processing (1)
  • packages/crypto/src/rfc/8410/ed25519-pem.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
packages/crypto/src/rfc/8410/ed25519-pem.ts (1)
packages/crypto/src/der/index.ts (5)
  • privateKeyVersionTlv (30-34)
  • algorithmIdentifierTlv (108-111)
  • octetStringTlv (89-106)
  • sequenceOrderedTlv (58-86)
  • bitStringTlv (114-126)
🔇 Additional comments (2)
packages/crypto/src/rfc/8410/ed25519-pem.ts (2)

38-63: PKCS#8 encoding looks correct for Ed25519.

  • Version=0, AlgorithmIdentifier with OID and parameters absent, and privateKey as OCTET STRING of CurvePrivateKey (nested OCTET STRING) matches RFC 8410/5958.

Please confirm algorithmIdentifierTlv encodes OID 1.3.101.112 with parameters absent (as suggested by your DER helpers).


80-86: SPKI structure is correct.

AlgId + BIT STRING(public key), unused bits = 0, is per RFC 8410.

Comment on lines +9 to +12
enum KeyType {
private = 'PRIVATE KEY',
public = 'PUBLIC KEY',
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix enum: reserved identifiers and private type leak in exported signature.

  • private/public as enum members are reserved identifiers and will fail to compile.
  • toPem exports a signature using a non‑exported KeyType, which will break type declarations.

Apply this diff to use an inline string‑union and avoid the enum altogether:

-enum KeyType {
-	private = 'PRIVATE KEY',
-	public = 'PUBLIC KEY',
-}
-
-export function toPem(base64: string, type: KeyType): string {
+export function toPem(
+	base64: string,
+	type: 'PRIVATE KEY' | 'PUBLIC KEY',
+): string {
-	return toPem(
-		Buffer.from(oneAsymmetricKey).toString('base64'),
-		KeyType.private,
-	);
+	return toPem(
+		Buffer.from(oneAsymmetricKey).toString('base64'),
+		'PRIVATE KEY',
+	);
-	return toPem(Buffer.from(spki).toString('base64'), KeyType.public);
+	return toPem(Buffer.from(spki).toString('base64'), 'PUBLIC KEY');

Also applies to: 14-14, 61-62, 87-87

🤖 Prompt for AI Agents
In packages/crypto/src/rfc/8410/ed25519-pem.ts around lines 9-12 (and also
touching lines 14, 61-62, 87), replace the private/public enum with an exported
inline string-union type (e.g. export type KeyType = 'PRIVATE KEY' | 'PUBLIC
KEY') and update all references to use that exported KeyType so compilation and
.d.ts generation succeed; remove the enum declaration and any uses of the
non-exported enum members, and ensure the toPem signature and any exported
functions/values reference the exported string-union type instead.

// SubjectPublicKeyInfo -> SEQUENCE
const spki = sequenceOrderedTlv([algId, pubKeyBitString]);

return toPem(Buffer.from(spki).toString('base64'), KeyType.public);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Same note as above for public key base64.

Swap to the isomorphic helper here as well if you adopt it.

🤖 Prompt for AI Agents
In packages/crypto/src/rfc/8410/ed25519-pem.ts around line 87, replace the
Node-specific Buffer.from(spki).toString('base64') call with the project’s
isomorphic base64 helper (e.g., base64Encode or the agreed helper) and pass its
result to toPem(..., KeyType.public); also add the corresponding import for that
helper at the top of the file so the function works in both browser and Node
environments.

@codecov-commenter
Copy link

Codecov Report

❌ Patch coverage is 81.31868% with 51 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.41%. Comparing base (5f43238) to head (672f1d8).

Files with missing lines Patch % Lines
packages/crypto/src/utils/data-types.ts 67.12% 24 Missing ⚠️
packages/crypto/src/der/index.ts 84.50% 11 Missing ⚠️
packages/crypto/src/keys/ed25519.ts 87.93% 7 Missing ⚠️
packages/crypto/src/utils/constants.ts 22.22% 7 Missing ⚠️
packages/crypto/src/rfc/8410/ed25519-pem.ts 93.93% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #190      +/-   ##
==========================================
+ Coverage   68.48%   69.41%   +0.92%     
==========================================
  Files          58       62       +4     
  Lines        5210     5319     +109     
==========================================
+ Hits         3568     3692     +124     
+ Misses       1642     1627      -15     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (3)
packages/crypto/src/utils/utils.spec.ts (3)

25-31: Don’t make describe async; move async setup to beforeAll.

Async describe breaks test discovery. Initialize signer in beforeAll instead.

-describe('Signing and verifying payloads', async () => {
-	const seedFileContent =
-		'ed25519 a_XRhW YjbSyfqQeGto+OFswt+XwtJUUooHXH5w+czSgawN63U';
-
-	const signer = await getSignerFromKeyContent(seedFileContent);
+describe('Signing and verifying payloads', () => {
+	const seedFileContent =
+		'ed25519 a_XRhW YjbSyfqQeGto+OFswt+XwtJUUooHXH5w+czSgawN63U';
+	let signer: Awaited<ReturnType<typeof getSignerFromKeyContent>>;
+	beforeAll(async () => {
+		signer = await getSignerFromKeyContent(seedFileContent);
+	});

102-104: Await (and loosen) the rejection assertion.

Await the promise and avoid brittle message matching.

-		expect(verifyJsonSignature({}, signature, verifier)).rejects.toThrowError(
-			'Invalid signature',
-		);
+		await expect(verifyJsonSignature({}, signature, verifier)).rejects.toBeDefined();

85-86: Await the .resolves assertion.

Without await (or return), the test can false‑pass.

-		expect(verifyJsonSignature({}, signature, signer)).resolves;
+		await expect(verifyJsonSignature({}, signature, signer)).resolves.toBeUndefined();
🧹 Nitpick comments (5)
packages/crypto/src/utils/utils.spec.ts (5)

1-1: Import beforeAll.

-import { describe, expect, it } from 'bun:test';
+import { describe, expect, it, beforeAll } from 'bun:test';

13-22: Harden key-content parsing; avoid non-null assertion.

Trim, split on any whitespace, validate presence, and drop the temporary comment.

-async function getSignerFromKeyContent(content: string) {
-	// biome-ignore lint/style/noNonNullAssertion: I can see pop won't fail, input isn't unknown -__-
-	const seed = content.split(' ').pop()!;
-	const seedBytes = fromBase64ToBytes(seed);
-
-	//vvv
-	const signer = await loadEd25519SignerFromSeed(seedBytes);
-
-	return signer;
-}
+async function getSignerFromKeyContent(content: string) {
+	const tokens = content.trim().split(/\s+/);
+	const seed = tokens.at(-1);
+	if (!seed) {
+		throw new Error('Invalid key content: missing seed base64');
+	}
+	const seedBytes = fromBase64ToBytes(seed);
+	return loadEd25519SignerFromSeed(seedBytes);
+}

76-86: Prefer verifying with a verifier key, not the signer instance.

If accessible, derive a verifier from the public key to decouple tests from the signing interface.

If signer exposes a public key, switch to:

const verifier = await loadEd25519VerifierFromPublicKey(signer.publicKey, '0');
await expect(verifyJsonSignature({}, signature, verifier)).resolves.toBeUndefined();

Confirm whether Ed25519SigningKeyImpl exposes publicKey (or an equivalent getter).


294-296: Remove duplicate test case.

This entry duplicates the case at lines 269–272.

-			{
-				input: { 'key\nnewLine': 1, 'key"quote': 2, 'key\\backslash': 3 },
-				expected: '{"key\\nnewLine":1,"key\\"quote":2,"key\\\\backslash":3}',
-			},

10-12: Prune temporary comments.

Drop the porting notes and stray //vvv.

Also applies to: 18-18

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 716481f and 672f1d8.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • packages/core/src/events/m.room.message.edit.spec.ts (0 hunks)
  • packages/core/src/events/m.room.message.spec.ts (0 hunks)
  • packages/crypto/src/index.spec.ts (3 hunks)
  • packages/crypto/src/utils/data-types.ts (1 hunks)
  • packages/crypto/src/utils/utils.spec.ts (1 hunks)
💤 Files with no reviewable changes (2)
  • packages/core/src/events/m.room.message.spec.ts
  • packages/core/src/events/m.room.message.edit.spec.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/crypto/src/index.spec.ts
  • packages/crypto/src/utils/data-types.ts
🧰 Additional context used
🧬 Code graph analysis (1)
packages/crypto/src/utils/utils.spec.ts (2)
packages/crypto/src/utils/data-types.ts (2)
  • fromBase64ToBytes (102-104)
  • encodeCanonicalJson (63-100)
packages/crypto/src/utils/keys.ts (4)
  • loadEd25519SignerFromSeed (11-18)
  • signJson (27-36)
  • verifyJsonSignature (39-49)
  • loadEd25519VerifierFromPublicKey (20-25)

@debdutdeb debdutdeb marked this pull request as ready for review September 15, 2025 13:07
@ggazzo ggazzo merged commit 5a496a7 into main Sep 15, 2025
3 checks passed
@ggazzo ggazzo deleted the fdr108-crypto-package branch September 15, 2025 19:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants