diff --git a/bun.lock b/bun.lock index d8c4e97c6..5eea50d60 100644 --- a/bun.lock +++ b/bun.lock @@ -10,7 +10,6 @@ "rollup": "^4.52.4", "rollup-plugin-dts": "^6.2.3", "tsyringe": "^4.10.0", - "tweetnacl": "^1.0.3", }, "devDependencies": { "@biomejs/biome": "^1.9.4", @@ -23,6 +22,7 @@ "sinon": "^20.0.0", "tsconfig-paths": "^4.2.0", "turbo": "~2.5.6", + "tweetnacl": "^1.0.3", "typescript": "~5.9.2", }, }, @@ -54,7 +54,7 @@ }, "packages/federation-sdk": { "name": "@rocket.chat/federation-sdk", - "version": "0.3.0", + "version": "0.3.1", "dependencies": { "@rocket.chat/emitter": "^0.31.25", "@rocket.chat/federation-core": "workspace:*", @@ -63,7 +63,6 @@ "mongodb": "^6.16.0", "reflect-metadata": "^0.2.2", "tsyringe": "^4.10.0", - "tweetnacl": "^1.0.3", "zod": "^3.24.1", }, "peerDependencies": { @@ -291,7 +290,7 @@ "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], - "bun-types": ["bun-types@1.3.1", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw=="], + "bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="], "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], diff --git a/bun.lockb b/bun.lockb index 3fff2edf7..99cc1f0f1 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/bundle.ts b/bundle.ts index 3eff95e88..5c44e7948 100644 --- a/bundle.ts +++ b/bundle.ts @@ -115,7 +115,6 @@ bun build ./packages/federation-sdk/src/index.ts \ -e @rocket.chat/emitter \ -e reflect-metadata \ -e tsyringe \ - -e tweetnacl \ --production \ --sourcemap=inline */ diff --git a/package.json b/package.json index c9f99df21..7b6b7d4ab 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "sinon": "^20.0.0", "tsconfig-paths": "^4.2.0", "turbo": "~2.5.6", - "typescript": "~5.9.2" + "typescript": "~5.9.2", + "tweetnacl": "^1.0.3" }, "workspaces": ["packages/*"], "dependencies": { @@ -23,8 +24,7 @@ "reflect-metadata": "^0.2.2", "rollup": "^4.52.4", "rollup-plugin-dts": "^6.2.3", - "tsyringe": "^4.10.0", - "tweetnacl": "^1.0.3" + "tsyringe": "^4.10.0" }, "husky": { "hooks": { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 73e615ba5..4b3d53679 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -8,14 +8,12 @@ export { signEvent } from './utils/signEvent'; // Authentication utilities export { generateId } from './utils/generateId'; export { pruneEventDict } from './utils/pruneEventDict'; -export { checkSignAndHashes } from './utils/checkSignAndHashes'; export { authorizationHeaders, computeAndMergeHash, computeHash, extractSignaturesFromHeader, signRequest, - validateAuthorizationHeader, type HashedEvent, } from './utils/authentication'; @@ -24,12 +22,6 @@ export type { ProtocolVersionKey, SignedJson } from './utils/signJson'; export { signJson, isValidAlgorithm, - getSignaturesFromRemote, - verifySignature, - verifyJsonSignature, - verifySignaturesFromRemote, - signText, - signData, } from './utils/signJson'; // Binary data utilities @@ -72,7 +64,6 @@ export * from './models/event.model'; // Procedures export { makeJoinEventBuilder } from './procedures/makeJoin'; -export { getPublicKeyFromRemoteServer } from './procedures/getPublicKeyFromServer'; export { createLogger, logger } from './utils/logger'; diff --git a/packages/core/src/procedures/getPublicKeyFromServer.spec.ts b/packages/core/src/procedures/getPublicKeyFromServer.spec.ts deleted file mode 100644 index 4da5e4aa0..000000000 --- a/packages/core/src/procedures/getPublicKeyFromServer.spec.ts +++ /dev/null @@ -1,232 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it } from 'bun:test'; -import { encodeCanonicalJson } from '@rocket.chat/federation-crypto'; -import nacl from 'tweetnacl'; -import { EncryptionValidAlgorithm } from '../types'; -import { generateKeyPairs } from '../utils/keys'; -import { getPublicKeyFromRemoteServer } from './getPublicKeyFromServer'; - -describe('getPublicKeyFromRemoteServer', () => { - let originalFetch: typeof globalThis.fetch; - let mockServerKeys: { - server_name: string; - verify_keys: Record; - old_verify_keys: Record; - signatures: Record>; - valid_until_ts: number; - }; - - const createValidServerKeyResponse = async () => { - const seed = nacl.randomBytes(32); - const keyPair = await generateKeyPairs( - seed, - EncryptionValidAlgorithm.ed25519, - 'test_key', - ); - const serverName = 'test.server'; - const keyId = `${keyPair.algorithm}:${keyPair.version}`; - const validUntil = Date.now() + 24 * 60 * 60 * 1000; - - const serverKey = { - server_name: serverName, - old_verify_keys: {}, - valid_until_ts: validUntil, - verify_keys: { - [keyId]: { - key: Buffer.from(keyPair.publicKey).toString('base64'), - }, - }, - }; - - const canonicalJson = encodeCanonicalJson(serverKey); - const signature = await keyPair.sign( - new TextEncoder().encode(canonicalJson), - ); - - return { - ...serverKey, - signatures: { - [serverName]: { - [keyId]: Buffer.from(signature).toString('base64'), - }, - }, - }; - }; - - beforeEach(async () => { - originalFetch = globalThis.fetch; - mockServerKeys = await createValidServerKeyResponse(); - - const mockFetch = async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes('/_matrix/key/v2/server')) { - return new Response(JSON.stringify(mockServerKeys), { - status: 200, - headers: { 'Content-Type': 'application/json' }, - }); - } - return new Response(null, { status: 404 }); - }; - mockFetch.preconnect = async () => undefined; - globalThis.fetch = mockFetch; - }); - - afterEach(() => { - globalThis.fetch = originalFetch; - }); - - it('should successfully retrieve and validate a public key', async () => { - const domain = 'test.server'; - const origin = 'test.server'; - const algorithmAndVersion = `${EncryptionValidAlgorithm.ed25519}:test_key`; - - const result = await getPublicKeyFromRemoteServer( - domain, - origin, - algorithmAndVersion, - ); - - expect(result).toBeDefined(); - expect(result.key).toBeDefined(); - expect(result.validUntil).toBeNumber(); - expect(result.validUntil).toBeGreaterThan(Date.now()); - }); - - it('should throw error when server key is expired', async () => { - mockServerKeys.valid_until_ts = Date.now() - 1000; - - await expect( - getPublicKeyFromRemoteServer( - 'test.server', - 'test.server', - `${EncryptionValidAlgorithm.ed25519}:test_key`, - ), - ).rejects.toThrow('Expired remote public key'); - }); - - it('should throw error when public key is not found', async () => { - mockServerKeys.verify_keys = {}; - - await expect( - getPublicKeyFromRemoteServer( - 'test.server', - 'test.server', - `${EncryptionValidAlgorithm.ed25519}:test_key`, - ), - ).rejects.toThrow('Public key not found'); - }); - - it('should throw error for invalid algorithm', async () => { - await expect( - getPublicKeyFromRemoteServer( - 'test.server', - 'test.server', - 'invalid_alg:test_key', - ), - ).rejects.toThrow('Invalid algorithm'); - }); - - it('should throw error for invalid algorithm format', async () => { - await expect( - getPublicKeyFromRemoteServer( - 'test.server', - 'test.server', - 'invalid_algorithm_format', - ), - ).rejects.toThrow('Invalid algorithm and version format'); - }); - - it('should handle network errors gracefully', async () => { - const errorFetch = async () => { - throw new Error('Network error'); - }; - errorFetch.preconnect = async () => undefined; - globalThis.fetch = errorFetch; - - await expect( - getPublicKeyFromRemoteServer( - 'test.server', - 'test.server', - `${EncryptionValidAlgorithm.ed25519}:test_key`, - ), - ).rejects.toThrow(); - }); - - it('should handle invalid server response', async () => { - const invalidJsonFetch = async () => { - return new Response('invalid json', { - status: 200, - headers: { 'Content-Type': 'application/json' }, - }); - }; - invalidJsonFetch.preconnect = async () => undefined; - globalThis.fetch = invalidJsonFetch; - - await expect( - getPublicKeyFromRemoteServer( - 'test.server', - 'test.server', - `${EncryptionValidAlgorithm.ed25519}:test_key`, - ), - ).rejects.toThrow(); - }); - - it('should successfully retrieve and validate a public key with different algorithm version', async () => { - const seed = nacl.randomBytes(32); - const keyPair = await generateKeyPairs( - seed, - EncryptionValidAlgorithm.ed25519, - 'another_version', - ); - const serverName = 'test.server'; - const keyId = `${keyPair.algorithm}:${keyPair.version}`; - - const specialMockFetch = async (input: RequestInfo | URL) => { - const url = input.toString(); - if (url.includes('/_matrix/key/v2/server')) { - const baseServerKey = { - server_name: serverName, - old_verify_keys: {}, - valid_until_ts: Date.now() + 24 * 60 * 60 * 1000, - verify_keys: { - [keyId]: { - key: Buffer.from(keyPair.publicKey).toString('base64'), - }, - }, - }; - - const canonicalJson = encodeCanonicalJson(baseServerKey); - const signature = await keyPair.sign( - new TextEncoder().encode(canonicalJson), - ); - - const fullServerKey = { - ...baseServerKey, - signatures: { - [serverName]: { - [keyId]: Buffer.from(signature).toString('base64'), - }, - }, - }; - - return new Response(JSON.stringify(fullServerKey), { - status: 200, - headers: { 'Content-Type': 'application/json' }, - }); - } - return new Response(null, { status: 404 }); - }; - specialMockFetch.preconnect = async () => undefined; - globalThis.fetch = specialMockFetch; - - const result = await getPublicKeyFromRemoteServer( - serverName, - serverName, - keyId, - ); - - expect(result).toBeDefined(); - expect(result.key).toBeDefined(); - expect(result.validUntil).toBeNumber(); - expect(result.validUntil).toBeGreaterThan(Date.now()); - }); -}); diff --git a/packages/core/src/procedures/getPublicKeyFromServer.ts b/packages/core/src/procedures/getPublicKeyFromServer.ts deleted file mode 100644 index 0819a7a1b..000000000 --- a/packages/core/src/procedures/getPublicKeyFromServer.ts +++ /dev/null @@ -1,69 +0,0 @@ -import type { ServerKey } from '../server'; -import { makeRequest } from '../utils/makeRequest'; - -import { - getSignaturesFromRemote, - isValidAlgorithm, - verifyJsonSignature, -} from '../utils/signJson'; - -export const getPublicKeyFromRemoteServer = async ( - domain: string, - origin: string, - algorithmAndVersion: string, -) => { - const [algorithm, version] = algorithmAndVersion.split(':'); - if (!algorithm || !version) { - throw new Error('Invalid algorithm and version format'); - } - - if (!isValidAlgorithm(algorithm)) { - throw new Error('Invalid algorithm'); - } - - const result = await makeRequest({ - method: 'GET', - domain, - uri: '/_matrix/key/v2/server', - signingName: origin, - }); - - if (result.valid_until_ts < Date.now()) { - throw new Error('Expired remote public key'); - } - - const publickey = result.verify_keys[algorithmAndVersion]?.key; - if (!publickey) { - throw new Error('Public key not found'); - } - - const [signature] = await getSignaturesFromRemote(result, domain); - if (!signature) { - throw new Error(`No valid signature found for ${domain}`); - } - - const publicKeyBytes = Uint8Array.from(atob(publickey), (c) => - c.charCodeAt(0), - ); - const signatureBytes = Uint8Array.from(atob(signature.signature), (c) => - c.charCodeAt(0), - ); - - if ( - !verifyJsonSignature( - result, - domain, - signatureBytes, - publicKeyBytes, - signature.algorithm, - signature.version, - ) - ) { - throw new Error('Invalid signature'); - } - - return { - key: publickey, - validUntil: result.valid_until_ts, - }; -}; diff --git a/packages/core/src/utils/authentication.spec.ts b/packages/core/src/utils/authentication.spec.ts index 35d8b09c2..97651995c 100644 --- a/packages/core/src/utils/authentication.spec.ts +++ b/packages/core/src/utils/authentication.spec.ts @@ -6,7 +6,6 @@ import { computeHash, extractSignaturesFromHeader, signRequest, - validateAuthorizationHeader, } from './authentication'; import { generateId } from './generateId'; diff --git a/packages/core/src/utils/authentication.ts b/packages/core/src/utils/authentication.ts index abbe99e3d..d518a9e16 100644 --- a/packages/core/src/utils/authentication.ts +++ b/packages/core/src/utils/authentication.ts @@ -1,6 +1,4 @@ -import { encodeCanonicalJson } from '@rocket.chat/federation-crypto'; import { type Pdu, PersistentEventBase } from '@rocket.chat/federation-room'; -import nacl from 'tweetnacl'; import { type SigningKey } from '../types'; import { signJson } from './signJson'; @@ -66,45 +64,6 @@ export async function authorizationHeaders( return `X-Matrix origin="${origin}",destination="${destination}",key="${key}",sig="${signed}"`; } -export const validateAuthorizationHeader = async ( - origin: string, - signingKey: string, - destination: string, - method: string, - uri: string, - hash: string, - content?: T, -) => { - const canonicalJson = encodeCanonicalJson({ - method, - uri, - origin, - destination, - ...(content && { content }), - }); - - const signature = Uint8Array.from(atob(hash as string), (c) => - c.charCodeAt(0), - ); - const signingKeyBytes = Uint8Array.from(atob(signingKey as string), (c) => - c.charCodeAt(0), - ); - const messageBytes = new TextEncoder().encode(canonicalJson); - const isValid = nacl.sign.detached.verify( - messageBytes, - signature, - signingKeyBytes, - ); - - if (!isValid) { - throw new Error( - `Invalid signature from ${origin} for request to ${destination}`, - ); - } - - return true; -}; - export async function signRequest( origin: string, signingKey: SigningKey, diff --git a/packages/core/src/utils/checkSignAndHashes.spec.ts b/packages/core/src/utils/checkSignAndHashes.spec.ts deleted file mode 100644 index 1268e5cb3..000000000 --- a/packages/core/src/utils/checkSignAndHashes.spec.ts +++ /dev/null @@ -1,164 +0,0 @@ -import 'reflect-metadata'; -import { afterAll, beforeAll, describe, expect, it, spyOn } from 'bun:test'; -import type { EventBase } from '../events/eventBase'; -import { EncryptionValidAlgorithm } from '../types'; -import * as authentication from '../utils/authentication'; -import type { HashedEvent } from './authentication'; -import { checkSignAndHashes } from './checkSignAndHashes'; -import { MatrixError } from './errors'; -import * as signJson from './signJson'; -import type { SignedJson } from './signJson'; - -describe('checkSignAndHashes', () => { - const originalAtob = globalThis.atob; - - const mockOrigin = 'example.com'; - const mockSignature = { - algorithm: EncryptionValidAlgorithm.ed25519, - version: 'key_version', - signature: 'bW9ja1NpZ25hdHVyZQ==', - }; - const mockPublicKey = 'bW9ja1B1YmxpY0tleQ=='; - const mockHash = 'mockHash'; - - const mockPdu = { - type: 'm.room.message', - content: { body: 'Hello' }, - hashes: { - sha256: mockHash, - }, - signatures: { - [mockOrigin]: { - 'ed25519:key_version': 'someSignature', - }, - }, - } as unknown as HashedEvent>; - - const getPublicKeyFromServerMock = ( - _origin: string, - _key: string, - ): Promise => { - return Promise.resolve(mockPublicKey); - }; - - beforeAll(() => { - globalThis.atob = (str: string): string => { - if (str === mockSignature.signature) { - return 'mockSignature'; - } - if (str === mockPublicKey) { - return 'mockPublicKey'; - } - - return originalAtob(str); - }; - }); - - afterAll(() => { - globalThis.atob = originalAtob; - }); - - it('should validate signature and hash successfully', async () => { - const getSignaturesSpy = spyOn( - signJson, - 'getSignaturesFromRemote', - ).mockResolvedValue([mockSignature]); - const verifyJsonSpy = spyOn( - signJson, - 'verifyJsonSignature', - ).mockReturnValue(true); - const computeHashSpy = spyOn(authentication, 'computeHash').mockReturnValue( - ['sha256', mockHash], - ); - - const result = await checkSignAndHashes( - mockPdu, - mockOrigin, - getPublicKeyFromServerMock, - ); - - expect(getSignaturesSpy).toHaveBeenCalledWith(mockPdu, mockOrigin); - expect(verifyJsonSpy).toHaveBeenCalled(); - expect(computeHashSpy).toHaveBeenCalledWith(mockPdu); - - expect(result).toEqual(mockPdu); - - getSignaturesSpy.mockRestore(); - verifyJsonSpy.mockRestore(); - computeHashSpy.mockRestore(); - }); - - it('should throw error for invalid signature', async () => { - const getSignaturesSpy = spyOn( - signJson, - 'getSignaturesFromRemote', - ).mockResolvedValue([mockSignature]); - const verifyJsonSpy = spyOn( - signJson, - 'verifyJsonSignature', - ).mockReturnValue(false); - const computeHashSpy = spyOn(authentication, 'computeHash').mockReturnValue( - ['sha256', mockHash], - ); - - let error: Error | undefined; - try { - await checkSignAndHashes(mockPdu, mockOrigin, getPublicKeyFromServerMock); - } catch (e) { - error = e as Error; - } - - expect(error).toBeInstanceOf(MatrixError); - expect(error?.message).toBe('Invalid signature'); - - getSignaturesSpy.mockRestore(); - verifyJsonSpy.mockRestore(); - computeHashSpy.mockRestore(); - }); - - it('should throw error for invalid hash', async () => { - const getSignaturesSpy = spyOn( - signJson, - 'getSignaturesFromRemote', - ).mockResolvedValue([mockSignature]); - const verifyJsonSpy = spyOn( - signJson, - 'verifyJsonSignature', - ).mockReturnValue(true); - const computeHashSpy = spyOn(authentication, 'computeHash').mockReturnValue( - ['sha256', 'differentHash'], - ); - - let error: Error | undefined; - try { - await checkSignAndHashes(mockPdu, mockOrigin, getPublicKeyFromServerMock); - } catch (e) { - error = e as Error; - } - getSignaturesSpy.mockRestore(); - verifyJsonSpy.mockRestore(); - computeHashSpy.mockRestore(); - - expect(error).toBeInstanceOf(MatrixError); - expect(error?.message).toBe('Invalid hash'); - }); - - it('should throw error if signature verification fails', async () => { - const getSignaturesSpy = spyOn(signJson, 'getSignaturesFromRemote'); - getSignaturesSpy.mockImplementation(() => { - throw new Error('Signature not found'); - }); - - let error: Error | undefined; - try { - await checkSignAndHashes(mockPdu, mockOrigin, getPublicKeyFromServerMock); - } catch (e) { - error = e as Error; - } - - expect(error).toBeInstanceOf(Error); - expect(error?.message).toBe('Signature not found'); - - getSignaturesSpy.mockRestore(); - }); -}); diff --git a/packages/core/src/utils/checkSignAndHashes.ts b/packages/core/src/utils/checkSignAndHashes.ts deleted file mode 100644 index 58171d0a4..000000000 --- a/packages/core/src/utils/checkSignAndHashes.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Pdu } from '@rocket.chat/federation-room'; -import { type HashedEvent, computeHash } from './authentication'; -import { MatrixError } from './errors'; -import { logger } from './logger'; -import { pruneEventDict } from './pruneEventDict'; -import { - type SignedJson, - getSignaturesFromRemote, - verifyJsonSignature, -} from './signJson'; - -export async function checkSignAndHashes>( - pdu: HashedEvent, - origin: string, - getPublicKeyFromServer: (origin: string, key: string) => Promise, -) { - const [signature] = await getSignaturesFromRemote(pdu, origin); - const publicKey = await getPublicKeyFromServer( - origin, - `${signature.algorithm}:${signature.version}`, - ); - - if ( - !verifyJsonSignature( - pruneEventDict(pdu), - origin, - Uint8Array.from(atob(signature.signature), (c) => c.charCodeAt(0)), - Uint8Array.from(atob(publicKey), (c) => c.charCodeAt(0)), - signature.algorithm, - signature.version, - ) - ) { - throw new MatrixError('400', 'Invalid signature'); - } - - const { - hashes: { sha256: expectedHash }, - } = pdu; - - const [, hash] = computeHash(pdu); - - if (hash !== expectedHash) { - logger.error({ msg: 'Invalid hash', hash, expectedHash }); - throw new MatrixError('400', 'Invalid hash'); - } - - return pdu; -} diff --git a/packages/core/src/utils/keys.spec.ts b/packages/core/src/utils/keys.spec.ts index db5e40cc7..ab8e2a36c 100644 --- a/packages/core/src/utils/keys.spec.ts +++ b/packages/core/src/utils/keys.spec.ts @@ -1,13 +1,7 @@ -import { afterEach, beforeEach, describe, expect, it, spyOn } from 'bun:test'; -import fs from 'node:fs/promises'; +import { describe, expect, it } from 'bun:test'; import nacl from 'tweetnacl'; import { EncryptionValidAlgorithm } from '../types'; -import { toUnpaddedBase64 } from './binaryData'; -import { - generateKeyPairs, - generateKeyPairsFromString, - getKeyPair, -} from './keys'; +import { generateKeyPairs, generateKeyPairsFromString } from './keys'; describe('keys', () => { describe('generateKeyPairs', () => { @@ -65,61 +59,4 @@ describe('keys', () => { expect(keyPair.privateKey).toEqual(expectedKeyPair.secretKey); }); }); - - describe('getKeyPair', () => { - const signingKeyPath = '/tmp/test-signing.key'; - let readFileSpy: ReturnType; - let writeFileSpy: ReturnType; - - beforeEach(() => { - readFileSpy = spyOn(fs, 'readFile'); - writeFileSpy = spyOn(fs, 'writeFile').mockResolvedValue(undefined); - }); - - afterEach(async () => { - readFileSpy.mockRestore(); - writeFileSpy.mockRestore(); - }); - - it('should generate and store new key pairs if file does not exist', async () => { - readFileSpy.mockRejectedValue(new Error('File not found')); - - const keyPairs = await getKeyPair({ signingKeyPath }); - - expect(keyPairs.length).toBe(1); - - const keyPair = keyPairs[0]; - - expect(keyPair.algorithm).toBe(EncryptionValidAlgorithm.ed25519); - expect(keyPair.version).toBe('0'); - expect(writeFileSpy).toHaveBeenCalledTimes(1); - - const writeCallArg = writeFileSpy.mock.calls[0][1] as string; - - expect(writeCallArg.startsWith('ed25519 0 ')).toBe(true); - }); - - it('should load key pairs from existing file', async () => { - const seed = nacl.randomBytes(nacl.sign.seedLength); - const seedString = toUnpaddedBase64(seed); - const fileContent = `${EncryptionValidAlgorithm.ed25519} 1 ${seedString}`; - - readFileSpy.mockResolvedValue(fileContent); - - const keyPairs = await getKeyPair({ signingKeyPath }); - - expect(keyPairs.length).toBe(1); - - const keyPair = keyPairs[0]; - - expect(keyPair.algorithm).toBe(EncryptionValidAlgorithm.ed25519); - expect(keyPair.version).toBe('1'); - - const expectedKeyPair = await nacl.sign.keyPair.fromSeed(seed); - - expect(keyPair.publicKey).toEqual(expectedKeyPair.publicKey); - expect(keyPair.privateKey).toEqual(expectedKeyPair.secretKey); - expect(writeFileSpy).not.toHaveBeenCalled(); - }); - }); }); diff --git a/packages/core/src/utils/keys.ts b/packages/core/src/utils/keys.ts index 39fc9bf3c..9cab5c317 100644 --- a/packages/core/src/utils/keys.ts +++ b/packages/core/src/utils/keys.ts @@ -1,9 +1,20 @@ -import fs from 'node:fs/promises'; import nacl from 'tweetnacl'; import { EncryptionValidAlgorithm } from '../types'; import type { SigningKey } from '../types'; -import { toUnpaddedBase64 } from './binaryData'; -import { signData } from './signJson'; + +// these functions are used by tests only + +async function signData( + data: string | Uint8Array, + signingKey: Uint8Array, +): Promise { + const signature = nacl.sign.detached( + typeof data === 'string' ? new TextEncoder().encode(data) : data, + signingKey, + ); + + return signature; +} export async function generateKeyPairs( seed: Uint8Array, @@ -36,57 +47,5 @@ export async function generateKeyPairsFromString(content: string) { ); } -async function storeKeyPairs( - seeds: { - algorithm: string; - version: string; - seed: Uint8Array; - }[], - path: string, -) { - for await (const keyPair of seeds) { - await fs.writeFile( - path, - `${keyPair.algorithm} ${keyPair.version} ${toUnpaddedBase64(keyPair.seed)}`, - ); - } -} - -export const getKeyPair = async (config: { - signingKeyPath: string; -}): Promise => { - const { signingKeyPath } = config; - - const seeds = []; - - const existingKeyContent = await fs - .readFile(signingKeyPath, 'utf8') - .catch(() => null); - - if (existingKeyContent) { - const [algorithm, version, seed] = existingKeyContent.trim().split(' '); - seeds.push({ - algorithm: algorithm as EncryptionValidAlgorithm, - version, - seed: Uint8Array.from(atob(seed), (c) => c.charCodeAt(0)), - }); - } else { - seeds.push({ - algorithm: 'ed25519' as EncryptionValidAlgorithm, - version: '0', - seed: nacl.randomBytes(32), - }); - - await storeKeyPairs(seeds, signingKeyPath); - } - - return Promise.all( - seeds.map( - async (seed) => - await generateKeyPairs(seed.seed, seed.algorithm, seed.version), - ), - ); -}; - export const convertSigningKeyToBase64 = (signingKey: SigningKey): string => `${signingKey.algorithm} ${signingKey.version} ${Buffer.from(signingKey.privateKey.slice(0, 32)).toString('base64')}`; diff --git a/packages/core/src/utils/signJson.ts b/packages/core/src/utils/signJson.ts index dc192b85a..779485c31 100644 --- a/packages/core/src/utils/signJson.ts +++ b/packages/core/src/utils/signJson.ts @@ -1,5 +1,4 @@ import { encodeCanonicalJson } from '@rocket.chat/federation-crypto'; -import nacl from 'tweetnacl'; import type { SigningKey } from '../types'; import { EncryptionValidAlgorithm } from '../types'; import { toBinaryData, toUnpaddedBase64 } from './binaryData'; @@ -54,144 +53,3 @@ export const isValidAlgorithm = ( ): algorithm is EncryptionValidAlgorithm => { return Object.values(EncryptionValidAlgorithm).includes(algorithm as any); }; - -export async function getSignaturesFromRemote< - T extends object & { - signatures?: Record>; - unsigned?: unknown; - }, ->(jsonObject: T, signingName: string) { - const { signatures, unsigned: _unsigned /*..._rest */ } = jsonObject; - const remoteSignatures = - signatures?.[signingName] && - Object.entries(signatures[signingName]) - .map(([keyId, signature]) => { - const [algorithm, version] = keyId.split(':'); - if (!isValidAlgorithm(algorithm)) { - throw new Error(`Invalid algorithm ${algorithm} for ${signingName}`); - } - - return { - algorithm, - version, - signature, - }; - }) - .filter(({ algorithm }) => - Object.values(EncryptionValidAlgorithm).includes(algorithm as any), - ); - - if (!remoteSignatures?.length) { - throw new Error(`Signatures not found for ${signingName}`); - } - - return remoteSignatures; -} - -export const verifySignature = ( - content: string, - signingName: string, - signature: Uint8Array, - publicKey: Uint8Array, - algorithm: EncryptionValidAlgorithm, - _version: string, -) => { - if (algorithm !== EncryptionValidAlgorithm.ed25519) { - throw new Error(`Invalid algorithm ${algorithm} for ${signingName}`); - } - - if ( - !nacl.sign.detached.verify( - new TextEncoder().encode(content), - signature, - publicKey, - ) - ) { - throw new Error(`Invalid signature for ${signingName}`); - } - return true; -}; - -export const verifyJsonSignature = ( - content: T, - signingName: string, - signature: Uint8Array, - publicKey: Uint8Array, - algorithm: EncryptionValidAlgorithm, - version: string, -) => { - const { signatures: _, unsigned: _unsigned, ...__rest } = content as any; - const canonicalJson = encodeCanonicalJson(__rest); - - return verifySignature( - canonicalJson, - signingName, - signature, - publicKey, - algorithm, - version, - ); -}; - -export async function verifySignaturesFromRemote< - T extends object & { - signatures?: Record>; - unsigned?: unknown; - }, ->( - jsonObject: T, - signingName: string, - getPublicKey: ( - algorithm: EncryptionValidAlgorithm, - version: string, - ) => Promise, -) { - const { signatures: _, unsigned: _unsigned, ...__rest } = jsonObject; - - const canonicalJson = encodeCanonicalJson(__rest); - - const signatures = await getSignaturesFromRemote(jsonObject, signingName); - - for await (const { algorithm, version, signature } of signatures) { - const publicKey = await getPublicKey( - algorithm as EncryptionValidAlgorithm, - version, - ); - - if ( - !nacl.sign.detached.verify( - new TextEncoder().encode(canonicalJson), - new Uint8Array(Buffer.from(signature, 'base64')), - publicKey, - ) - ) { - throw new Error(`Invalid signature for ${signingName}`); - } - } - - return true; -} - -export async function signText( - data: string | Uint8Array, - signingKey: Uint8Array, -) { - const signature = nacl.sign.detached( - typeof data === 'string' ? new TextEncoder().encode(data) : data, - signingKey, - ); - - return toUnpaddedBase64(signature); -} - -export async function signData( - data: string | Uint8Array, - signingKey: Uint8Array, -): Promise { - const signature = nacl.sign.detached( - typeof data === 'string' ? new TextEncoder().encode(data) : data, - signingKey, - ); - - return signature; -} diff --git a/packages/crypto/src/index.spec.ts b/packages/crypto/src/index.spec.ts index c689ddcd7..8b4c1dfa4 100644 --- a/packages/crypto/src/index.spec.ts +++ b/packages/crypto/src/index.spec.ts @@ -4,7 +4,6 @@ import { loadEd25519SignerFromSeed, loadEd25519VerifierFromPublicKey, signJson, - verifyJsonSignature, } from './utils/keys'; describe('signJson', () => { @@ -50,61 +49,4 @@ describe('signJson', () => { 'ZDz7K7NRz0OwgR6n96YMIyt9h8KUCb7T9TklId7S1UDVOwc2y45+tC12/51kxRxpUkaOgr+iBtSBBh74BIrsBQ', ); }); - - it('should verify a signature', async () => { - const json = { - method: 'PUT', - uri: '/_matrix/federation/v1/send/1743489715804', - origin: 'syn1.tunnel.dev.rocket.chat', - destination: 'syn2.tunnel.dev.rocket.chat', - content: { - edus: [ - { - content: { - push: [ - { - last_active_ago: 45931, - presence: 'offline', - user_id: '@debdut:syn1.tunnel.dev.rocket.chat', - }, - ], - }, - edu_type: 'm.presence', - }, - ], - origin: 'syn1.tunnel.dev.rocket.chat', - origin_server_ts: 1743490730808, - pdus: [], - }, - }; - - const signature = - 'ZDz7K7NRz0OwgR6n96YMIyt9h8KUCb7T9TklId7S1UDVOwc2y45+tC12/51kxRxpUkaOgr+iBtSBBh74BIrsBQ'; - - const keyv2serverresponsefromorigin = { - old_verify_keys: {}, - server_name: 'syn1.tunnel.dev.rocket.chat', - signatures: { - 'syn1.tunnel.dev.rocket.chat': { - 'ed25519:a_FAET': - 'MZF+8pncxhUNp7JzdSTIqriaANQ4QTYTe1AIqBNAtVhWcKz1Mc/6nzkP3/1HXZHAzCLYrmuFnTGb874XT4TJDg', - }, - }, - valid_until_ts: 1747753307525, - verify_keys: { - 'ed25519:a_FAET': { - key: 'kryovKVnhHESOdWuZ05ViNotRMVdEh/mG2yJ0npLzEo', - }, - }, - }; - - const verifyKey = - keyv2serverresponsefromorigin.verify_keys['ed25519:a_FAET'].key; - - const verifier = await loadEd25519VerifierFromPublicKey( - fromBase64ToBytes(verifyKey), - ); - - expect(verifyJsonSignature(json, signature, verifier)).resolves; - }); }); diff --git a/packages/federation-sdk/package.json b/packages/federation-sdk/package.json index f3d31abbb..a6ff8ab4b 100644 --- a/packages/federation-sdk/package.json +++ b/packages/federation-sdk/package.json @@ -23,7 +23,6 @@ "mongodb": "^6.16.0", "reflect-metadata": "^0.2.2", "tsyringe": "^4.10.0", - "tweetnacl": "^1.0.3", "zod": "^3.24.1" }, "license": "AGPL-3.0", diff --git a/packages/federation-sdk/src/services/event-authorization.service.ts b/packages/federation-sdk/src/services/event-authorization.service.ts index 3d67260c5..e4dc2b224 100644 --- a/packages/federation-sdk/src/services/event-authorization.service.ts +++ b/packages/federation-sdk/src/services/event-authorization.service.ts @@ -2,7 +2,6 @@ import { createLogger, extractSignaturesFromHeader, generateId, - validateAuthorizationHeader, } from '@rocket.chat/federation-core'; import type { EventID, diff --git a/packages/federation-sdk/src/utils/signJson.spec.ts b/packages/federation-sdk/src/utils/signJson.spec.ts index fc52d9eba..7ec7464c2 100644 --- a/packages/federation-sdk/src/utils/signJson.spec.ts +++ b/packages/federation-sdk/src/utils/signJson.spec.ts @@ -4,102 +4,8 @@ import { generateKeyPairsFromString, pruneEventDict, signJson, - verifySignaturesFromRemote, } from '@rocket.chat/federation-core'; -describe('verifySignaturesFromRemote', async () => { - test('it should verify a valid signature', async () => { - const serverName = 'synapse'; - const signature = await generateKeyPairsFromString( - 'ed25519 a_yNbw tBD7FfjyBHgT4TwhwzvyS9Dq2Z9ck38RRQKaZ6Sz2z8', - ); - - const signed = await signJson({}, signature, serverName); - - await verifySignaturesFromRemote( - signed, - serverName, - async () => signature.publicKey, - ); - - expect( - async () => - await verifySignaturesFromRemote( - signed, - serverName, - async () => signature.publicKey, - ), - ).not.toThrow(); - }); - - test('it should throw an error if the signature is invalid', async () => { - const serverName = 'synapse'; - const signature = await generateKeyPairsFromString( - 'ed25519 a_yNbw tBD7FfjyBHgT4TwhwzvyS9Dq2Z9ck38RRQKaZ6Sz2z8', - ); - - const signed = await signJson({}, signature, serverName); - - expect( - async () => - await verifySignaturesFromRemote(signed, serverName, async () => - Uint8Array.from( - atob('tBD7FfjyBHgT4TwhwzvyS9Dq2Z9ck38RRQKaZ6Sz2z8'), - (c) => c.charCodeAt(0), - ), - ), - ).toThrow(); - }); - - test('it should throw an error if there is no valid protocol version', async () => { - const serverName = 'synapse'; - - expect( - async () => - await verifySignaturesFromRemote( - { - signatures: { - [serverName]: { - [`${EncryptionValidAlgorithm.ed25519}1:a_yNbw`]: 'invalid', - }, - }, - }, - serverName, - async () => - Uint8Array.from( - atob('tBD7FfjyBHgT4TwhwzvyS9Dq2Z9ck38RRQKaZ6Sz2z8'), - (c) => c.charCodeAt(0), - ), - ), - ).toThrow( - `Invalid algorithm ${EncryptionValidAlgorithm.ed25519}1 for ${serverName}`, - ); - }); - - it('it should throw an error if the signature is invalid for the serverName', async () => { - const serverName = 'synapse'; - - expect( - async () => - await verifySignaturesFromRemote( - { - signatures: { - differentServer: { - [`${EncryptionValidAlgorithm.ed25519}1:a_yNbw`]: 'invalid', - }, - }, - }, - serverName, - async () => - Uint8Array.from( - atob('tBD7FfjyBHgT4TwhwzvyS9Dq2Z9ck38RRQKaZ6Sz2z8'), - (c) => c.charCodeAt(0), - ), - ), - ).toThrow(`Signatures not found for ${serverName}`); - }); -}); - // { // "content": { // "auth_events": [ diff --git a/rollup.config.js b/rollup.config.js index 941b3e93d..8c0343666 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -31,7 +31,6 @@ const isExternal = (id) => { 'pino', 'pino-std-serializers', 'sonic-boom', - 'tweetnacl', ].includes(id) ) { return true;