From 47b38ac58f99ade8d550c03004b240b409bbd81b Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Thu, 9 Oct 2025 12:16:12 +0200 Subject: [PATCH 01/40] combine fixes for community authentication --- .../api/1_0/resolver/AuthenticationResolver.ts | 16 ++++++++++++---- .../api/1_0/util/authenticateCommunity.ts | 8 ++++++-- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts index 4c14360e9..84d85a0af 100644 --- a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts @@ -9,7 +9,15 @@ import { getHomeCommunity, } from 'database' import { getLogger } from 'log4js' -import { AuthenticationJwtPayloadType, AuthenticationResponseJwtPayloadType, encryptAndSign, OpenConnectionCallbackJwtPayloadType, OpenConnectionJwtPayloadType, uint32Schema, uuidv4Schema } from 'shared' +import { + AuthenticationJwtPayloadType, + AuthenticationResponseJwtPayloadType, + encryptAndSign, + OpenConnectionCallbackJwtPayloadType, + OpenConnectionJwtPayloadType, + uint32Schema, + uuidv4Schema +} from 'shared' import { Arg, Mutation, Resolver } from 'type-graphql' import { startAuthentication, startOpenConnectionCallback } from '../util/authenticateCommunity' @@ -134,15 +142,15 @@ export class AuthenticationResolver { const authCom = await DbCommunity.findOneByOrFail({ communityUuid: authArgs.oneTimeCode }) if (authCom) { methodLogger.debug('found authCom:', new CommunityLoggingView(authCom)) - if (authCom.publicKey !== authArgs.publicKey) { - const errmsg = `corrupt authentication call detected, oneTimeCode: ${authArgs.oneTimeCode} doesn't belong to caller: ${authArgs.publicKey}` + if (authCom.publicKey.compare(Buffer.from(args.publicKey, 'hex')) !== 0) { + const errmsg = `corrupt authentication call detected, oneTimeCode: ${authArgs.oneTimeCode} doesn't belong to caller: ${args.publicKey}` methodLogger.error(errmsg) // no infos to the caller return null } const communityUuid = uuidv4Schema.safeParse(authArgs.uuid) if (!communityUuid.success) { - const errmsg = `invalid uuid: ${authArgs.uuid} for community with publicKey ${authArgs.publicKey}` + const errmsg = `invalid uuid: ${authArgs.uuid} for community with publicKey ${authCom.publicKey}` methodLogger.error(errmsg) // no infos to the caller return null diff --git a/federation/src/graphql/api/1_0/util/authenticateCommunity.ts b/federation/src/graphql/api/1_0/util/authenticateCommunity.ts index 33f725737..a5a3f532a 100644 --- a/federation/src/graphql/api/1_0/util/authenticateCommunity.ts +++ b/federation/src/graphql/api/1_0/util/authenticateCommunity.ts @@ -14,7 +14,7 @@ import { randombytes_random } from 'sodium-native' import { AuthenticationClient as V1_0_AuthenticationClient } from '@/client/1_0/AuthenticationClient' import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' -import { AuthenticationJwtPayloadType, AuthenticationResponseJwtPayloadType, encryptAndSign, OpenConnectionCallbackJwtPayloadType, uuidv4Schema, verifyAndDecrypt } from 'shared' +import { AuthenticationJwtPayloadType, AuthenticationResponseJwtPayloadType, encryptAndSign, OpenConnectionCallbackJwtPayloadType, uint32Schema, uuidv4Schema, verifyAndDecrypt } from 'shared' const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.util.authenticateCommunity`) @@ -43,7 +43,11 @@ export async function startOpenConnectionCallback( // store oneTimeCode in requestedCom.community_uuid as authenticate-request-identifier // prevent overwriting valid UUID with oneTimeCode, because this request could be initiated at any time from federated community if (uuidv4Schema.safeParse(comA.communityUuid).success) { - throw new Error('Community UUID is already a valid UUID') + methodLogger.debug('Community UUID is already a valid UUID') + return + } else if (uint32Schema.safeParse(Number(comA.communityUuid)).success) { + methodLogger.debug('Community UUID is still in authentication...oneTimeCode=', comA.communityUuid) + return } // TODO: make sure it is unique const oneTimeCode = randombytes_random().toString() From e8ef1bc3109a9bcb631a2de0fa90da72c33a05a0 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Sun, 12 Oct 2025 08:48:08 +0200 Subject: [PATCH 02/40] try to make oneTimeCode more robust --- .../src/federation/authenticateCommunities.ts | 13 +++---- backend/src/federation/validateCommunities.ts | 13 ++++++- database/src/queries/communities.ts | 9 +++++ .../1_0/resolver/AuthenticationResolver.ts | 6 ++++ .../api/1_0/util/authenticateCommunity.ts | 8 +++-- shared/src/const/index.ts | 4 ++- shared/src/index.ts | 1 + shared/src/schema/community.schema.test.ts | 35 +++++++++++++++++++ shared/src/schema/community.schema.ts | 7 ++++ shared/src/schema/index.ts | 3 +- 10 files changed, 86 insertions(+), 13 deletions(-) create mode 100644 shared/src/schema/community.schema.test.ts create mode 100644 shared/src/schema/community.schema.ts diff --git a/backend/src/federation/authenticateCommunities.ts b/backend/src/federation/authenticateCommunities.ts index ad2d91469..f186adb35 100644 --- a/backend/src/federation/authenticateCommunities.ts +++ b/backend/src/federation/authenticateCommunities.ts @@ -7,7 +7,7 @@ import { AuthenticationClient as V1_0_AuthenticationClient } from '@/federation/ import { ensureUrlEndsWithSlash } from 'core' import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' -import { encryptAndSign, OpenConnectionJwtPayloadType } from 'shared' +import { communityAuthenticatedSchema, encryptAndSign, OpenConnectionJwtPayloadType } from 'shared' import { getLogger } from 'log4js' import { AuthenticationClientFactory } from './client/AuthenticationClientFactory' import { EncryptedTransferArgs } from 'core' @@ -34,13 +34,10 @@ export async function startCommunityAuthentication( methodLogger.debug('started with comB:', new CommunityLoggingView(comB)) // check if communityUuid is not a valid v4Uuid try { - if ( - comB && - ((comB.communityUuid === null && comB.authenticatedAt === null) || - (comB.communityUuid !== null && - (!validateUUID(comB.communityUuid) || - versionUUID(comB.communityUuid!) !== 4))) - ) { + // communityAuthenticatedSchema.safeParse return true + // - if communityUuid is a valid v4Uuid and + // - if authenticatedAt is a valid date + if (comB && !communityAuthenticatedSchema.safeParse(comB).success) { methodLogger.debug('comB.uuid is null or is a not valid v4Uuid...', comB.communityUuid || 'null', comB.authenticatedAt || 'null') const client = AuthenticationClientFactory.getInstance(fedComB) diff --git a/backend/src/federation/validateCommunities.ts b/backend/src/federation/validateCommunities.ts index 36c9985c9..cb56a8c53 100644 --- a/backend/src/federation/validateCommunities.ts +++ b/backend/src/federation/validateCommunities.ts @@ -3,6 +3,7 @@ import { FederatedCommunity as DbFederatedCommunity, FederatedCommunityLoggingView, getHomeCommunity, + getNotReachableCommunities, } from 'database' import { IsNull } from 'typeorm' @@ -11,7 +12,7 @@ import { FederationClient as V1_0_FederationClient } from '@/federation/client/1 import { PublicCommunityInfo } from '@/federation/client/1_0/model/PublicCommunityInfo' import { FederationClientFactory } from '@/federation/client/FederationClientFactory' import { LogError } from '@/server/LogError' -import { createKeyPair } from 'shared' +import { createKeyPair, uint32Schema } from 'shared' import { getLogger } from 'log4js' import { startCommunityAuthentication } from './authenticateCommunities' import { PublicCommunityInfoLoggingView } from './client/1_0/logging/PublicCommunityInfoLogging.view' @@ -27,6 +28,16 @@ export async function startValidateCommunities(timerInterval: number): Promise see https://javascript.info/settimeout-setinterval setTimeout(async function run() { diff --git a/database/src/queries/communities.ts b/database/src/queries/communities.ts index e216f8af6..cdb7cabb6 100644 --- a/database/src/queries/communities.ts +++ b/database/src/queries/communities.ts @@ -60,4 +60,13 @@ export async function getReachableCommunities( ], order, }) +} + +export async function getNotReachableCommunities( + order?: FindOptionsOrder +): Promise { + return await DbCommunity.find({ + where: { authenticatedAt: IsNull(), foreign: true }, + order, + }) } \ No newline at end of file diff --git a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts index 84d85a0af..fd948300e 100644 --- a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts @@ -127,21 +127,27 @@ export class AuthenticationResolver { methodLogger.debug(`authenticate() via apiVersion=1_0 ...`, args) try { const authArgs = await interpretEncryptedTransferArgs(args) as AuthenticationJwtPayloadType + methodLogger.debug(`interpreted authentication payload...authArgs:`, authArgs) if (!authArgs) { const errmsg = `invalid authentication payload of requesting community with publicKey` + args.publicKey methodLogger.error(errmsg) // no infos to the caller return null } + if (!uint32Schema.safeParse(Number(authArgs.oneTimeCode)).success) { const errmsg = `invalid oneTimeCode: ${authArgs.oneTimeCode} for community with publicKey ${authArgs.publicKey}, expect uint32` methodLogger.error(errmsg) // no infos to the caller return null } + + methodLogger.debug(`search community per oneTimeCode:`, authArgs.oneTimeCode) const authCom = await DbCommunity.findOneByOrFail({ communityUuid: authArgs.oneTimeCode }) if (authCom) { methodLogger.debug('found authCom:', new CommunityLoggingView(authCom)) + methodLogger.debug('authCom.publicKey', authCom.publicKey.toString('hex')) + methodLogger.debug('args.publicKey', args.publicKey) if (authCom.publicKey.compare(Buffer.from(args.publicKey, 'hex')) !== 0) { const errmsg = `corrupt authentication call detected, oneTimeCode: ${authArgs.oneTimeCode} doesn't belong to caller: ${args.publicKey}` methodLogger.error(errmsg) diff --git a/federation/src/graphql/api/1_0/util/authenticateCommunity.ts b/federation/src/graphql/api/1_0/util/authenticateCommunity.ts index a5a3f532a..4d8af9187 100644 --- a/federation/src/graphql/api/1_0/util/authenticateCommunity.ts +++ b/federation/src/graphql/api/1_0/util/authenticateCommunity.ts @@ -15,6 +15,7 @@ import { randombytes_random } from 'sodium-native' import { AuthenticationClient as V1_0_AuthenticationClient } from '@/client/1_0/AuthenticationClient' import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { AuthenticationJwtPayloadType, AuthenticationResponseJwtPayloadType, encryptAndSign, OpenConnectionCallbackJwtPayloadType, uint32Schema, uuidv4Schema, verifyAndDecrypt } from 'shared' +import { FEDERATION_AUTHENTICATION_TIMEOUT_MS } from 'shared' const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.util.authenticateCommunity`) @@ -45,9 +46,12 @@ export async function startOpenConnectionCallback( if (uuidv4Schema.safeParse(comA.communityUuid).success) { methodLogger.debug('Community UUID is already a valid UUID') return + // check for still ongoing authentication, but with timeout } else if (uint32Schema.safeParse(Number(comA.communityUuid)).success) { - methodLogger.debug('Community UUID is still in authentication...oneTimeCode=', comA.communityUuid) - return + if (comA.updatedAt && (Date.now() - comA.updatedAt.getTime()) < FEDERATION_AUTHENTICATION_TIMEOUT_MS) { + methodLogger.debug('Community UUID is still in authentication...oneTimeCode=', comA.communityUuid) + return + } } // TODO: make sure it is unique const oneTimeCode = randombytes_random().toString() diff --git a/shared/src/const/index.ts b/shared/src/const/index.ts index e6fb80990..549991c61 100644 --- a/shared/src/const/index.ts +++ b/shared/src/const/index.ts @@ -1,4 +1,6 @@ export const DECAY_START_TIME = new Date('2021-05-13T17:46:31Z') export const SECONDS_PER_YEAR_GREGORIAN_CALENDER = 31556952.0 export const LOG4JS_BASE_CATEGORY_NAME = 'shared' -export const REDEEM_JWT_TOKEN_EXPIRATION = '10m' \ No newline at end of file +export const REDEEM_JWT_TOKEN_EXPIRATION = '10m' +// 10 minutes +export const FEDERATION_AUTHENTICATION_TIMEOUT_MS = 60 * 60 * 1000 * 10 \ No newline at end of file diff --git a/shared/src/index.ts b/shared/src/index.ts index a9e070d7f..1e44e2c48 100644 --- a/shared/src/index.ts +++ b/shared/src/index.ts @@ -1,5 +1,6 @@ export * from './schema' export * from './enum' +export * from './const' export * from './helper' export * from './logic/decay' export * from './jwt/JWT' diff --git a/shared/src/schema/community.schema.test.ts b/shared/src/schema/community.schema.test.ts new file mode 100644 index 000000000..a46ea9c94 --- /dev/null +++ b/shared/src/schema/community.schema.test.ts @@ -0,0 +1,35 @@ +import { v4 as uuidv4 } from 'uuid' +import { communityAuthenticatedSchema } from './community.schema' +import { describe, it, expect } from 'bun:test' + + +describe('communityAuthenticatedSchema', () => { + it('should return an error if communityUuid is not a uuidv4', () => { + const data = communityAuthenticatedSchema.safeParse({ + communityUuid: '1234567890', + authenticatedAt: new Date(), + }) + + expect(data.success).toBe(false) + expect(data.error?.issues[0].path).toEqual(['communityUuid']) + }) + + it('should return an error if authenticatedAt is not a date', () => { + const data = communityAuthenticatedSchema.safeParse({ + communityUuid: uuidv4(), + authenticatedAt: '2022-01-01', + }) + + expect(data.success).toBe(false) + expect(data.error?.issues[0].path).toEqual(['authenticatedAt']) + }) + + it('should return no error for valid data and valid uuid4', () => { + const data = communityAuthenticatedSchema.safeParse({ + communityUuid: uuidv4(), + authenticatedAt: new Date(), + }) + + expect(data.success).toBe(true) + }) +}) diff --git a/shared/src/schema/community.schema.ts b/shared/src/schema/community.schema.ts new file mode 100644 index 000000000..6067e3538 --- /dev/null +++ b/shared/src/schema/community.schema.ts @@ -0,0 +1,7 @@ +import { object, date } from 'zod' +import { uuidv4Schema } from './base.schema' + +export const communityAuthenticatedSchema = object({ + communityUuid: uuidv4Schema, + authenticatedAt: date(), +}) \ No newline at end of file diff --git a/shared/src/schema/index.ts b/shared/src/schema/index.ts index d8c9f9e4c..83455fb73 100644 --- a/shared/src/schema/index.ts +++ b/shared/src/schema/index.ts @@ -1,2 +1,3 @@ export * from './user.schema' -export * from './base.schema' \ No newline at end of file +export * from './base.schema' +export * from './community.schema' \ No newline at end of file From d6d626c848bb2ff222069cc98f7fb6990b3d3715 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Mon, 13 Oct 2025 07:40:38 +0200 Subject: [PATCH 03/40] add community handshake states table and enum --- ...95-add_community_handshake_states_table.ts | 19 +++++++++ .../src/entity/CommunityHandshakeState.ts | 39 +++++++++++++++++++ database/src/entity/FederatedCommunity.ts | 6 +++ .../src/enum/CommunityHandshakeStateType.ts | 8 ++++ database/src/enum/index.ts | 1 + 5 files changed, 73 insertions(+) create mode 100644 database/migration/migrations/0095-add_community_handshake_states_table.ts create mode 100644 database/src/entity/CommunityHandshakeState.ts create mode 100644 database/src/enum/CommunityHandshakeStateType.ts create mode 100644 database/src/enum/index.ts diff --git a/database/migration/migrations/0095-add_community_handshake_states_table.ts b/database/migration/migrations/0095-add_community_handshake_states_table.ts new file mode 100644 index 000000000..a00d9f6d7 --- /dev/null +++ b/database/migration/migrations/0095-add_community_handshake_states_table.ts @@ -0,0 +1,19 @@ +export async function upgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn(` + CREATE TABLE community_handshake_states ( + id int unsigned NOT NULL AUTO_INCREMENT, + handshake_id int unsigned NOT NULL, + one_time_code int unsigned NOT NULL, + public_key binary(32) NOT NULL, + status varchar(255) NOT NULL DEFAULT 'OPEN_CONNECTION', + last_error text, + created_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + updated_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (id), + KEY idx_public_key (public_key), + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`) +} + +export async function downgrade(queryFn: (query: string, values?: any[]) => Promise>) { + await queryFn(`DROP TABLE community_handshake_states;`) +} diff --git a/database/src/entity/CommunityHandshakeState.ts b/database/src/entity/CommunityHandshakeState.ts new file mode 100644 index 000000000..25352e2b4 --- /dev/null +++ b/database/src/entity/CommunityHandshakeState.ts @@ -0,0 +1,39 @@ +import { CommunityHandshakeStateType } from '../enum' +import { BaseEntity, Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm' +import { FederatedCommunity } from './FederatedCommunity' + +@Entity('community_handshake_states') +export class CommunityHandshakeState extends BaseEntity { + @PrimaryGeneratedColumn({ unsigned: true }) + id: number + + @Column({ name: 'handshake_id', type: 'int', unsigned: true }) + handshakeId: number + + @Column({ name: 'one_time_code', type: 'int', unsigned: true }) + oneTimeCode: number + + @Column({ name: 'public_key', type: 'binary', length: 32 }) + publicKey: Buffer + + @Column({ + type: 'varchar', + length: 255, + default: CommunityHandshakeStateType.OPEN_CONNECTION, + nullable: false, + }) + status: CommunityHandshakeStateType + + @Column({ name: 'last_error', type: 'text', nullable: true }) + lastError?: string + + @CreateDateColumn({ name: 'created_at', type: 'datetime', precision: 3 }) + createdAt: Date + + @UpdateDateColumn({ name: 'updated_at', type: 'datetime', precision: 3 }) + updatedAt: Date + + @ManyToOne(() => FederatedCommunity, (federatedCommunity) => federatedCommunity.communityHandshakeStates) + @JoinColumn({ name: 'public_key', referencedColumnName: 'publicKey' }) + federatedCommunity: FederatedCommunity +} \ No newline at end of file diff --git a/database/src/entity/FederatedCommunity.ts b/database/src/entity/FederatedCommunity.ts index a6eaee80f..8993f0663 100644 --- a/database/src/entity/FederatedCommunity.ts +++ b/database/src/entity/FederatedCommunity.ts @@ -5,10 +5,12 @@ import { Entity, JoinColumn, ManyToOne, + OneToMany, PrimaryGeneratedColumn, UpdateDateColumn, } from 'typeorm' import { Community } from './Community' +import { CommunityHandshakeState } from './CommunityHandshakeState' @Entity('federated_communities') export class FederatedCommunity extends BaseEntity { @@ -60,4 +62,8 @@ export class FederatedCommunity extends BaseEntity { ) @JoinColumn({ name: 'public_key', referencedColumnName: 'publicKey' }) community?: Community + + @OneToMany(() => CommunityHandshakeState, (communityHandshakeState) => communityHandshakeState.federatedCommunity) + @JoinColumn({ name: 'public_key', referencedColumnName: 'publicKey' }) + communityHandshakeStates: CommunityHandshakeState[] } diff --git a/database/src/enum/CommunityHandshakeStateType.ts b/database/src/enum/CommunityHandshakeStateType.ts new file mode 100644 index 000000000..e79c4a04c --- /dev/null +++ b/database/src/enum/CommunityHandshakeStateType.ts @@ -0,0 +1,8 @@ +export enum CommunityHandshakeStateType { + OPEN_CONNECTION = 'OPEN_CONNECTION', + OPEN_CONNECTION_CALLBACK = 'OPEN_CONNECTION_CALLBACK', + + SUCCESS = 'SUCCESS', + FAILED = 'FAILED', + EXPIRED = 'EXPIRED' +} \ No newline at end of file diff --git a/database/src/enum/index.ts b/database/src/enum/index.ts new file mode 100644 index 000000000..c1d445299 --- /dev/null +++ b/database/src/enum/index.ts @@ -0,0 +1 @@ +export * from './CommunityHandshakeStateType' \ No newline at end of file From b5b4e94c2a85c93f46e884602c9e0d2f59115b91 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Mon, 13 Oct 2025 07:41:47 +0200 Subject: [PATCH 04/40] add entity to index --- database/src/entity/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/database/src/entity/index.ts b/database/src/entity/index.ts index 01195c37e..4d4b5d803 100644 --- a/database/src/entity/index.ts +++ b/database/src/entity/index.ts @@ -7,6 +7,7 @@ import { Event } from './Event' import { FederatedCommunity } from './FederatedCommunity' import { LoginElopageBuys } from './LoginElopageBuys' import { Migration } from './Migration' +import { CommunityHandshakeState } from './CommunityHandshakeState' import { OpenaiThreads } from './OpenaiThreads' import { PendingTransaction } from './PendingTransaction' import { ProjectBranding } from './ProjectBranding' @@ -26,6 +27,7 @@ export { FederatedCommunity, LoginElopageBuys, Migration, + CommunityHandshakeState, ProjectBranding, OpenaiThreads, PendingTransaction, @@ -46,6 +48,7 @@ export const entities = [ FederatedCommunity, LoginElopageBuys, Migration, + CommunityHandshakeState, ProjectBranding, OpenaiThreads, PendingTransaction, From ebd43543c62cce18a2ae4d87be67d2e4afd12ba6 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Mon, 13 Oct 2025 07:42:14 +0200 Subject: [PATCH 05/40] add enum to index --- database/src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/database/src/index.ts b/database/src/index.ts index 56dec24ee..b694567a2 100644 --- a/database/src/index.ts +++ b/database/src/index.ts @@ -60,5 +60,6 @@ export const entities = [ export { latestDbVersion } export * from './logging' export * from './queries' -export * from './util' +export * from './util' +export * from './enum' export { AppDatabase } from './AppDatabase' From ed422fe2930275b7d793a2c8b73af8dde12c186c Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Mon, 13 Oct 2025 09:13:26 +0200 Subject: [PATCH 06/40] startCommunityAuthentication refactored --- .../src/federation/authenticateCommunities.ts | 133 +++++++++++------- core/src/index.ts | 1 + .../logic/CommunityHandshakeState.logic.ts | 22 +++ core/src/logic/index.ts | 1 + ...95-add_community_handshake_states_table.ts | 3 +- .../src/entity/CommunityHandshakeState.ts | 5 +- database/src/entity/index.ts | 6 +- .../src/enum/CommunityHandshakeStateType.ts | 1 + database/src/index.ts | 60 +------- .../CommunityHandshakeStateLogging.view.ts | 25 ++++ database/src/logging/CommunityLogging.view.ts | 5 +- database/src/logging/index.ts | 2 + database/src/queries/communities.test.ts | 20 ++- database/src/queries/communities.ts | 9 +- database/src/queries/communityHandshakes.ts | 15 ++ database/src/queries/index.ts | 1 + database/src/seeds/community.ts | 22 ++- 17 files changed, 212 insertions(+), 119 deletions(-) create mode 100644 core/src/logic/CommunityHandshakeState.logic.ts create mode 100644 core/src/logic/index.ts create mode 100644 database/src/logging/CommunityHandshakeStateLogging.view.ts create mode 100644 database/src/queries/communityHandshakes.ts diff --git a/backend/src/federation/authenticateCommunities.ts b/backend/src/federation/authenticateCommunities.ts index f186adb35..0a94b24c8 100644 --- a/backend/src/federation/authenticateCommunities.ts +++ b/backend/src/federation/authenticateCommunities.ts @@ -1,5 +1,14 @@ -import { CommunityLoggingView, Community as DbCommunity, FederatedCommunity as DbFederatedCommunity, FederatedCommunityLoggingView, getHomeCommunity } from 'database' -import { validate as validateUUID, version as versionUUID } from 'uuid' +import { + CommunityHandshakeState as DbCommunityHandshakeState, + CommunityHandshakeStateLoggingView, + CommunityLoggingView, + Community as DbCommunity, + FederatedCommunity as DbFederatedCommunity, + FederatedCommunityLoggingView, + findPendingCommunityHandshake, + getHomeCommunityWithFederatedCommunityOrFail, + CommunityHandshakeStateType +} from 'database' import { randombytes_random } from 'sodium-native' import { CONFIG as CONFIG_CORE } from 'core' @@ -11,65 +20,91 @@ import { communityAuthenticatedSchema, encryptAndSign, OpenConnectionJwtPayloadT import { getLogger } from 'log4js' import { AuthenticationClientFactory } from './client/AuthenticationClientFactory' import { EncryptedTransferArgs } from 'core' +import { CommunityHandshakeStateLogic } from 'core' -const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.authenticateCommunities`) +const createLogger = (functionName: string) => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.authenticateCommunities.${functionName}`) export async function startCommunityAuthentication( fedComB: DbFederatedCommunity, ): Promise { - const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.authenticateCommunities.startCommunityAuthentication`) + const methodLogger = createLogger('startCommunityAuthentication') const handshakeID = randombytes_random().toString() methodLogger.addContext('handshakeID', handshakeID) methodLogger.debug(`startCommunityAuthentication()...`, { fedComB: new FederatedCommunityLoggingView(fedComB), }) - const homeComA = await getHomeCommunity() - methodLogger.debug('homeComA', new CommunityLoggingView(homeComA!)) - const homeFedComA = await DbFederatedCommunity.findOneByOrFail({ - foreign: false, - apiVersion: CONFIG_CORE.FEDERATION_BACKEND_SEND_ON_API, - }) - methodLogger.debug('homeFedComA', new FederatedCommunityLoggingView(homeFedComA)) + const homeComA = await getHomeCommunityWithFederatedCommunityOrFail(fedComB.apiVersion) + methodLogger.debug('homeComA', new CommunityLoggingView(homeComA)) + // check if result is like expected + // TODO: use zod/valibot + if ( + !homeComA.federatedCommunities || + homeComA.federatedCommunities.length === 0 || + homeComA.federatedCommunities[0].apiVersion !== fedComB.apiVersion + ) { + throw new Error(`Missing home community or federated community with api version ${fedComB.apiVersion}`) + } + const homeFedComA = homeComA.federatedCommunities[0] const comB = await DbCommunity.findOneByOrFail({ publicKey: fedComB.publicKey }) methodLogger.debug('started with comB:', new CommunityLoggingView(comB)) // check if communityUuid is not a valid v4Uuid - try { - // communityAuthenticatedSchema.safeParse return true - // - if communityUuid is a valid v4Uuid and - // - if authenticatedAt is a valid date - if (comB && !communityAuthenticatedSchema.safeParse(comB).success) { - methodLogger.debug('comB.uuid is null or is a not valid v4Uuid...', comB.communityUuid || 'null', comB.authenticatedAt || 'null') - const client = AuthenticationClientFactory.getInstance(fedComB) - - if (client instanceof V1_0_AuthenticationClient) { - if (!comB.publicJwtKey) { - throw new Error('Public JWT key still not exist for comB ' + comB.name) - } - //create JWT with url in payload encrypted by foreignCom.publicJwtKey and signed with homeCom.privateJwtKey - const payload = new OpenConnectionJwtPayloadType(handshakeID, - ensureUrlEndsWithSlash(homeFedComA.endPoint).concat(homeFedComA.apiVersion), - ) - methodLogger.debug('payload', payload) - const jws = await encryptAndSign(payload, homeComA!.privateJwtKey!, comB.publicJwtKey!) - methodLogger.debug('jws', jws) - // prepare the args for the client invocation - const args = new EncryptedTransferArgs() - args.publicKey = homeComA!.publicKey.toString('hex') - args.jwt = jws - args.handshakeID = handshakeID - methodLogger.debug('before client.openConnection() args:', args) - const result = await client.openConnection(args) - if (result) { - methodLogger.debug(`successful initiated at community:`, fedComB.endPoint) - } else { - methodLogger.error(`can't initiate at community:`, fedComB.endPoint) - } - } - } else { - methodLogger.debug(`comB.communityUuid is already a valid v4Uuid ${ comB.communityUuid || 'null' } and was authenticated at ${ comB.authenticatedAt || 'null'}`) - } - } catch (err) { - methodLogger.error(`Error:`, err) + + // communityAuthenticatedSchema.safeParse return true + // - if communityUuid is a valid v4Uuid and + // - if authenticatedAt is a valid date + if (communityAuthenticatedSchema.safeParse(comB).success) { + methodLogger.debug(`comB.communityUuid is already a valid v4Uuid ${ comB.communityUuid || 'null' } and was authenticated at ${ comB.authenticatedAt || 'null'}`) + return + } + methodLogger.debug('comB.uuid is null or is a not valid v4Uuid...', + comB.communityUuid || 'null', comB.authenticatedAt || 'null' + ) + + // check if a authentication is already in progress + const existingState = await findPendingCommunityHandshake(fedComB, false) + if (existingState) { + const logic = new CommunityHandshakeStateLogic(existingState) + if (!await logic.isTimeoutUpdate()) { + // authentication with community and api version is still in progress and it is not timeout yet + methodLogger.debug('existingState', new CommunityHandshakeStateLoggingView(existingState)) + return + } + } + + const state = new DbCommunityHandshakeState() + state.publicKey = fedComB.publicKey + state.apiVersion = fedComB.apiVersion + state.status = CommunityHandshakeStateType.START_COMMUNITY_AUTHENTICATION + state.handshakeId = parseInt(handshakeID) + + const client = AuthenticationClientFactory.getInstance(fedComB) + + if (client instanceof V1_0_AuthenticationClient) { + if (!comB.publicJwtKey) { + state.lastError = 'Public JWT key still not exist for comB ' + comB.name + await state.save() + throw new Error(state.lastError) + } + const stateSaveResolver = state.save() + //create JWT with url in payload encrypted by foreignCom.publicJwtKey and signed with homeCom.privateJwtKey + const payload = new OpenConnectionJwtPayloadType(handshakeID, + ensureUrlEndsWithSlash(homeFedComA.endPoint).concat(homeFedComA.apiVersion), + ) + methodLogger.debug('payload', payload) + const jws = await encryptAndSign(payload, homeComA!.privateJwtKey!, comB.publicJwtKey!) + methodLogger.debug('jws', jws) + // prepare the args for the client invocation + const args = new EncryptedTransferArgs() + args.publicKey = homeComA!.publicKey.toString('hex') + args.jwt = jws + args.handshakeID = handshakeID + await stateSaveResolver + methodLogger.debug('before client.openConnection() args:', args) + const result = await client.openConnection(args) + if (result) { + methodLogger.info(`successful initiated at community:`, fedComB.endPoint) + } else { + methodLogger.error(`can't initiate at community:`, fedComB.endPoint) + } } - methodLogger.removeContext('handshakeID') } diff --git a/core/src/index.ts b/core/src/index.ts index a355bb9bd..a10eb6caa 100644 --- a/core/src/index.ts +++ b/core/src/index.ts @@ -22,4 +22,5 @@ export * from './util/calculateSenderBalance' export * from './util/utilities' export * from './validation/user' export * from './config/index' +export * from './logic' diff --git a/core/src/logic/CommunityHandshakeState.logic.ts b/core/src/logic/CommunityHandshakeState.logic.ts new file mode 100644 index 000000000..a3e8631eb --- /dev/null +++ b/core/src/logic/CommunityHandshakeState.logic.ts @@ -0,0 +1,22 @@ +import { CommunityHandshakeState, CommunityHandshakeStateType } from 'database' +import { FEDERATION_AUTHENTICATION_TIMEOUT_MS } from 'shared' + +export class CommunityHandshakeStateLogic { + public constructor(private communityHandshakeStateEntity: CommunityHandshakeState) {} + + /** + * Check for expired state and if not, check timeout and update (write into db) to expired state + * @returns true if the community handshake state is expired + */ + public async isTimeoutUpdate(): Promise { + if (this.communityHandshakeStateEntity.status === CommunityHandshakeStateType.EXPIRED) { + return true + } + if (Date.now() - this.communityHandshakeStateEntity.updatedAt.getTime() > FEDERATION_AUTHENTICATION_TIMEOUT_MS) { + this.communityHandshakeStateEntity.status = CommunityHandshakeStateType.EXPIRED + await this.communityHandshakeStateEntity.save() + return true + } + return false + } +} diff --git a/core/src/logic/index.ts b/core/src/logic/index.ts new file mode 100644 index 000000000..3a5c48f58 --- /dev/null +++ b/core/src/logic/index.ts @@ -0,0 +1 @@ +export { CommunityHandshakeStateLogic } from './CommunityHandshakeState.logic' \ No newline at end of file diff --git a/database/migration/migrations/0095-add_community_handshake_states_table.ts b/database/migration/migrations/0095-add_community_handshake_states_table.ts index a00d9f6d7..38553c336 100644 --- a/database/migration/migrations/0095-add_community_handshake_states_table.ts +++ b/database/migration/migrations/0095-add_community_handshake_states_table.ts @@ -5,12 +5,13 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis handshake_id int unsigned NOT NULL, one_time_code int unsigned NOT NULL, public_key binary(32) NOT NULL, + api_version varchar(255) NOT NULL, status varchar(255) NOT NULL DEFAULT 'OPEN_CONNECTION', last_error text, created_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), updated_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), PRIMARY KEY (id), - KEY idx_public_key (public_key), + KEY idx_public_key (public_key) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`) } diff --git a/database/src/entity/CommunityHandshakeState.ts b/database/src/entity/CommunityHandshakeState.ts index 25352e2b4..4fb62a733 100644 --- a/database/src/entity/CommunityHandshakeState.ts +++ b/database/src/entity/CommunityHandshakeState.ts @@ -16,10 +16,13 @@ export class CommunityHandshakeState extends BaseEntity { @Column({ name: 'public_key', type: 'binary', length: 32 }) publicKey: Buffer + @Column({ name: 'api_version', type: 'varchar', length: 255 }) + apiVersion: string + @Column({ type: 'varchar', length: 255, - default: CommunityHandshakeStateType.OPEN_CONNECTION, + default: CommunityHandshakeStateType.START_COMMUNITY_AUTHENTICATION, nullable: false, }) status: CommunityHandshakeStateType diff --git a/database/src/entity/index.ts b/database/src/entity/index.ts index 4d4b5d803..32bbe239b 100644 --- a/database/src/entity/index.ts +++ b/database/src/entity/index.ts @@ -19,6 +19,7 @@ import { UserRole } from './UserRole' export { Community, + CommunityHandshakeState, Contribution, ContributionLink, ContributionMessage, @@ -26,8 +27,7 @@ export { Event, FederatedCommunity, LoginElopageBuys, - Migration, - CommunityHandshakeState, + Migration, ProjectBranding, OpenaiThreads, PendingTransaction, @@ -40,6 +40,7 @@ export { export const entities = [ Community, + CommunityHandshakeState, Contribution, ContributionLink, ContributionMessage, @@ -48,7 +49,6 @@ export const entities = [ FederatedCommunity, LoginElopageBuys, Migration, - CommunityHandshakeState, ProjectBranding, OpenaiThreads, PendingTransaction, diff --git a/database/src/enum/CommunityHandshakeStateType.ts b/database/src/enum/CommunityHandshakeStateType.ts index e79c4a04c..a047e740d 100644 --- a/database/src/enum/CommunityHandshakeStateType.ts +++ b/database/src/enum/CommunityHandshakeStateType.ts @@ -1,4 +1,5 @@ export enum CommunityHandshakeStateType { + START_COMMUNITY_AUTHENTICATION = 'START_COMMUNITY_AUTHENTICATION', OPEN_CONNECTION = 'OPEN_CONNECTION', OPEN_CONNECTION_CALLBACK = 'OPEN_CONNECTION_CALLBACK', diff --git a/database/src/index.ts b/database/src/index.ts index b694567a2..45b60530c 100644 --- a/database/src/index.ts +++ b/database/src/index.ts @@ -1,63 +1,7 @@ import { latestDbVersion } from './detectLastDBVersion' -import { Community } from './entity/Community' -import { Contribution } from './entity/Contribution' -import { ContributionLink } from './entity/ContributionLink' -import { ContributionMessage } from './entity/ContributionMessage' -import { DltTransaction } from './entity/DltTransaction' -import { Event } from './entity/Event' -import { FederatedCommunity } from './entity/FederatedCommunity' -import { LoginElopageBuys } from './entity/LoginElopageBuys' -import { Migration } from './entity/Migration' -import { OpenaiThreads } from './entity/OpenaiThreads' -import { PendingTransaction } from './entity/PendingTransaction' -import { ProjectBranding } from './entity/ProjectBranding' -import { Transaction } from './entity/Transaction' -import { TransactionLink } from './entity/TransactionLink' -import { User } from './entity/User' -import { UserContact } from './entity/UserContact' -import { UserRole } from './entity/UserRole' - -export { - Community, - Contribution, - ContributionLink, - ContributionMessage, - DltTransaction, - Event, - FederatedCommunity, - LoginElopageBuys, - Migration, - ProjectBranding, - OpenaiThreads, - PendingTransaction, - Transaction, - TransactionLink, - User, - UserContact, - UserRole, -} - -export const entities = [ - Community, - Contribution, - ContributionLink, - ContributionMessage, - DltTransaction, - Event, - FederatedCommunity, - LoginElopageBuys, - Migration, - ProjectBranding, - OpenaiThreads, - PendingTransaction, - Transaction, - TransactionLink, - User, - UserContact, - UserRole, -] - export { latestDbVersion } + +export * from './entity' export * from './logging' export * from './queries' export * from './util' diff --git a/database/src/logging/CommunityHandshakeStateLogging.view.ts b/database/src/logging/CommunityHandshakeStateLogging.view.ts new file mode 100644 index 000000000..5345474a0 --- /dev/null +++ b/database/src/logging/CommunityHandshakeStateLogging.view.ts @@ -0,0 +1,25 @@ +import { CommunityHandshakeState } from '..' +import { AbstractLoggingView } from './AbstractLogging.view' +import { FederatedCommunityLoggingView } from './FederatedCommunityLogging.view' + +export class CommunityHandshakeStateLoggingView extends AbstractLoggingView { + public constructor(private self: CommunityHandshakeState) { + super() + } + + public toJSON(): any { + return { + id: this.self.id, + handshakeId: this.self.handshakeId, + oneTimeCode: this.self.oneTimeCode, + publicKey: this.self.publicKey.toString(this.bufferStringFormat), + status: this.self.status, + lastError: this.self.lastError, + createdAt: this.dateToString(this.self.createdAt), + updatedAt: this.dateToString(this.self.updatedAt), + federatedCommunity: this.self.federatedCommunity + ? new FederatedCommunityLoggingView(this.self.federatedCommunity) + : undefined, + } + } +} \ No newline at end of file diff --git a/database/src/logging/CommunityLogging.view.ts b/database/src/logging/CommunityLogging.view.ts index c06a4db41..1d675828c 100644 --- a/database/src/logging/CommunityLogging.view.ts +++ b/database/src/logging/CommunityLogging.view.ts @@ -1,5 +1,5 @@ import { Community } from '../entity' - +import { FederatedCommunityLoggingView } from './FederatedCommunityLogging.view' import { AbstractLoggingView } from './AbstractLogging.view' export class CommunityLoggingView extends AbstractLoggingView { @@ -21,6 +21,9 @@ export class CommunityLoggingView extends AbstractLoggingView { creationDate: this.dateToString(this.self.creationDate), createdAt: this.dateToString(this.self.createdAt), updatedAt: this.dateToString(this.self.updatedAt), + federatedCommunities: this.self.federatedCommunities?.map( + (federatedCommunity) => new FederatedCommunityLoggingView(federatedCommunity) + ), } } } diff --git a/database/src/logging/index.ts b/database/src/logging/index.ts index c19bd9a57..522fc3b56 100644 --- a/database/src/logging/index.ts +++ b/database/src/logging/index.ts @@ -11,6 +11,7 @@ import { TransactionLoggingView } from './TransactionLogging.view' import { UserContactLoggingView } from './UserContactLogging.view' import { UserLoggingView } from './UserLogging.view' import { UserRoleLoggingView } from './UserRoleLogging.view' +import { CommunityHandshakeStateLoggingView } from './CommunityHandshakeStateLogging.view' export { AbstractLoggingView, @@ -24,6 +25,7 @@ export { UserContactLoggingView, UserLoggingView, UserRoleLoggingView, + CommunityHandshakeStateLoggingView, } export const logger = getLogger(LOG4JS_BASE_CATEGORY_NAME) diff --git a/database/src/queries/communities.test.ts b/database/src/queries/communities.test.ts index 18975256c..2d208a224 100644 --- a/database/src/queries/communities.test.ts +++ b/database/src/queries/communities.test.ts @@ -1,6 +1,6 @@ import { Community as DbCommunity, FederatedCommunity as DbFederatedCommunity } from '..' import { AppDatabase } from '../AppDatabase' -import { getHomeCommunity, getReachableCommunities } from './communities' +import { getHomeCommunity, getHomeCommunityWithFederatedCommunityOrFail, getReachableCommunities } from './communities' import { describe, expect, it, beforeEach, beforeAll, afterAll } from 'vitest' import { createCommunity, createVerifiedFederatedCommunity } from '../seeds/community' @@ -39,6 +39,24 @@ describe('community.queries', () => { expect(community?.privateKey).toStrictEqual(homeCom.privateKey) }) }) + describe('getHomeCommunityWithFederatedCommunityOrFail', () => { + it('should return the home community with federated communities', async () => { + const homeCom = await createCommunity(false) + await createVerifiedFederatedCommunity('1_0', 100, homeCom) + const community = await getHomeCommunityWithFederatedCommunityOrFail('1_0') + expect(community).toBeDefined() + expect(community?.federatedCommunities).toHaveLength(1) + }) + + it('should throw if no home community exists', async () => { + expect(() => getHomeCommunityWithFederatedCommunityOrFail('1_0')).rejects.toThrow() + }) + + it('should throw if no federated community exists', async () => { + await createCommunity(false) + expect(() => getHomeCommunityWithFederatedCommunityOrFail('1_0')).rejects.toThrow() + }) + }) describe('getReachableCommunities', () => { it('home community counts also to reachable communities', async () => { await createCommunity(false) diff --git a/database/src/queries/communities.ts b/database/src/queries/communities.ts index cdb7cabb6..265be41d4 100644 --- a/database/src/queries/communities.ts +++ b/database/src/queries/communities.ts @@ -10,7 +10,14 @@ export async function getHomeCommunity(): Promise { // TODO: Put in Cache, it is needed nearly always // TODO: return only DbCommunity or throw to reduce unnecessary checks, because there should be always a home community return await DbCommunity.findOne({ - where: { foreign: false }, + where: { foreign: false } + }) +} + +export async function getHomeCommunityWithFederatedCommunityOrFail(apiVersion: string): Promise { + return await DbCommunity.findOneOrFail({ + where: { foreign: false, federatedCommunities: { apiVersion } }, + relations: { federatedCommunities: true }, }) } diff --git a/database/src/queries/communityHandshakes.ts b/database/src/queries/communityHandshakes.ts new file mode 100644 index 000000000..991bd9c47 --- /dev/null +++ b/database/src/queries/communityHandshakes.ts @@ -0,0 +1,15 @@ +import { CommunityHandshakeState } from '../entity' +import { FederatedCommunity } from '../entity/FederatedCommunity' + +/** + * Find a pending community handshake by public key. + * @param publicKey The public key of the community. + * @param withRelations Whether to include the federated community and community in the result, default true. + * @returns The CommunityHandshakeState with associated federated community and community. + */ +export function findPendingCommunityHandshake(federatedCommunity: FederatedCommunity, withRelations = true): Promise { + return CommunityHandshakeState.findOne({ + where: { publicKey: federatedCommunity.publicKey, apiVersion: federatedCommunity.apiVersion }, + relations: withRelations ? { federatedCommunity: { community: true } } : undefined, + }) +} \ No newline at end of file diff --git a/database/src/queries/index.ts b/database/src/queries/index.ts index 1fec568bf..73a2cc15b 100644 --- a/database/src/queries/index.ts +++ b/database/src/queries/index.ts @@ -5,5 +5,6 @@ export * from './communities' export * from './pendingTransactions' export * from './transactions' export * from './transactionLinks' +export * from './communityHandshakes' export const LOG4JS_QUERIES_CATEGORY_NAME = `${LOG4JS_BASE_CATEGORY_NAME}.queries` diff --git a/database/src/seeds/community.ts b/database/src/seeds/community.ts index 4db872398..12a5bd67f 100644 --- a/database/src/seeds/community.ts +++ b/database/src/seeds/community.ts @@ -2,7 +2,13 @@ import { Community, FederatedCommunity } from '../entity' import { randomBytes } from 'node:crypto' import { v4 as uuidv4 } from 'uuid' -export async function createCommunity(foreign: boolean, save: boolean = true): Promise { +/** + * Creates a community. + * @param foreign + * @param store if true, write to db, default: true + * @returns + */ +export async function createCommunity(foreign: boolean, store: boolean = true): Promise { const community = new Community() community.publicKey = randomBytes(32) community.communityUuid = uuidv4() @@ -23,14 +29,22 @@ export async function createCommunity(foreign: boolean, save: boolean = true): P community.description = 'HomeCommunity-description' community.url = 'http://localhost/api' } - return save ? await community.save() : community + return store ? await community.save() : community } +/** + * Creates a verified federated community. + * @param apiVersion + * @param verifiedBeforeMs time in ms before the current time + * @param community + * @param store if true, write to db, default: true + * @returns + */ export async function createVerifiedFederatedCommunity( apiVersion: string, verifiedBeforeMs: number, community: Community, - save: boolean = true + store: boolean = true ): Promise { const federatedCommunity = new FederatedCommunity() federatedCommunity.apiVersion = apiVersion @@ -38,5 +52,5 @@ export async function createVerifiedFederatedCommunity( federatedCommunity.publicKey = community.publicKey federatedCommunity.community = community federatedCommunity.verifiedAt = new Date(Date.now() - verifiedBeforeMs) - return save ? await federatedCommunity.save() : federatedCommunity + return store ? await federatedCommunity.save() : federatedCommunity } From ae23aafd8728c31fb81de270e834fe02983fe3fb Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Mon, 13 Oct 2025 09:27:19 +0200 Subject: [PATCH 07/40] update error handling for startCommunityAuthentication --- .../src/federation/authenticateCommunities.ts | 29 ++++++++++--------- .../logic/CommunityHandshakeState.logic.ts | 15 ++++++++-- database/src/queries/communityHandshakes.ts | 14 +++++++-- .../1_0/resolver/AuthenticationResolver.ts | 11 ++++++- 4 files changed, 50 insertions(+), 19 deletions(-) diff --git a/backend/src/federation/authenticateCommunities.ts b/backend/src/federation/authenticateCommunities.ts index 0a94b24c8..4b2e5b77b 100644 --- a/backend/src/federation/authenticateCommunities.ts +++ b/backend/src/federation/authenticateCommunities.ts @@ -63,29 +63,28 @@ export async function startCommunityAuthentication( // check if a authentication is already in progress const existingState = await findPendingCommunityHandshake(fedComB, false) if (existingState) { - const logic = new CommunityHandshakeStateLogic(existingState) - if (!await logic.isTimeoutUpdate()) { + const stateLogic = new CommunityHandshakeStateLogic(existingState) + // retry on timeout or failure + if (!await stateLogic.isTimeoutUpdate()) { // authentication with community and api version is still in progress and it is not timeout yet methodLogger.debug('existingState', new CommunityHandshakeStateLoggingView(existingState)) return } } - - const state = new DbCommunityHandshakeState() - state.publicKey = fedComB.publicKey - state.apiVersion = fedComB.apiVersion - state.status = CommunityHandshakeStateType.START_COMMUNITY_AUTHENTICATION - state.handshakeId = parseInt(handshakeID) - + const client = AuthenticationClientFactory.getInstance(fedComB) if (client instanceof V1_0_AuthenticationClient) { if (!comB.publicJwtKey) { - state.lastError = 'Public JWT key still not exist for comB ' + comB.name - await state.save() - throw new Error(state.lastError) + throw new Error(`Public JWT key still not exist for comB ${comB.name}`) } + const state = new DbCommunityHandshakeState() + state.publicKey = fedComB.publicKey + state.apiVersion = fedComB.apiVersion + state.status = CommunityHandshakeStateType.START_COMMUNITY_AUTHENTICATION + state.handshakeId = parseInt(handshakeID) const stateSaveResolver = state.save() + //create JWT with url in payload encrypted by foreignCom.publicJwtKey and signed with homeCom.privateJwtKey const payload = new OpenConnectionJwtPayloadType(handshakeID, ensureUrlEndsWithSlash(homeFedComA.endPoint).concat(homeFedComA.apiVersion), @@ -104,7 +103,11 @@ export async function startCommunityAuthentication( if (result) { methodLogger.info(`successful initiated at community:`, fedComB.endPoint) } else { - methodLogger.error(`can't initiate at community:`, fedComB.endPoint) + const errorMsg = `can't initiate at community: ${fedComB.endPoint}` + methodLogger.error(errorMsg) + state.status = CommunityHandshakeStateType.FAILED + state.lastError = errorMsg + await state.save() } } } diff --git a/core/src/logic/CommunityHandshakeState.logic.ts b/core/src/logic/CommunityHandshakeState.logic.ts index a3e8631eb..03bf9a762 100644 --- a/core/src/logic/CommunityHandshakeState.logic.ts +++ b/core/src/logic/CommunityHandshakeState.logic.ts @@ -9,14 +9,25 @@ export class CommunityHandshakeStateLogic { * @returns true if the community handshake state is expired */ public async isTimeoutUpdate(): Promise { + const timeout = this.isTimeout() + if (timeout && this.communityHandshakeStateEntity.status !== CommunityHandshakeStateType.EXPIRED) { + this.communityHandshakeStateEntity.status = CommunityHandshakeStateType.EXPIRED + await this.communityHandshakeStateEntity.save() + } + return timeout + } + + public isTimeout(): boolean { if (this.communityHandshakeStateEntity.status === CommunityHandshakeStateType.EXPIRED) { return true } if (Date.now() - this.communityHandshakeStateEntity.updatedAt.getTime() > FEDERATION_AUTHENTICATION_TIMEOUT_MS) { - this.communityHandshakeStateEntity.status = CommunityHandshakeStateType.EXPIRED - await this.communityHandshakeStateEntity.save() return true } return false } + + public isFailed(): boolean { + return this.communityHandshakeStateEntity.status === CommunityHandshakeStateType.FAILED + } } diff --git a/database/src/queries/communityHandshakes.ts b/database/src/queries/communityHandshakes.ts index 991bd9c47..3248b311e 100644 --- a/database/src/queries/communityHandshakes.ts +++ b/database/src/queries/communityHandshakes.ts @@ -1,5 +1,5 @@ -import { CommunityHandshakeState } from '../entity' -import { FederatedCommunity } from '../entity/FederatedCommunity' +import { Not, In } from 'typeorm' +import { CommunityHandshakeState, CommunityHandshakeStateType, FederatedCommunity} from '..' /** * Find a pending community handshake by public key. @@ -9,7 +9,15 @@ import { FederatedCommunity } from '../entity/FederatedCommunity' */ export function findPendingCommunityHandshake(federatedCommunity: FederatedCommunity, withRelations = true): Promise { return CommunityHandshakeState.findOne({ - where: { publicKey: federatedCommunity.publicKey, apiVersion: federatedCommunity.apiVersion }, + where: { + publicKey: federatedCommunity.publicKey, + apiVersion: federatedCommunity.apiVersion, + status: Not(In([ + CommunityHandshakeStateType.EXPIRED, + CommunityHandshakeStateType.FAILED, + CommunityHandshakeStateType.SUCCESS + ])) + }, relations: withRelations ? { federatedCommunity: { community: true } } : undefined, }) } \ No newline at end of file diff --git a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts index fd948300e..dc6e1347b 100644 --- a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts @@ -6,9 +6,11 @@ import { Community as DbCommunity, FederatedCommunity as DbFedCommunity, FederatedCommunityLoggingView, + CommunityHandshakeStateType, + CommunityHandshakeState as DbCommunityHandshakeState, getHomeCommunity, } from 'database' -import { getLogger } from 'log4js' +import { getLogger, Logger } from 'log4js' import { AuthenticationJwtPayloadType, AuthenticationResponseJwtPayloadType, @@ -25,6 +27,13 @@ const createLogger = (method: string ) => getLogger(`${LOG4JS_BASE_CATEGORY_NAM @Resolver() export class AuthenticationResolver { + + private async errorState(errmsg: string, methodLogger: Logger, state: DbCommunityHandshakeState) { + methodLogger.error(errmsg) + state.lastError = errmsg + await state.save() + } + @Mutation(() => Boolean) async openConnection( @Arg('data') From ced6f42fa0c9214764ca85d3592bc3474bb28544 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Mon, 13 Oct 2025 10:11:27 +0200 Subject: [PATCH 08/40] refactor startOpenConnectionCallback --- .../src/federation/authenticateCommunities.ts | 15 +-- core/src/logic/Community.logic.ts | 13 ++ .../logic/CommunityHandshakeState.logic.ts | 14 +- core/src/logic/index.ts | 3 +- .../src/enum/CommunityHandshakeStateType.ts | 2 +- database/src/queries/communities.ts | 10 ++ database/src/queries/communityHandshakes.ts | 10 +- .../1_0/resolver/AuthenticationResolver.ts | 10 +- .../api/1_0/util/authenticateCommunity.ts | 123 ++++++++++++------ shared/src/schema/community.schema.ts | 4 +- 10 files changed, 126 insertions(+), 78 deletions(-) create mode 100644 core/src/logic/Community.logic.ts diff --git a/backend/src/federation/authenticateCommunities.ts b/backend/src/federation/authenticateCommunities.ts index 4b2e5b77b..d71314efa 100644 --- a/backend/src/federation/authenticateCommunities.ts +++ b/backend/src/federation/authenticateCommunities.ts @@ -21,6 +21,7 @@ import { getLogger } from 'log4js' import { AuthenticationClientFactory } from './client/AuthenticationClientFactory' import { EncryptedTransferArgs } from 'core' import { CommunityHandshakeStateLogic } from 'core' +import { CommunityLogic } from 'core' const createLogger = (functionName: string) => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.authenticateCommunities.${functionName}`) @@ -35,16 +36,8 @@ export async function startCommunityAuthentication( }) const homeComA = await getHomeCommunityWithFederatedCommunityOrFail(fedComB.apiVersion) methodLogger.debug('homeComA', new CommunityLoggingView(homeComA)) - // check if result is like expected - // TODO: use zod/valibot - if ( - !homeComA.federatedCommunities || - homeComA.federatedCommunities.length === 0 || - homeComA.federatedCommunities[0].apiVersion !== fedComB.apiVersion - ) { - throw new Error(`Missing home community or federated community with api version ${fedComB.apiVersion}`) - } - const homeFedComA = homeComA.federatedCommunities[0] + const homeComALogic = new CommunityLogic(homeComA) + const homeFedComA = homeComALogic.getFederatedCommunityWithApiOrFail(fedComB.apiVersion) const comB = await DbCommunity.findOneByOrFail({ publicKey: fedComB.publicKey }) methodLogger.debug('started with comB:', new CommunityLoggingView(comB)) // check if communityUuid is not a valid v4Uuid @@ -61,7 +54,7 @@ export async function startCommunityAuthentication( ) // check if a authentication is already in progress - const existingState = await findPendingCommunityHandshake(fedComB, false) + const existingState = await findPendingCommunityHandshake(fedComB.publicKey, fedComB.apiVersion, false) if (existingState) { const stateLogic = new CommunityHandshakeStateLogic(existingState) // retry on timeout or failure diff --git a/core/src/logic/Community.logic.ts b/core/src/logic/Community.logic.ts new file mode 100644 index 000000000..40779c703 --- /dev/null +++ b/core/src/logic/Community.logic.ts @@ -0,0 +1,13 @@ +import { Community as DbCommunity, FederatedCommunity as DbFederatedCommunity } from 'database' + +export class CommunityLogic { + public constructor(private self: DbCommunity) {} + + public getFederatedCommunityWithApiOrFail(apiVersion: string): DbFederatedCommunity { + const fedCom = this.self.federatedCommunities?.find((fedCom) => fedCom.apiVersion === apiVersion) + if (!fedCom) { + throw new Error(`Missing federated community with api version ${apiVersion}`) + } + return fedCom + } +} \ No newline at end of file diff --git a/core/src/logic/CommunityHandshakeState.logic.ts b/core/src/logic/CommunityHandshakeState.logic.ts index 03bf9a762..e9cb23d5c 100644 --- a/core/src/logic/CommunityHandshakeState.logic.ts +++ b/core/src/logic/CommunityHandshakeState.logic.ts @@ -2,7 +2,7 @@ import { CommunityHandshakeState, CommunityHandshakeStateType } from 'database' import { FEDERATION_AUTHENTICATION_TIMEOUT_MS } from 'shared' export class CommunityHandshakeStateLogic { - public constructor(private communityHandshakeStateEntity: CommunityHandshakeState) {} + public constructor(private self: CommunityHandshakeState) {} /** * Check for expired state and if not, check timeout and update (write into db) to expired state @@ -10,24 +10,24 @@ export class CommunityHandshakeStateLogic { */ public async isTimeoutUpdate(): Promise { const timeout = this.isTimeout() - if (timeout && this.communityHandshakeStateEntity.status !== CommunityHandshakeStateType.EXPIRED) { - this.communityHandshakeStateEntity.status = CommunityHandshakeStateType.EXPIRED - await this.communityHandshakeStateEntity.save() + if (timeout && this.self.status !== CommunityHandshakeStateType.EXPIRED) { + this.self.status = CommunityHandshakeStateType.EXPIRED + await this.self.save() } return timeout } public isTimeout(): boolean { - if (this.communityHandshakeStateEntity.status === CommunityHandshakeStateType.EXPIRED) { + if (this.self.status === CommunityHandshakeStateType.EXPIRED) { return true } - if (Date.now() - this.communityHandshakeStateEntity.updatedAt.getTime() > FEDERATION_AUTHENTICATION_TIMEOUT_MS) { + if (Date.now() - this.self.updatedAt.getTime() > FEDERATION_AUTHENTICATION_TIMEOUT_MS) { return true } return false } public isFailed(): boolean { - return this.communityHandshakeStateEntity.status === CommunityHandshakeStateType.FAILED + return this.self.status === CommunityHandshakeStateType.FAILED } } diff --git a/core/src/logic/index.ts b/core/src/logic/index.ts index 3a5c48f58..809465850 100644 --- a/core/src/logic/index.ts +++ b/core/src/logic/index.ts @@ -1 +1,2 @@ -export { CommunityHandshakeStateLogic } from './CommunityHandshakeState.logic' \ No newline at end of file +export { CommunityHandshakeStateLogic } from './CommunityHandshakeState.logic' +export { CommunityLogic } from './Community.logic' \ No newline at end of file diff --git a/database/src/enum/CommunityHandshakeStateType.ts b/database/src/enum/CommunityHandshakeStateType.ts index a047e740d..5378974aa 100644 --- a/database/src/enum/CommunityHandshakeStateType.ts +++ b/database/src/enum/CommunityHandshakeStateType.ts @@ -1,6 +1,6 @@ export enum CommunityHandshakeStateType { START_COMMUNITY_AUTHENTICATION = 'START_COMMUNITY_AUTHENTICATION', - OPEN_CONNECTION = 'OPEN_CONNECTION', + START_OPEN_CONNECTION_CALLBACK = 'START_OPEN_CONNECTION_CALLBACK', OPEN_CONNECTION_CALLBACK = 'OPEN_CONNECTION_CALLBACK', SUCCESS = 'SUCCESS', diff --git a/database/src/queries/communities.ts b/database/src/queries/communities.ts index 265be41d4..22bfdb39f 100644 --- a/database/src/queries/communities.ts +++ b/database/src/queries/communities.ts @@ -49,6 +49,16 @@ export async function getCommunityWithFederatedCommunityByIdentifier( }) } +export async function getCommunityWithFederatedCommunityWithApiOrFail( + publicKey: Buffer, + apiVersion: string +): Promise { + return await DbCommunity.findOneOrFail({ + where: { foreign: true, publicKey, federatedCommunities: { apiVersion } }, + relations: { federatedCommunities: true }, + }) +} + // returns all reachable communities // home community and all federated communities which have been verified within the last authenticationTimeoutMs export async function getReachableCommunities( diff --git a/database/src/queries/communityHandshakes.ts b/database/src/queries/communityHandshakes.ts index 3248b311e..3902fb26b 100644 --- a/database/src/queries/communityHandshakes.ts +++ b/database/src/queries/communityHandshakes.ts @@ -1,5 +1,5 @@ import { Not, In } from 'typeorm' -import { CommunityHandshakeState, CommunityHandshakeStateType, FederatedCommunity} from '..' +import { CommunityHandshakeState, CommunityHandshakeStateType} from '..' /** * Find a pending community handshake by public key. @@ -7,11 +7,13 @@ import { CommunityHandshakeState, CommunityHandshakeStateType, FederatedCommunit * @param withRelations Whether to include the federated community and community in the result, default true. * @returns The CommunityHandshakeState with associated federated community and community. */ -export function findPendingCommunityHandshake(federatedCommunity: FederatedCommunity, withRelations = true): Promise { +export function findPendingCommunityHandshake( + publicKey: Buffer, apiVersion: string, withRelations = true +): Promise { return CommunityHandshakeState.findOne({ where: { - publicKey: federatedCommunity.publicKey, - apiVersion: federatedCommunity.apiVersion, + publicKey, + apiVersion, status: Not(In([ CommunityHandshakeStateType.EXPIRED, CommunityHandshakeStateType.FAILED, diff --git a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts index dc6e1347b..74f75239e 100644 --- a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts @@ -6,11 +6,9 @@ import { Community as DbCommunity, FederatedCommunity as DbFedCommunity, FederatedCommunityLoggingView, - CommunityHandshakeStateType, - CommunityHandshakeState as DbCommunityHandshakeState, getHomeCommunity, } from 'database' -import { getLogger, Logger } from 'log4js' +import { getLogger } from 'log4js' import { AuthenticationJwtPayloadType, AuthenticationResponseJwtPayloadType, @@ -28,12 +26,6 @@ const createLogger = (method: string ) => getLogger(`${LOG4JS_BASE_CATEGORY_NAM @Resolver() export class AuthenticationResolver { - private async errorState(errmsg: string, methodLogger: Logger, state: DbCommunityHandshakeState) { - methodLogger.error(errmsg) - state.lastError = errmsg - await state.save() - } - @Mutation(() => Boolean) async openConnection( @Arg('data') diff --git a/federation/src/graphql/api/1_0/util/authenticateCommunity.ts b/federation/src/graphql/api/1_0/util/authenticateCommunity.ts index 4d8af9187..3ffd4b3d5 100644 --- a/federation/src/graphql/api/1_0/util/authenticateCommunity.ts +++ b/federation/src/graphql/api/1_0/util/authenticateCommunity.ts @@ -1,12 +1,16 @@ -import { EncryptedTransferArgs } from 'core' +import { CommunityHandshakeStateLogic, CommunityLogic, EncryptedTransferArgs, ensureUrlEndsWithSlash } from 'core' import { + CommunityHandshakeStateLoggingView, CommunityLoggingView, Community as DbCommunity, FederatedCommunity as DbFedCommunity, FederatedCommunityLoggingView, + findPendingCommunityHandshake, + getCommunityWithFederatedCommunityWithApiOrFail, getHomeCommunity, + getHomeCommunityWithFederatedCommunityOrFail, } from 'database' -import { getLogger } from 'log4js' +import { getLogger, Logger } from 'log4js' import { validate as validateUUID, version as versionUUID } from 'uuid' import { AuthenticationClientFactory } from '@/client/AuthenticationClientFactory' @@ -14,80 +18,113 @@ import { randombytes_random } from 'sodium-native' import { AuthenticationClient as V1_0_AuthenticationClient } from '@/client/1_0/AuthenticationClient' import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' -import { AuthenticationJwtPayloadType, AuthenticationResponseJwtPayloadType, encryptAndSign, OpenConnectionCallbackJwtPayloadType, uint32Schema, uuidv4Schema, verifyAndDecrypt } from 'shared' -import { FEDERATION_AUTHENTICATION_TIMEOUT_MS } from 'shared' +import { + AuthenticationJwtPayloadType, + AuthenticationResponseJwtPayloadType, + encryptAndSign, + OpenConnectionCallbackJwtPayloadType, + verifyAndDecrypt +} from 'shared' +import { CommunityHandshakeState as DbCommunityHandshakeState, CommunityHandshakeStateType } from 'database' -const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.util.authenticateCommunity`) +const createLogger = (method: string ) => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.util.authenticateCommunity.${method}`) + +async function errorState( + error: string, + methodLogger: Logger, + state: DbCommunityHandshakeState, +): Promise { + methodLogger.error(error) + state.status = CommunityHandshakeStateType.FAILED + state.lastError = error + return state.save() +} export async function startOpenConnectionCallback( handshakeID: string, publicKey: string, api: string, ): Promise { - const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.util.authenticateCommunity.startOpenConnectionCallback`) + const methodLogger = createLogger('startOpenConnectionCallback') methodLogger.addContext('handshakeID', handshakeID) methodLogger.debug(`Authentication: startOpenConnectionCallback() with:`, { publicKey, }) - try { - const homeComB = await getHomeCommunity() - const homeFedComB = await DbFedCommunity.findOneByOrFail({ - foreign: false, - apiVersion: api, - }) - const comA = await DbCommunity.findOneByOrFail({ publicKey: Buffer.from(publicKey, 'hex') }) - const fedComA = await DbFedCommunity.findOneByOrFail({ - foreign: true, - apiVersion: api, - publicKey: comA.publicKey, - }) - // store oneTimeCode in requestedCom.community_uuid as authenticate-request-identifier - // prevent overwriting valid UUID with oneTimeCode, because this request could be initiated at any time from federated community - if (uuidv4Schema.safeParse(comA.communityUuid).success) { - methodLogger.debug('Community UUID is already a valid UUID') + const publicKeyBuffer = Buffer.from(publicKey, 'hex') + const pendingState = await findPendingCommunityHandshake(publicKeyBuffer, api, false) + if (pendingState) { + const stateLogic = new CommunityHandshakeStateLogic(pendingState) + // retry on timeout or failure + if (!await stateLogic.isTimeoutUpdate()) { + // authentication with community and api version is still in progress and it is not timeout yet + methodLogger.debug('existingState', new CommunityHandshakeStateLoggingView(pendingState)) return - // check for still ongoing authentication, but with timeout - } else if (uint32Schema.safeParse(Number(comA.communityUuid)).success) { - if (comA.updatedAt && (Date.now() - comA.updatedAt.getTime()) < FEDERATION_AUTHENTICATION_TIMEOUT_MS) { - methodLogger.debug('Community UUID is still in authentication...oneTimeCode=', comA.communityUuid) - return - } } + } + let stateSaveResolver: Promise | undefined = undefined + const state = new DbCommunityHandshakeState() + try { + const [homeComB, comA] = await Promise.all([ + getHomeCommunityWithFederatedCommunityOrFail(api), + getCommunityWithFederatedCommunityWithApiOrFail(publicKeyBuffer, api), + ]) + // load helpers + const homeComBLogic = new CommunityLogic(homeComB) + const comALogic = new CommunityLogic(comA) + // get federated communities with correct api version + const homeFedComB = homeComBLogic.getFederatedCommunityWithApiOrFail(api) + const fedComA = comALogic.getFederatedCommunityWithApiOrFail(api) + // TODO: make sure it is unique - const oneTimeCode = randombytes_random().toString() - comA.communityUuid = oneTimeCode - await DbCommunity.save(comA) + const oneTimeCode = randombytes_random() + const oneTimeCodeString = oneTimeCode.toString() + + state.publicKey = publicKeyBuffer + state.apiVersion = api + state.status = CommunityHandshakeStateType.START_OPEN_CONNECTION_CALLBACK + state.handshakeId = parseInt(handshakeID) + state.oneTimeCode = oneTimeCode + stateSaveResolver = state.save() methodLogger.debug( - `Authentication: stored oneTimeCode in requestedCom:`, - new CommunityLoggingView(comA), + `Authentication: store oneTimeCode in CommunityHandshakeState:`, + new CommunityHandshakeStateLoggingView(state), ) const client = AuthenticationClientFactory.getInstance(fedComA) if (client instanceof V1_0_AuthenticationClient) { - const url = homeFedComB.endPoint.endsWith('/') - ? homeFedComB.endPoint + homeFedComB.apiVersion - : homeFedComB.endPoint + '/' + homeFedComB.apiVersion + const url = ensureUrlEndsWithSlash(homeFedComB.endPoint) + homeFedComB.apiVersion - const callbackArgs = new OpenConnectionCallbackJwtPayloadType(handshakeID, oneTimeCode, url) + const callbackArgs = new OpenConnectionCallbackJwtPayloadType(handshakeID, oneTimeCodeString, url) methodLogger.debug(`Authentication: start openConnectionCallback with args:`, callbackArgs) // encrypt callbackArgs with requestedCom.publicJwtKey and sign it with homeCom.privateJwtKey - const jwt = await encryptAndSign(callbackArgs, homeComB!.privateJwtKey!, comA.publicJwtKey!) + const jwt = await encryptAndSign(callbackArgs, homeComB.privateJwtKey!, comA.publicJwtKey!) const args = new EncryptedTransferArgs() - args.publicKey = homeComB!.publicKey.toString('hex') + args.publicKey = homeComB.publicKey.toString('hex') args.jwt = jwt args.handshakeID = handshakeID + await stateSaveResolver const result = await client.openConnectionCallback(args) if (result) { - methodLogger.debug('startOpenConnectionCallback() successful:', jwt) + methodLogger.debug(`startOpenConnectionCallback() successful: ${jwt}`) } else { - methodLogger.error('startOpenConnectionCallback() failed:', jwt) + methodLogger.debug(`jwt: ${jwt}`) + stateSaveResolver = errorState('startOpenConnectionCallback() failed', methodLogger, state) } } } catch (err) { - methodLogger.error('error in startOpenConnectionCallback:', err) + let errorString: string = '' + if (err instanceof Error) { + errorString = err.message + } else { + errorString = String(err) + } + stateSaveResolver = errorState(`error in startOpenConnectionCallback: ${errorString}`, methodLogger, state) + } finally { + if (stateSaveResolver) { + await stateSaveResolver + } } - methodLogger.removeContext('handshakeID') } export async function startAuthentication( diff --git a/shared/src/schema/community.schema.ts b/shared/src/schema/community.schema.ts index 6067e3538..6957b16b0 100644 --- a/shared/src/schema/community.schema.ts +++ b/shared/src/schema/community.schema.ts @@ -1,7 +1,7 @@ -import { object, date } from 'zod' +import { object, date, array, string } from 'zod' import { uuidv4Schema } from './base.schema' export const communityAuthenticatedSchema = object({ communityUuid: uuidv4Schema, authenticatedAt: date(), -}) \ No newline at end of file +}) From 6e13f5d8ab558c6e0379b2583d74a1c1fb4f6624 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Mon, 13 Oct 2025 12:53:04 +0200 Subject: [PATCH 09/40] finish refactor authentication with states --- .../src/federation/authenticateCommunities.ts | 2 +- .../logic/interpretEncryptedTransferArgs.ts | 16 ++--- core/src/util/utilities.ts | 5 ++ .../src/enum/CommunityHandshakeStateType.ts | 1 + database/src/queries/communityHandshakes.ts | 12 +++- .../1_0/resolver/AuthenticationResolver.ts | 69 ++++++++++++------- .../api/1_0/util/authenticateCommunity.ts | 51 ++++++++++---- shared/src/jwt/JWT.ts | 10 --- 8 files changed, 110 insertions(+), 56 deletions(-) diff --git a/backend/src/federation/authenticateCommunities.ts b/backend/src/federation/authenticateCommunities.ts index d71314efa..78fea419f 100644 --- a/backend/src/federation/authenticateCommunities.ts +++ b/backend/src/federation/authenticateCommunities.ts @@ -100,7 +100,7 @@ export async function startCommunityAuthentication( methodLogger.error(errorMsg) state.status = CommunityHandshakeStateType.FAILED state.lastError = errorMsg - await state.save() } + await state.save() } } diff --git a/core/src/graphql/logic/interpretEncryptedTransferArgs.ts b/core/src/graphql/logic/interpretEncryptedTransferArgs.ts index 301f6da16..31bf47a56 100644 --- a/core/src/graphql/logic/interpretEncryptedTransferArgs.ts +++ b/core/src/graphql/logic/interpretEncryptedTransferArgs.ts @@ -6,37 +6,35 @@ import { CommunityLoggingView, getHomeCommunity } from 'database' import { verifyAndDecrypt } from 'shared' import { LOG4JS_BASE_CATEGORY_NAME } from '../../config/const' -const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.logic.interpretEncryptedTransferArgs`) +const createLogger = (functionName: string) => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.logic.interpretEncryptedTransferArgs.${functionName}`) export const interpretEncryptedTransferArgs = async (args: EncryptedTransferArgs): Promise => { - const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.logic.interpretEncryptedTransferArgs-method`) + const methodLogger = createLogger('interpretEncryptedTransferArgs') methodLogger.addContext('handshakeID', args.handshakeID) methodLogger.debug('interpretEncryptedTransferArgs()... args:', args) // first find with args.publicKey the community 'requestingCom', which starts the request + // TODO: maybe use community from caller instead of loading it separately const requestingCom = await DbCommunity.findOneBy({ publicKey: Buffer.from(args.publicKey, 'hex') }) if (!requestingCom) { - const errmsg = `unknown requesting community with publicKey ${Buffer.from(args.publicKey, 'hex')}` + const errmsg = `unknown requesting community with publicKey ${args.publicKey}` methodLogger.error(errmsg) - methodLogger.removeContext('handshakeID') throw new Error(errmsg) } if (!requestingCom.publicJwtKey) { - const errmsg = `missing publicJwtKey of requesting community with publicKey ${Buffer.from(args.publicKey, 'hex')}` + const errmsg = `missing publicJwtKey of requesting community with publicKey ${args.publicKey}` methodLogger.error(errmsg) - methodLogger.removeContext('handshakeID') throw new Error(errmsg) } methodLogger.debug(`found requestingCom:`, new CommunityLoggingView(requestingCom)) // verify the signing of args.jwt with homeCom.privateJwtKey and decrypt args.jwt with requestingCom.publicJwtKey + // TODO: maybe use community from caller instead of loading it separately const homeCom = await getHomeCommunity() const jwtPayload = await verifyAndDecrypt(args.handshakeID, args.jwt, homeCom!.privateJwtKey!, requestingCom.publicJwtKey) as JwtPayloadType if (!jwtPayload) { - const errmsg = `invalid payload of community with publicKey ${Buffer.from(args.publicKey, 'hex')}` + const errmsg = `invalid payload of community with publicKey ${args.publicKey}` methodLogger.error(errmsg) - methodLogger.removeContext('handshakeID') throw new Error(errmsg) } methodLogger.debug('jwtPayload', jwtPayload) - methodLogger.removeContext('handshakeID') return jwtPayload } diff --git a/core/src/util/utilities.ts b/core/src/util/utilities.ts index 0e8a8da85..be32736fc 100644 --- a/core/src/util/utilities.ts +++ b/core/src/util/utilities.ts @@ -36,6 +36,11 @@ export const delay = promisify(setTimeout) export const ensureUrlEndsWithSlash = (url: string): string => { return url.endsWith('/') ? url : url.concat('/') } +export function splitUrlInEndPointAndApiVersion(url: string): { endPoint: string, apiVersion: string } { + const endPoint = url.slice(0, url.lastIndexOf('/') + 1) + const apiVersion = url.slice(url.lastIndexOf('/') + 1, url.length) + return { endPoint, apiVersion } +} /** * Calculates the date representing the first day of the month, a specified number of months prior to a given date. * diff --git a/database/src/enum/CommunityHandshakeStateType.ts b/database/src/enum/CommunityHandshakeStateType.ts index 5378974aa..8b811da61 100644 --- a/database/src/enum/CommunityHandshakeStateType.ts +++ b/database/src/enum/CommunityHandshakeStateType.ts @@ -1,6 +1,7 @@ export enum CommunityHandshakeStateType { START_COMMUNITY_AUTHENTICATION = 'START_COMMUNITY_AUTHENTICATION', START_OPEN_CONNECTION_CALLBACK = 'START_OPEN_CONNECTION_CALLBACK', + START_AUTHENTICATION = 'START_AUTHENTICATION', OPEN_CONNECTION_CALLBACK = 'OPEN_CONNECTION_CALLBACK', SUCCESS = 'SUCCESS', diff --git a/database/src/queries/communityHandshakes.ts b/database/src/queries/communityHandshakes.ts index 3902fb26b..f8fa2ba89 100644 --- a/database/src/queries/communityHandshakes.ts +++ b/database/src/queries/communityHandshakes.ts @@ -22,4 +22,14 @@ export function findPendingCommunityHandshake( }, relations: withRelations ? { federatedCommunity: { community: true } } : undefined, }) -} \ No newline at end of file +} + +export function findPendingCommunityHandshakeOrFailByOneTimeCode( + oneTimeCode: number +): Promise { + return CommunityHandshakeState.findOneOrFail({ + where: { oneTimeCode }, + relations: { federatedCommunity: { community: true } }, + }) +} + \ No newline at end of file diff --git a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts index 74f75239e..40b5e2c18 100644 --- a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts @@ -1,12 +1,17 @@ import { CONFIG } from '@/config' import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' -import { EncryptedTransferArgs, interpretEncryptedTransferArgs } from 'core' +import { CommunityHandshakeStateLogic, EncryptedTransferArgs, interpretEncryptedTransferArgs, splitUrlInEndPointAndApiVersion } from 'core' import { CommunityLoggingView, + CommunityHandshakeStateLoggingView, + CommunityHandshakeState as DbCommunityHandshakeState, + CommunityHandshakeStateType, Community as DbCommunity, FederatedCommunity as DbFedCommunity, FederatedCommunityLoggingView, getHomeCommunity, + findPendingCommunityHandshake, + findPendingCommunityHandshakeOrFailByOneTimeCode, } from 'database' import { getLogger } from 'log4js' import { @@ -93,9 +98,7 @@ export class AuthenticationResolver { // no infos to the caller return true } - - const endPoint = openConnectionCallbackJwtPayload.url.slice(0, openConnectionCallbackJwtPayload.url.lastIndexOf('/') + 1) - const apiVersion = openConnectionCallbackJwtPayload.url.slice(openConnectionCallbackJwtPayload.url.lastIndexOf('/') + 1, openConnectionCallbackJwtPayload.url.length) + const { endPoint, apiVersion } = splitUrlInEndPointAndApiVersion(openConnectionCallbackJwtPayload.url) methodLogger.debug(`search fedComB per:`, endPoint, apiVersion) const fedComB = await DbFedCommunity.findOneBy({ endPoint, apiVersion }) if (!fedComB) { @@ -126,45 +129,47 @@ export class AuthenticationResolver { const methodLogger = createLogger('authenticate') methodLogger.addContext('handshakeID', args.handshakeID) methodLogger.debug(`authenticate() via apiVersion=1_0 ...`, args) + let state: DbCommunityHandshakeState | null = null + let stateSaveResolver: Promise | undefined = undefined try { const authArgs = await interpretEncryptedTransferArgs(args) as AuthenticationJwtPayloadType methodLogger.debug(`interpreted authentication payload...authArgs:`, authArgs) if (!authArgs) { - const errmsg = `invalid authentication payload of requesting community with publicKey` + args.publicKey - methodLogger.error(errmsg) - // no infos to the caller - return null + throw new Error(`invalid authentication payload of requesting community with publicKey ${args.publicKey}`) } if (!uint32Schema.safeParse(Number(authArgs.oneTimeCode)).success) { - const errmsg = `invalid oneTimeCode: ${authArgs.oneTimeCode} for community with publicKey ${authArgs.publicKey}, expect uint32` - methodLogger.error(errmsg) - // no infos to the caller - return null + throw new Error( + `invalid oneTimeCode: ${authArgs.oneTimeCode} for community with publicKey ${authArgs.publicKey}, expect uint32` + ) } + state = await findPendingCommunityHandshakeOrFailByOneTimeCode(Number(authArgs.oneTimeCode)) + const stateLogic = new CommunityHandshakeStateLogic(state) + if (await stateLogic.isTimeoutUpdate() || state.status !== CommunityHandshakeStateType.START_OPEN_CONNECTION_CALLBACK) { + throw new Error('No valid pending community handshake found') + } + state.status = CommunityHandshakeStateType.SUCCESS + stateSaveResolver = state.save() + methodLogger.debug(`search community per oneTimeCode:`, authArgs.oneTimeCode) - const authCom = await DbCommunity.findOneByOrFail({ communityUuid: authArgs.oneTimeCode }) + const authCom = state.federatedCommunity.community if (authCom) { methodLogger.debug('found authCom:', new CommunityLoggingView(authCom)) methodLogger.debug('authCom.publicKey', authCom.publicKey.toString('hex')) methodLogger.debug('args.publicKey', args.publicKey) if (authCom.publicKey.compare(Buffer.from(args.publicKey, 'hex')) !== 0) { - const errmsg = `corrupt authentication call detected, oneTimeCode: ${authArgs.oneTimeCode} doesn't belong to caller: ${args.publicKey}` - methodLogger.error(errmsg) - // no infos to the caller - return null + throw new Error( + `corrupt authentication call detected, oneTimeCode: ${authArgs.oneTimeCode} doesn't belong to caller: ${args.publicKey}` + ) } const communityUuid = uuidv4Schema.safeParse(authArgs.uuid) if (!communityUuid.success) { - const errmsg = `invalid uuid: ${authArgs.uuid} for community with publicKey ${authCom.publicKey}` - methodLogger.error(errmsg) - // no infos to the caller - return null + throw new Error(`invalid uuid: ${authArgs.uuid} for community with publicKey ${authCom.publicKey}`) } authCom.communityUuid = communityUuid.data authCom.authenticatedAt = new Date() - await DbCommunity.save(authCom) + await authCom.save() methodLogger.debug('store authCom.uuid successfully:', new CommunityLoggingView(authCom)) const homeComB = await getHomeCommunity() if (homeComB?.communityUuid) { @@ -175,8 +180,26 @@ export class AuthenticationResolver { } return null } catch (err) { - methodLogger.error('invalid jwt token:', err) + let errorString = '' + if (err instanceof Error) { + errorString = err.message + } else { + errorString = String(err) + } + if (state) { + methodLogger.info(`state: ${new CommunityHandshakeStateLoggingView(state)}`) + state.status = CommunityHandshakeStateType.FAILED + state.lastError = errorString + stateSaveResolver = state.save() + } + methodLogger.error(`failed: ${errorString}`) + // no infos to the caller return null + } finally { + if (stateSaveResolver) { + await stateSaveResolver + } } + } } diff --git a/federation/src/graphql/api/1_0/util/authenticateCommunity.ts b/federation/src/graphql/api/1_0/util/authenticateCommunity.ts index 3ffd4b3d5..d901a84c3 100644 --- a/federation/src/graphql/api/1_0/util/authenticateCommunity.ts +++ b/federation/src/graphql/api/1_0/util/authenticateCommunity.ts @@ -132,12 +132,14 @@ export async function startAuthentication( oneTimeCode: string, fedComB: DbFedCommunity, ): Promise { - const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.util.authenticateCommunity.startAuthentication`) + const methodLogger = createLogger('startAuthentication') methodLogger.addContext('handshakeID', handshakeID) methodLogger.debug(`startAuthentication()...`, { oneTimeCode, fedComB: new FederatedCommunityLoggingView(fedComB), }) + let state: DbCommunityHandshakeState | null = null + let stateSaveResolver: Promise | undefined = undefined try { const homeComA = await getHomeCommunity() const comB = await DbCommunity.findOneByOrFail({ @@ -147,6 +149,17 @@ export async function startAuthentication( if (!comB.publicJwtKey) { throw new Error('Public JWT key still not exist for foreign community') } + state = await findPendingCommunityHandshake(fedComB.publicKey, fedComB.apiVersion, false) + if (!state) { + throw new Error('No pending community handshake found') + } + const stateLogic = new CommunityHandshakeStateLogic(state) + if (await stateLogic.isTimeoutUpdate() || state.status !== CommunityHandshakeStateType.START_COMMUNITY_AUTHENTICATION) { + methodLogger.debug('invalid state', new CommunityHandshakeStateLoggingView(state)) + throw new Error('No valid pending community handshake found') + } + state.status = CommunityHandshakeStateType.START_AUTHENTICATION + stateSaveResolver = state.save() const client = AuthenticationClientFactory.getInstance(fedComB) @@ -161,6 +174,7 @@ export async function startAuthentication( methodLogger.debug(`invoke authenticate() with:`, args) const responseJwt = await client.authenticate(args) methodLogger.debug(`response of authenticate():`, responseJwt) + if (responseJwt !== null) { const payload = await verifyAndDecrypt(handshakeID, responseJwt, homeComA!.privateJwtKey!, comB.publicJwtKey!) as AuthenticationResponseJwtPayloadType methodLogger.debug( @@ -169,27 +183,40 @@ export async function startAuthentication( new FederatedCommunityLoggingView(fedComB), ) if (payload.tokentype !== AuthenticationResponseJwtPayloadType.AUTHENTICATION_RESPONSE_TYPE) { - const errmsg = `Invalid tokentype in authenticate-response of community with publicKey` + comB.publicKey - methodLogger.error(errmsg) - methodLogger.removeContext('handshakeID') - throw new Error(errmsg) + throw new Error(`Invalid tokentype in authenticate-response of community with publicKey ${comB.publicKey}`) } if (!payload.uuid || !validateUUID(payload.uuid) || versionUUID(payload.uuid) !== 4) { - const errmsg = `Invalid uuid in authenticate-response of community with publicKey` + comB.publicKey - methodLogger.error(errmsg) - methodLogger.removeContext('handshakeID') - throw new Error(errmsg) + throw new Error(`Invalid uuid in authenticate-response of community with publicKey ${comB.publicKey}`) } comB.communityUuid = payload.uuid comB.authenticatedAt = new Date() - await DbCommunity.save(comB) + await DbCommunity.save(comB) + state.status = CommunityHandshakeStateType.SUCCESS + stateSaveResolver = state.save() methodLogger.debug('Community Authentication successful:', new CommunityLoggingView(comB)) } else { + state.status = CommunityHandshakeStateType.FAILED + state.lastError = 'Community Authentication failed, empty response' + stateSaveResolver = state.save() methodLogger.error('Community Authentication failed:', authenticationArgs) } } } catch (err) { - methodLogger.error('error in startAuthentication:', err) + let errorString: string = '' + if (err instanceof Error) { + errorString = err.message + } else { + errorString = String(err) + } + if (state) { + state.status = CommunityHandshakeStateType.FAILED + state.lastError = errorString + stateSaveResolver = state.save() + } + methodLogger.error('error in startAuthentication:', errorString) + } finally { + if (stateSaveResolver) { + await stateSaveResolver + } } - methodLogger.removeContext('handshakeID') } diff --git a/shared/src/jwt/JWT.ts b/shared/src/jwt/JWT.ts index 1af50f5bd..7c8fd799d 100644 --- a/shared/src/jwt/JWT.ts +++ b/shared/src/jwt/JWT.ts @@ -43,11 +43,9 @@ export const verify = async (handshakeID: string, token: string, publicKey: stri }) payload.handshakeID = handshakeID methodLogger.debug('verify after jwtVerify... payload=', payload) - methodLogger.removeContext('handshakeID') return payload as JwtPayloadType } catch (err) { methodLogger.error('verify after jwtVerify... error=', err) - methodLogger.removeContext('handshakeID') return null } } @@ -74,11 +72,9 @@ export const encode = async (payload: JwtPayloadType, privatekey: string): Promi .setExpirationTime(payload.expiration) .sign(secret) methodLogger.debug('encode... token=', token) - methodLogger.removeContext('handshakeID') return token } catch (e) { methodLogger.error('Failed to sign JWT:', e) - methodLogger.removeContext('handshakeID') throw e } } @@ -111,11 +107,9 @@ export const encrypt = async (payload: JwtPayloadType, publicKey: string): Promi .setProtectedHeader({ alg: 'RSA-OAEP-256', enc: 'A256GCM' }) .encrypt(recipientKey) methodLogger.debug('encrypt... jwe=', jwe) - methodLogger.removeContext('handshakeID') return jwe.toString() } catch (e) { methodLogger.error('Failed to encrypt JWT:', e) - methodLogger.removeContext('handshakeID') throw e } } @@ -131,11 +125,9 @@ export const decrypt = async(handshakeID: string, jwe: string, privateKey: strin await compactDecrypt(jwe, decryptKey) methodLogger.debug('decrypt... plaintext=', plaintext) methodLogger.debug('decrypt... protectedHeader=', protectedHeader) - methodLogger.removeContext('handshakeID') return new TextDecoder().decode(plaintext) } catch (e) { methodLogger.error('Failed to decrypt JWT:', e) - methodLogger.removeContext('handshakeID') throw e } } @@ -147,7 +139,6 @@ export const encryptAndSign = async (payload: JwtPayloadType, privateKey: string methodLogger.debug('encryptAndSign... jwe=', jwe) const jws = await encode(new EncryptedJWEJwtPayloadType(payload.handshakeID, jwe), privateKey) methodLogger.debug('encryptAndSign... jws=', jws) - methodLogger.removeContext('handshakeID') return jws } @@ -171,6 +162,5 @@ export const verifyAndDecrypt = async (handshakeID: string, token: string, priva methodLogger.debug('verifyAndDecrypt... jwe=', jwe) const payload = await decrypt(handshakeID, jwe as string, privateKey) methodLogger.debug('verifyAndDecrypt... payload=', payload) - methodLogger.removeContext('handshakeID') return JSON.parse(payload) as JwtPayloadType } From a31ba3756a38766aa4ed65cf3f6f735c8ee4d8e3 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Mon, 13 Oct 2025 13:09:51 +0200 Subject: [PATCH 10/40] one time code default NULL --- .../migrations/0095-add_community_handshake_states_table.ts | 2 +- database/src/entity/CommunityHandshakeState.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/database/migration/migrations/0095-add_community_handshake_states_table.ts b/database/migration/migrations/0095-add_community_handshake_states_table.ts index 38553c336..45e5b29a8 100644 --- a/database/migration/migrations/0095-add_community_handshake_states_table.ts +++ b/database/migration/migrations/0095-add_community_handshake_states_table.ts @@ -3,7 +3,7 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis CREATE TABLE community_handshake_states ( id int unsigned NOT NULL AUTO_INCREMENT, handshake_id int unsigned NOT NULL, - one_time_code int unsigned NOT NULL, + one_time_code int unsigned NULL DEFAULT NULL, public_key binary(32) NOT NULL, api_version varchar(255) NOT NULL, status varchar(255) NOT NULL DEFAULT 'OPEN_CONNECTION', diff --git a/database/src/entity/CommunityHandshakeState.ts b/database/src/entity/CommunityHandshakeState.ts index 4fb62a733..f3a27b834 100644 --- a/database/src/entity/CommunityHandshakeState.ts +++ b/database/src/entity/CommunityHandshakeState.ts @@ -10,8 +10,8 @@ export class CommunityHandshakeState extends BaseEntity { @Column({ name: 'handshake_id', type: 'int', unsigned: true }) handshakeId: number - @Column({ name: 'one_time_code', type: 'int', unsigned: true }) - oneTimeCode: number + @Column({ name: 'one_time_code', type: 'int', unsigned: true, default: null, nullable: true }) + oneTimeCode?: number @Column({ name: 'public_key', type: 'binary', length: 32 }) publicKey: Buffer From 49010af54fa1b856c8f4c84717f1f9bde79eff89 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Mon, 13 Oct 2025 17:12:03 +0200 Subject: [PATCH 11/40] use own class for buffer/string hex handling --- .../src/federation/authenticateCommunities.ts | 16 ++-- backend/src/federation/validateCommunities.ts | 11 ++- database/src/queries/communities.ts | 12 ++- database/src/queries/communityHandshakes.ts | 5 +- .../1_0/resolver/AuthenticationResolver.ts | 87 ++++++++++--------- .../api/1_0/util/authenticateCommunity.ts | 32 ++++--- shared/src/helper/BinaryData.ts | 41 +++++++++ shared/src/helper/index.ts | 3 +- shared/src/schema/base.schema.test.ts | 17 +++- shared/src/schema/base.schema.ts | 9 +- 10 files changed, 157 insertions(+), 76 deletions(-) create mode 100644 shared/src/helper/BinaryData.ts diff --git a/backend/src/federation/authenticateCommunities.ts b/backend/src/federation/authenticateCommunities.ts index 78fea419f..3f4b769e8 100644 --- a/backend/src/federation/authenticateCommunities.ts +++ b/backend/src/federation/authenticateCommunities.ts @@ -2,15 +2,14 @@ import { CommunityHandshakeState as DbCommunityHandshakeState, CommunityHandshakeStateLoggingView, CommunityLoggingView, - Community as DbCommunity, FederatedCommunity as DbFederatedCommunity, FederatedCommunityLoggingView, findPendingCommunityHandshake, getHomeCommunityWithFederatedCommunityOrFail, - CommunityHandshakeStateType + CommunityHandshakeStateType, + getCommunityByPublicKeyOrFail } from 'database' import { randombytes_random } from 'sodium-native' -import { CONFIG as CONFIG_CORE } from 'core' import { AuthenticationClient as V1_0_AuthenticationClient } from '@/federation/client/1_0/AuthenticationClient' import { ensureUrlEndsWithSlash } from 'core' @@ -22,6 +21,7 @@ import { AuthenticationClientFactory } from './client/AuthenticationClientFactor import { EncryptedTransferArgs } from 'core' import { CommunityHandshakeStateLogic } from 'core' import { CommunityLogic } from 'core' +import { Ed25519PublicKey } from 'shared' const createLogger = (functionName: string) => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.authenticateCommunities.${functionName}`) @@ -38,7 +38,8 @@ export async function startCommunityAuthentication( methodLogger.debug('homeComA', new CommunityLoggingView(homeComA)) const homeComALogic = new CommunityLogic(homeComA) const homeFedComA = homeComALogic.getFederatedCommunityWithApiOrFail(fedComB.apiVersion) - const comB = await DbCommunity.findOneByOrFail({ publicKey: fedComB.publicKey }) + const fedComBPublicKey = new Ed25519PublicKey(fedComB.publicKey) + const comB = await getCommunityByPublicKeyOrFail(fedComBPublicKey) methodLogger.debug('started with comB:', new CommunityLoggingView(comB)) // check if communityUuid is not a valid v4Uuid @@ -54,7 +55,7 @@ export async function startCommunityAuthentication( ) // check if a authentication is already in progress - const existingState = await findPendingCommunityHandshake(fedComB.publicKey, fedComB.apiVersion, false) + const existingState = await findPendingCommunityHandshake(fedComBPublicKey, fedComB.apiVersion, false) if (existingState) { const stateLogic = new CommunityHandshakeStateLogic(existingState) // retry on timeout or failure @@ -72,7 +73,7 @@ export async function startCommunityAuthentication( throw new Error(`Public JWT key still not exist for comB ${comB.name}`) } const state = new DbCommunityHandshakeState() - state.publicKey = fedComB.publicKey + state.publicKey = fedComBPublicKey.asBuffer() state.apiVersion = fedComB.apiVersion state.status = CommunityHandshakeStateType.START_COMMUNITY_AUTHENTICATION state.handshakeId = parseInt(handshakeID) @@ -87,7 +88,8 @@ export async function startCommunityAuthentication( methodLogger.debug('jws', jws) // prepare the args for the client invocation const args = new EncryptedTransferArgs() - args.publicKey = homeComA!.publicKey.toString('hex') + const homeComAPublicKey = new Ed25519PublicKey(homeComA!.publicKey) + args.publicKey = homeComAPublicKey.asHex() args.jwt = jws args.handshakeID = handshakeID await stateSaveResolver diff --git a/backend/src/federation/validateCommunities.ts b/backend/src/federation/validateCommunities.ts index cb56a8c53..8d8972ed5 100644 --- a/backend/src/federation/validateCommunities.ts +++ b/backend/src/federation/validateCommunities.ts @@ -12,7 +12,7 @@ import { FederationClient as V1_0_FederationClient } from '@/federation/client/1 import { PublicCommunityInfo } from '@/federation/client/1_0/model/PublicCommunityInfo' import { FederationClientFactory } from '@/federation/client/FederationClientFactory' import { LogError } from '@/server/LogError' -import { createKeyPair, uint32Schema } from 'shared' +import { buffer32Schema, createKeyPair, Ed25519PublicKey, hex64Schema, uint32Schema } from 'shared' import { getLogger } from 'log4js' import { startCommunityAuthentication } from './authenticateCommunities' import { PublicCommunityInfoLoggingView } from './client/1_0/logging/PublicCommunityInfoLogging.view' @@ -67,8 +67,11 @@ export async function validateCommunities(): Promise { const client = FederationClientFactory.getInstance(dbFedComB) if (client instanceof V1_0_FederationClient) { - const pubKey = await client.getPublicKey() - if (pubKey && pubKey === dbFedComB.publicKey.toString('hex')) { + // throw if key isn't valid hex with length 64 + const clientPublicKey = new Ed25519PublicKey(await client.getPublicKey()) + // throw if key isn't valid hex with length 64 + const fedComBPublicKey = new Ed25519PublicKey(dbFedComB.publicKey) + if (clientPublicKey.isSame(fedComBPublicKey)) { await DbFederatedCommunity.update({ id: dbFedComB.id }, { verifiedAt: new Date() }) logger.debug(`verified dbFedComB with:`, dbFedComB.endPoint) const pubComInfo = await client.getPublicCommunityInfo() @@ -84,7 +87,7 @@ export async function validateCommunities(): Promise { logger.debug('missing result of getPublicCommunityInfo') } } else { - logger.debug('received not matching publicKey:', pubKey, dbFedComB.publicKey.toString('hex')) + logger.debug('received not matching publicKey:', clientPublicKey.asHex(), fedComBPublicKey.asHex()) } } } catch (err) { diff --git a/database/src/queries/communities.ts b/database/src/queries/communities.ts index 22bfdb39f..81cd12765 100644 --- a/database/src/queries/communities.ts +++ b/database/src/queries/communities.ts @@ -1,6 +1,6 @@ import { FindOptionsOrder, FindOptionsWhere, IsNull, MoreThanOrEqual, Not } from 'typeorm' import { Community as DbCommunity } from '../entity' -import { urlSchema, uuidv4Schema } from 'shared' +import { Ed25519PublicKey, urlSchema, uuidv4Schema } from 'shared' /** * Retrieves the home community, i.e., a community that is not foreign. @@ -50,15 +50,21 @@ export async function getCommunityWithFederatedCommunityByIdentifier( } export async function getCommunityWithFederatedCommunityWithApiOrFail( - publicKey: Buffer, + publicKey: Ed25519PublicKey, apiVersion: string ): Promise { return await DbCommunity.findOneOrFail({ - where: { foreign: true, publicKey, federatedCommunities: { apiVersion } }, + where: { foreign: true, publicKey: publicKey.asBuffer(), federatedCommunities: { apiVersion } }, relations: { federatedCommunities: true }, }) } +export async function getCommunityByPublicKeyOrFail(publicKey: Ed25519PublicKey): Promise { + return await DbCommunity.findOneOrFail({ + where: { publicKey: publicKey.asBuffer() }, + }) +} + // returns all reachable communities // home community and all federated communities which have been verified within the last authenticationTimeoutMs export async function getReachableCommunities( diff --git a/database/src/queries/communityHandshakes.ts b/database/src/queries/communityHandshakes.ts index f8fa2ba89..9181cfbb3 100644 --- a/database/src/queries/communityHandshakes.ts +++ b/database/src/queries/communityHandshakes.ts @@ -1,5 +1,6 @@ import { Not, In } from 'typeorm' import { CommunityHandshakeState, CommunityHandshakeStateType} from '..' +import { Ed25519PublicKey } from 'shared' /** * Find a pending community handshake by public key. @@ -8,11 +9,11 @@ import { CommunityHandshakeState, CommunityHandshakeStateType} from '..' * @returns The CommunityHandshakeState with associated federated community and community. */ export function findPendingCommunityHandshake( - publicKey: Buffer, apiVersion: string, withRelations = true + publicKey: Ed25519PublicKey, apiVersion: string, withRelations = true ): Promise { return CommunityHandshakeState.findOne({ where: { - publicKey, + publicKey: publicKey.asBuffer(), apiVersion, status: Not(In([ CommunityHandshakeStateType.EXPIRED, diff --git a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts index 40b5e2c18..50bedf34c 100644 --- a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts @@ -6,17 +6,16 @@ import { CommunityHandshakeStateLoggingView, CommunityHandshakeState as DbCommunityHandshakeState, CommunityHandshakeStateType, - Community as DbCommunity, FederatedCommunity as DbFedCommunity, FederatedCommunityLoggingView, getHomeCommunity, - findPendingCommunityHandshake, findPendingCommunityHandshakeOrFailByOneTimeCode, } from 'database' import { getLogger } from 'log4js' import { AuthenticationJwtPayloadType, AuthenticationResponseJwtPayloadType, + Ed25519PublicKey, encryptAndSign, OpenConnectionCallbackJwtPayloadType, OpenConnectionJwtPayloadType, @@ -39,44 +38,40 @@ export class AuthenticationResolver { const methodLogger = createLogger('openConnection') methodLogger.addContext('handshakeID', args.handshakeID) methodLogger.debug(`openConnection() via apiVersion=1_0:`, args) + const argsPublicKey = new Ed25519PublicKey(args.publicKey) try { const openConnectionJwtPayload = await interpretEncryptedTransferArgs(args) as OpenConnectionJwtPayloadType methodLogger.debug('openConnectionJwtPayload', openConnectionJwtPayload) if (!openConnectionJwtPayload) { - const errmsg = `invalid OpenConnection payload of requesting community with publicKey` + args.publicKey - methodLogger.error(errmsg) - // no infos to the caller - return true + throw new Error(`invalid OpenConnection payload of requesting community with publicKey ${argsPublicKey.asHex()}`) } if (openConnectionJwtPayload.tokentype !== OpenConnectionJwtPayloadType.OPEN_CONNECTION_TYPE) { - const errmsg = `invalid tokentype of community with publicKey` + args.publicKey - methodLogger.error(errmsg) - // no infos to the caller - return true + throw new Error(`invalid tokentype of community with publicKey ${argsPublicKey.asHex()}`) } if (!openConnectionJwtPayload.url) { - const errmsg = `invalid url of community with publicKey` + args.publicKey - methodLogger.error(errmsg) - // no infos to the caller - return true + throw new Error(`invalid url of community with publicKey ${argsPublicKey.asHex()}`) } - methodLogger.debug(`vor DbFedCommunity.findOneByOrFail()...`, { publicKey: args.publicKey }) - const fedComA = await DbFedCommunity.findOneByOrFail({ publicKey: Buffer.from(args.publicKey, 'hex') }) + methodLogger.debug(`vor DbFedCommunity.findOneByOrFail()...`, { publicKey: argsPublicKey.asHex() }) + const fedComA = await DbFedCommunity.findOneByOrFail({ publicKey: argsPublicKey.asBuffer() }) methodLogger.debug(`nach DbFedCommunity.findOneByOrFail()...`, fedComA) methodLogger.debug('fedComA', new FederatedCommunityLoggingView(fedComA)) if (!openConnectionJwtPayload.url.startsWith(fedComA.endPoint)) { - const errmsg = `invalid url of community with publicKey` + args.publicKey - methodLogger.error(errmsg) - // no infos to the caller - return true + throw new Error(`invalid url of community with publicKey ${argsPublicKey.asHex()}`) } // no await to respond immediately and invoke callback-request asynchronously - void startOpenConnectionCallback(args.handshakeID, args.publicKey, CONFIG.FEDERATION_API) + void startOpenConnectionCallback(args.handshakeID, argsPublicKey, CONFIG.FEDERATION_API) methodLogger.debug('openConnection() successfully initiated callback and returns true immediately...') return true } catch (err) { - methodLogger.error('invalid jwt token:', err) + let errorText = '' + if (err instanceof Error) { + errorText = err.message + } else { + errorText = String(err) + } + methodLogger.error('invalid jwt token:', errorText) + // no infos to the caller return true } } @@ -93,19 +88,13 @@ export class AuthenticationResolver { // decrypt args.url with homeCom.privateJwtKey and verify signing with callbackFedCom.publicKey const openConnectionCallbackJwtPayload = await interpretEncryptedTransferArgs(args) as OpenConnectionCallbackJwtPayloadType if (!openConnectionCallbackJwtPayload) { - const errmsg = `invalid OpenConnectionCallback payload of requesting community with publicKey` + args.publicKey - methodLogger.error(errmsg) - // no infos to the caller - return true + throw new Error(`invalid OpenConnectionCallback payload of requesting community with publicKey ${args.publicKey}`) } const { endPoint, apiVersion } = splitUrlInEndPointAndApiVersion(openConnectionCallbackJwtPayload.url) methodLogger.debug(`search fedComB per:`, endPoint, apiVersion) const fedComB = await DbFedCommunity.findOneBy({ endPoint, apiVersion }) if (!fedComB) { - const errmsg = `unknown callback community with url` + openConnectionCallbackJwtPayload.url - methodLogger.error(errmsg) - // no infos to the caller - return true + throw new Error(`unknown callback community with url ${openConnectionCallbackJwtPayload.url}`) } methodLogger.debug( `found fedComB and start authentication:`, @@ -116,7 +105,14 @@ export class AuthenticationResolver { methodLogger.debug('openConnectionCallback() successfully initiated authentication and returns true immediately...') return true } catch (err) { - methodLogger.error('invalid jwt token:', err) + let errorText = '' + if (err instanceof Error) { + errorText = err.message + } else { + errorText = String(err) + } + methodLogger.error('invalid jwt token:', errorText) + // no infos to the caller return true } } @@ -131,22 +127,26 @@ export class AuthenticationResolver { methodLogger.debug(`authenticate() via apiVersion=1_0 ...`, args) let state: DbCommunityHandshakeState | null = null let stateSaveResolver: Promise | undefined = undefined + const argsPublicKey = new Ed25519PublicKey(args.publicKey) try { const authArgs = await interpretEncryptedTransferArgs(args) as AuthenticationJwtPayloadType methodLogger.debug(`interpreted authentication payload...authArgs:`, authArgs) if (!authArgs) { - throw new Error(`invalid authentication payload of requesting community with publicKey ${args.publicKey}`) + throw new Error(`invalid authentication payload of requesting community with publicKey ${argsPublicKey.asHex()}`) } - - if (!uint32Schema.safeParse(Number(authArgs.oneTimeCode)).success) { + const validOneTimeCode = uint32Schema.safeParse(Number(authArgs.oneTimeCode)) + if (!validOneTimeCode.success) { throw new Error( - `invalid oneTimeCode: ${authArgs.oneTimeCode} for community with publicKey ${authArgs.publicKey}, expect uint32` + `invalid oneTimeCode: ${authArgs.oneTimeCode} for community with publicKey ${argsPublicKey.asHex()}, expect uint32` ) } - state = await findPendingCommunityHandshakeOrFailByOneTimeCode(Number(authArgs.oneTimeCode)) + state = await findPendingCommunityHandshakeOrFailByOneTimeCode(validOneTimeCode.data) const stateLogic = new CommunityHandshakeStateLogic(state) - if (await stateLogic.isTimeoutUpdate() || state.status !== CommunityHandshakeStateType.START_OPEN_CONNECTION_CALLBACK) { + if ( + await stateLogic.isTimeoutUpdate() || + state.status !== CommunityHandshakeStateType.START_OPEN_CONNECTION_CALLBACK + ) { throw new Error('No valid pending community handshake found') } state.status = CommunityHandshakeStateType.SUCCESS @@ -156,16 +156,19 @@ export class AuthenticationResolver { const authCom = state.federatedCommunity.community if (authCom) { methodLogger.debug('found authCom:', new CommunityLoggingView(authCom)) - methodLogger.debug('authCom.publicKey', authCom.publicKey.toString('hex')) - methodLogger.debug('args.publicKey', args.publicKey) - if (authCom.publicKey.compare(Buffer.from(args.publicKey, 'hex')) !== 0) { + const authComPublicKey = new Ed25519PublicKey(authCom.publicKey) + methodLogger.debug('authCom.publicKey', authComPublicKey.asHex()) + methodLogger.debug('args.publicKey', argsPublicKey.asHex()) + if (!authComPublicKey.isSame(argsPublicKey)) { throw new Error( - `corrupt authentication call detected, oneTimeCode: ${authArgs.oneTimeCode} doesn't belong to caller: ${args.publicKey}` + `corrupt authentication call detected, oneTimeCode: ${authArgs.oneTimeCode} doesn't belong to caller: ${argsPublicKey.asHex()}` ) } const communityUuid = uuidv4Schema.safeParse(authArgs.uuid) if (!communityUuid.success) { - throw new Error(`invalid uuid: ${authArgs.uuid} for community with publicKey ${authCom.publicKey}`) + throw new Error( + `invalid uuid: ${authArgs.uuid} for community with publicKey ${authComPublicKey.asHex()}` + ) } authCom.communityUuid = communityUuid.data authCom.authenticatedAt = new Date() diff --git a/federation/src/graphql/api/1_0/util/authenticateCommunity.ts b/federation/src/graphql/api/1_0/util/authenticateCommunity.ts index d901a84c3..0c52dfb63 100644 --- a/federation/src/graphql/api/1_0/util/authenticateCommunity.ts +++ b/federation/src/graphql/api/1_0/util/authenticateCommunity.ts @@ -11,7 +11,6 @@ import { getHomeCommunityWithFederatedCommunityOrFail, } from 'database' import { getLogger, Logger } from 'log4js' -import { validate as validateUUID, version as versionUUID } from 'uuid' import { AuthenticationClientFactory } from '@/client/AuthenticationClientFactory' import { randombytes_random } from 'sodium-native' @@ -21,8 +20,10 @@ import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { AuthenticationJwtPayloadType, AuthenticationResponseJwtPayloadType, + Ed25519PublicKey, encryptAndSign, OpenConnectionCallbackJwtPayloadType, + uuidv4Schema, verifyAndDecrypt } from 'shared' import { CommunityHandshakeState as DbCommunityHandshakeState, CommunityHandshakeStateType } from 'database' @@ -42,7 +43,7 @@ async function errorState( export async function startOpenConnectionCallback( handshakeID: string, - publicKey: string, + publicKey: Ed25519PublicKey, api: string, ): Promise { const methodLogger = createLogger('startOpenConnectionCallback') @@ -50,8 +51,7 @@ export async function startOpenConnectionCallback( methodLogger.debug(`Authentication: startOpenConnectionCallback() with:`, { publicKey, }) - const publicKeyBuffer = Buffer.from(publicKey, 'hex') - const pendingState = await findPendingCommunityHandshake(publicKeyBuffer, api, false) + const pendingState = await findPendingCommunityHandshake(publicKey, api, false) if (pendingState) { const stateLogic = new CommunityHandshakeStateLogic(pendingState) // retry on timeout or failure @@ -66,7 +66,7 @@ export async function startOpenConnectionCallback( try { const [homeComB, comA] = await Promise.all([ getHomeCommunityWithFederatedCommunityOrFail(api), - getCommunityWithFederatedCommunityWithApiOrFail(publicKeyBuffer, api), + getCommunityWithFederatedCommunityWithApiOrFail(publicKey, api), ]) // load helpers const homeComBLogic = new CommunityLogic(homeComB) @@ -79,7 +79,7 @@ export async function startOpenConnectionCallback( const oneTimeCode = randombytes_random() const oneTimeCodeString = oneTimeCode.toString() - state.publicKey = publicKeyBuffer + state.publicKey = publicKey.asBuffer() state.apiVersion = api state.status = CommunityHandshakeStateType.START_OPEN_CONNECTION_CALLBACK state.handshakeId = parseInt(handshakeID) @@ -100,7 +100,7 @@ export async function startOpenConnectionCallback( // encrypt callbackArgs with requestedCom.publicJwtKey and sign it with homeCom.privateJwtKey const jwt = await encryptAndSign(callbackArgs, homeComB.privateJwtKey!, comA.publicJwtKey!) const args = new EncryptedTransferArgs() - args.publicKey = homeComB.publicKey.toString('hex') + args.publicKey = new Ed25519PublicKey(homeComB.publicKey).asHex() args.jwt = jwt args.handshakeID = handshakeID await stateSaveResolver @@ -140,21 +140,25 @@ export async function startAuthentication( }) let state: DbCommunityHandshakeState | null = null let stateSaveResolver: Promise | undefined = undefined + const fedComBPublicKey = new Ed25519PublicKey(fedComB.publicKey) try { const homeComA = await getHomeCommunity() const comB = await DbCommunity.findOneByOrFail({ foreign: true, - publicKey: fedComB.publicKey, + publicKey: fedComBPublicKey.asBuffer(), }) if (!comB.publicJwtKey) { throw new Error('Public JWT key still not exist for foreign community') } - state = await findPendingCommunityHandshake(fedComB.publicKey, fedComB.apiVersion, false) + state = await findPendingCommunityHandshake(fedComBPublicKey, fedComB.apiVersion, false) if (!state) { throw new Error('No pending community handshake found') } const stateLogic = new CommunityHandshakeStateLogic(state) - if (await stateLogic.isTimeoutUpdate() || state.status !== CommunityHandshakeStateType.START_COMMUNITY_AUTHENTICATION) { + if ( + await stateLogic.isTimeoutUpdate() || + state.status !== CommunityHandshakeStateType.START_COMMUNITY_AUTHENTICATION + ) { methodLogger.debug('invalid state', new CommunityHandshakeStateLoggingView(state)) throw new Error('No valid pending community handshake found') } @@ -168,7 +172,7 @@ export async function startAuthentication( // encrypt authenticationArgs.uuid with fedComB.publicJwtKey and sign it with homeCom.privateJwtKey const jwt = await encryptAndSign(authenticationArgs, homeComA!.privateJwtKey!, comB.publicJwtKey!) const args = new EncryptedTransferArgs() - args.publicKey = homeComA!.publicKey.toString('hex') + args.publicKey = new Ed25519PublicKey(homeComA!.publicKey).asHex() args.jwt = jwt args.handshakeID = handshakeID methodLogger.debug(`invoke authenticate() with:`, args) @@ -183,10 +187,10 @@ export async function startAuthentication( new FederatedCommunityLoggingView(fedComB), ) if (payload.tokentype !== AuthenticationResponseJwtPayloadType.AUTHENTICATION_RESPONSE_TYPE) { - throw new Error(`Invalid tokentype in authenticate-response of community with publicKey ${comB.publicKey}`) + throw new Error(`Invalid tokentype in authenticate-response of community with publicKey ${fedComBPublicKey.asHex()}`) } - if (!payload.uuid || !validateUUID(payload.uuid) || versionUUID(payload.uuid) !== 4) { - throw new Error(`Invalid uuid in authenticate-response of community with publicKey ${comB.publicKey}`) + if (!uuidv4Schema.safeParse(payload.uuid).success) { + throw new Error(`Invalid uuid in authenticate-response of community with publicKey ${fedComBPublicKey.asHex()}`) } comB.communityUuid = payload.uuid comB.authenticatedAt = new Date() diff --git a/shared/src/helper/BinaryData.ts b/shared/src/helper/BinaryData.ts new file mode 100644 index 000000000..f570ac381 --- /dev/null +++ b/shared/src/helper/BinaryData.ts @@ -0,0 +1,41 @@ +/** + * Class mainly for handling ed25519 public keys, + * to make sure we have always the correct Format (Buffer or Hex String) + */ +export class BinaryData { + private buf: Buffer + private hex: string + + constructor(input: Buffer | string | undefined) { + if (typeof input === 'string') { + this.buf = Buffer.from(input, 'hex') + this.hex = input + } else if (Buffer.isBuffer(input)) { + this.buf = input + this.hex = input.toString('hex') + } else { + throw new Error('Either valid hex string or Buffer expected') + } + } + + asBuffer(): Buffer { + return this.buf + } + + asHex(): string { + return this.hex + } + + isSame(other: BinaryData): boolean { + return this.buf.compare(other.buf) === 0 + } +} + +export class Ed25519PublicKey extends BinaryData { + constructor(input: Buffer | string | undefined) { + super(input) + if (this.asBuffer().length !== 32) { + throw new Error('Invalid ed25519 public key length') + } + } +} \ No newline at end of file diff --git a/shared/src/helper/index.ts b/shared/src/helper/index.ts index abfe2c8dc..39f988ba4 100644 --- a/shared/src/helper/index.ts +++ b/shared/src/helper/index.ts @@ -1 +1,2 @@ -export * from './updateField' \ No newline at end of file +export * from './updateField' +export * from './BinaryData' \ No newline at end of file diff --git a/shared/src/schema/base.schema.test.ts b/shared/src/schema/base.schema.test.ts index d12f2a3b4..db0a07d52 100644 --- a/shared/src/schema/base.schema.test.ts +++ b/shared/src/schema/base.schema.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'bun:test' -import { uuidv4Schema, uint32Schema } from './base.schema' +import { generateKeyPairSync } from 'node:crypto' +import { uuidv4Schema, uint32Schema, buffer32Schema } from './base.schema' import { v4 as uuidv4 } from 'uuid' describe('uuidv4 schema', () => { @@ -22,3 +23,17 @@ describe('uint32 schema', () => { expect(uint32Schema.safeParse(2092352810).success).toBeTruthy() }) }) + +describe('buffer32 schema', () => { + it('should validate buffer', () => { + const { publicKey } = generateKeyPairSync('ed25519') + const buffer = publicKey.export({ type: 'spki', format: 'der' }).slice(-32) + expect(Buffer.isBuffer(buffer)).toBeTruthy() + expect(buffer.length).toBe(32) + expect(buffer32Schema.safeParse(buffer).success).toBeTruthy() + }) + + it("shouldn't validate string", () => { + expect(buffer32Schema.safeParse('3e1a2eecc95c48fedf47a522a8c77b91').success).toBeFalsy() + }) +}) diff --git a/shared/src/schema/base.schema.ts b/shared/src/schema/base.schema.ts index ee9383dd2..0dcdf09a9 100644 --- a/shared/src/schema/base.schema.ts +++ b/shared/src/schema/base.schema.ts @@ -1,7 +1,12 @@ -import { string, number } from 'zod' +import { string, number, custom } from 'zod' import { validate, version } from 'uuid' export const uuidv4Schema = string().refine((val: string) => validate(val) && version(val) === 4, 'Invalid uuid') export const emailSchema = string().email() export const urlSchema = string().url() -export const uint32Schema = number().positive().lte(4294967295) \ No newline at end of file +export const uint32Schema = number().positive().lte(4294967295) +export const buffer32Schema = custom( + (val: Buffer) => Buffer.isBuffer(val) && val.length === 32, + 'Invalid buffer' +) +export const hex64Schema = string().length(64).regex(/^[0-9A-Fa-f]$/) \ No newline at end of file From 2491cf2fd61ac9d4b6fcf0963f6e8b4fb735dbf1 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Mon, 13 Oct 2025 17:28:57 +0200 Subject: [PATCH 12/40] fix test --- backend/src/federation/validateCommunities.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/federation/validateCommunities.test.ts b/backend/src/federation/validateCommunities.test.ts index 255f8f180..949195652 100644 --- a/backend/src/federation/validateCommunities.test.ts +++ b/backend/src/federation/validateCommunities.test.ts @@ -103,7 +103,7 @@ describe('validate Communities', () => { return { data: { getPublicKey: { - publicKey: 'somePubKey', + publicKey: '2222222222222222222222222222222222222222222222222222222222222222', }, }, } as Response @@ -170,8 +170,8 @@ describe('validate Communities', () => { it('logs not matching publicKeys', () => { expect(logger.debug).toBeCalledWith( 'received not matching publicKey:', - 'somePubKey', - expect.stringMatching('11111111111111111111111111111111'), + '2222222222222222222222222222222222222222222222222222222222222222', + expect.stringMatching('1111111111111111111111111111111100000000000000000000000000000000'), ) }) }) From 29363a0f093ed5dccf4354b781a3dec380afd4f9 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Mon, 13 Oct 2025 17:42:14 +0200 Subject: [PATCH 13/40] fix logic --- core/src/logic/CommunityHandshakeState.logic.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/logic/CommunityHandshakeState.logic.ts b/core/src/logic/CommunityHandshakeState.logic.ts index e9cb23d5c..52422be15 100644 --- a/core/src/logic/CommunityHandshakeState.logic.ts +++ b/core/src/logic/CommunityHandshakeState.logic.ts @@ -21,7 +21,7 @@ export class CommunityHandshakeStateLogic { if (this.self.status === CommunityHandshakeStateType.EXPIRED) { return true } - if (Date.now() - this.self.updatedAt.getTime() > FEDERATION_AUTHENTICATION_TIMEOUT_MS) { + if ((Date.now() - this.self.updatedAt.getTime()) > FEDERATION_AUTHENTICATION_TIMEOUT_MS) { return true } return false From 40c0c2f143c6848a409d00ae4c59616ee42cef93 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Mon, 13 Oct 2025 17:45:07 +0200 Subject: [PATCH 14/40] update log --- backend/src/federation/authenticateCommunities.ts | 2 +- federation/src/graphql/api/1_0/util/authenticateCommunity.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/federation/authenticateCommunities.ts b/backend/src/federation/authenticateCommunities.ts index 3f4b769e8..340705437 100644 --- a/backend/src/federation/authenticateCommunities.ts +++ b/backend/src/federation/authenticateCommunities.ts @@ -61,7 +61,7 @@ export async function startCommunityAuthentication( // retry on timeout or failure if (!await stateLogic.isTimeoutUpdate()) { // authentication with community and api version is still in progress and it is not timeout yet - methodLogger.debug('existingState', new CommunityHandshakeStateLoggingView(existingState)) + methodLogger.debug('existingState, so we exit here', new CommunityHandshakeStateLoggingView(existingState)) return } } diff --git a/federation/src/graphql/api/1_0/util/authenticateCommunity.ts b/federation/src/graphql/api/1_0/util/authenticateCommunity.ts index 0c52dfb63..7abda623c 100644 --- a/federation/src/graphql/api/1_0/util/authenticateCommunity.ts +++ b/federation/src/graphql/api/1_0/util/authenticateCommunity.ts @@ -57,7 +57,7 @@ export async function startOpenConnectionCallback( // retry on timeout or failure if (!await stateLogic.isTimeoutUpdate()) { // authentication with community and api version is still in progress and it is not timeout yet - methodLogger.debug('existingState', new CommunityHandshakeStateLoggingView(pendingState)) + methodLogger.debug('existingState, so we exit here', new CommunityHandshakeStateLoggingView(pendingState)) return } } From 5dd337e96c73430296db2bda44b682d28df48c9e Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Mon, 13 Oct 2025 18:03:37 +0200 Subject: [PATCH 15/40] fix const 10 minutes not 10 hours (._.);, update logging --- .../src/federation/authenticateCommunities.ts | 2 +- .../CommunityHandshakeStateLogic.test.ts | 22 +++++++++++++++++++ shared/src/const/index.ts | 2 +- 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 core/src/logic/CommunityHandshakeStateLogic.test.ts diff --git a/backend/src/federation/authenticateCommunities.ts b/backend/src/federation/authenticateCommunities.ts index 340705437..c548be8e7 100644 --- a/backend/src/federation/authenticateCommunities.ts +++ b/backend/src/federation/authenticateCommunities.ts @@ -59,7 +59,7 @@ export async function startCommunityAuthentication( if (existingState) { const stateLogic = new CommunityHandshakeStateLogic(existingState) // retry on timeout or failure - if (!await stateLogic.isTimeoutUpdate()) { + if (!(await stateLogic.isTimeoutUpdate())) { // authentication with community and api version is still in progress and it is not timeout yet methodLogger.debug('existingState, so we exit here', new CommunityHandshakeStateLoggingView(existingState)) return diff --git a/core/src/logic/CommunityHandshakeStateLogic.test.ts b/core/src/logic/CommunityHandshakeStateLogic.test.ts new file mode 100644 index 000000000..52d11d8fb --- /dev/null +++ b/core/src/logic/CommunityHandshakeStateLogic.test.ts @@ -0,0 +1,22 @@ +import { CommunityHandshakeState } from 'database' +import { CommunityHandshakeStateLogic } from './CommunityHandshakeState.logic' +import { CommunityHandshakeStateType } from 'database' +import { FEDERATION_AUTHENTICATION_TIMEOUT_MS } from 'shared' + +describe('CommunityHandshakeStateLogic', () => { + it('isTimeout', () => { + const state = new CommunityHandshakeState() + state.updatedAt = new Date(Date.now() - FEDERATION_AUTHENTICATION_TIMEOUT_MS * 2) + state.status = CommunityHandshakeStateType.START_AUTHENTICATION + const logic = new CommunityHandshakeStateLogic(state) + expect(logic.isTimeout()).toEqual(true) + }) + + it('isTimeout return false', () => { + const state = new CommunityHandshakeState() + state.updatedAt = new Date(Date.now()) + state.status = CommunityHandshakeStateType.START_AUTHENTICATION + const logic = new CommunityHandshakeStateLogic(state) + expect(logic.isTimeout()).toEqual(false) + }) +}) \ No newline at end of file diff --git a/shared/src/const/index.ts b/shared/src/const/index.ts index 549991c61..97fe8f306 100644 --- a/shared/src/const/index.ts +++ b/shared/src/const/index.ts @@ -3,4 +3,4 @@ export const SECONDS_PER_YEAR_GREGORIAN_CALENDER = 31556952.0 export const LOG4JS_BASE_CATEGORY_NAME = 'shared' export const REDEEM_JWT_TOKEN_EXPIRATION = '10m' // 10 minutes -export const FEDERATION_AUTHENTICATION_TIMEOUT_MS = 60 * 60 * 1000 * 10 \ No newline at end of file +export const FEDERATION_AUTHENTICATION_TIMEOUT_MS = 60 * 1000 * 10 \ No newline at end of file From b995c329ed53f69296f0ae0a95dbeeb1e4076a1e Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Mon, 13 Oct 2025 18:34:24 +0200 Subject: [PATCH 16/40] add brackets --- .../src/graphql/api/1_0/resolver/AuthenticationResolver.ts | 2 +- federation/src/graphql/api/1_0/util/authenticateCommunity.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts index 50bedf34c..fba153275 100644 --- a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts @@ -144,7 +144,7 @@ export class AuthenticationResolver { state = await findPendingCommunityHandshakeOrFailByOneTimeCode(validOneTimeCode.data) const stateLogic = new CommunityHandshakeStateLogic(state) if ( - await stateLogic.isTimeoutUpdate() || + (await stateLogic.isTimeoutUpdate()) || state.status !== CommunityHandshakeStateType.START_OPEN_CONNECTION_CALLBACK ) { throw new Error('No valid pending community handshake found') diff --git a/federation/src/graphql/api/1_0/util/authenticateCommunity.ts b/federation/src/graphql/api/1_0/util/authenticateCommunity.ts index 7abda623c..d69bfc4c5 100644 --- a/federation/src/graphql/api/1_0/util/authenticateCommunity.ts +++ b/federation/src/graphql/api/1_0/util/authenticateCommunity.ts @@ -55,7 +55,7 @@ export async function startOpenConnectionCallback( if (pendingState) { const stateLogic = new CommunityHandshakeStateLogic(pendingState) // retry on timeout or failure - if (!await stateLogic.isTimeoutUpdate()) { + if (!(await stateLogic.isTimeoutUpdate())) { // authentication with community and api version is still in progress and it is not timeout yet methodLogger.debug('existingState, so we exit here', new CommunityHandshakeStateLoggingView(pendingState)) return @@ -156,7 +156,7 @@ export async function startAuthentication( } const stateLogic = new CommunityHandshakeStateLogic(state) if ( - await stateLogic.isTimeoutUpdate() || + (await stateLogic.isTimeoutUpdate()) || state.status !== CommunityHandshakeStateType.START_COMMUNITY_AUTHENTICATION ) { methodLogger.debug('invalid state', new CommunityHandshakeStateLoggingView(state)) From 76cf268ef4f41913f61323b56c012f890f429f82 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Tue, 14 Oct 2025 07:55:30 +0200 Subject: [PATCH 17/40] add detailed debug log for BinaryData --- .../src/graphql/api/1_0/util/authenticateCommunity.ts | 2 +- shared/src/helper/BinaryData.ts | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/federation/src/graphql/api/1_0/util/authenticateCommunity.ts b/federation/src/graphql/api/1_0/util/authenticateCommunity.ts index d69bfc4c5..00c140d9c 100644 --- a/federation/src/graphql/api/1_0/util/authenticateCommunity.ts +++ b/federation/src/graphql/api/1_0/util/authenticateCommunity.ts @@ -49,7 +49,7 @@ export async function startOpenConnectionCallback( const methodLogger = createLogger('startOpenConnectionCallback') methodLogger.addContext('handshakeID', handshakeID) methodLogger.debug(`Authentication: startOpenConnectionCallback() with:`, { - publicKey, + publicKey: publicKey.asHex(), }) const pendingState = await findPendingCommunityHandshake(publicKey, api, false) if (pendingState) { diff --git a/shared/src/helper/BinaryData.ts b/shared/src/helper/BinaryData.ts index f570ac381..c21fad9dd 100644 --- a/shared/src/helper/BinaryData.ts +++ b/shared/src/helper/BinaryData.ts @@ -1,3 +1,8 @@ +import { getLogger } from 'log4js' +import { LOG4JS_BASE_CATEGORY_NAME } from '../const' + +const logging = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.helper.BinaryData`) + /** * Class mainly for handling ed25519 public keys, * to make sure we have always the correct Format (Buffer or Hex String) @@ -7,6 +12,12 @@ export class BinaryData { private hex: string constructor(input: Buffer | string | undefined) { + if (!input) { + logging.debug('constructor() with undefined input') + } + logging.debug(`constructor() input type: ${typeof input}`) + logging.debug(`constructor() input isBuffer: ${Buffer.isBuffer(input)}`) + logging.debug(`constructor() input: ${input}`) if (typeof input === 'string') { this.buf = Buffer.from(input, 'hex') this.hex = input From 6e1d12d3b826d6947f0bf36b89fbadaa6081fdec Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Tue, 14 Oct 2025 08:28:45 +0200 Subject: [PATCH 18/40] log shutdown reasons --- .../1_0/resolver/AuthenticationResolver.ts | 5 +-- federation/src/index.ts | 8 ++++ shared/src/helper/BinaryData.ts | 10 ++++- shared/src/helper/index.ts | 3 +- shared/src/helper/onShutdown.ts | 43 +++++++++++++++++++ 5 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 shared/src/helper/onShutdown.ts diff --git a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts index fba153275..6712c47d5 100644 --- a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts @@ -51,10 +51,9 @@ export class AuthenticationResolver { if (!openConnectionJwtPayload.url) { throw new Error(`invalid url of community with publicKey ${argsPublicKey.asHex()}`) } - methodLogger.debug(`vor DbFedCommunity.findOneByOrFail()...`, { publicKey: argsPublicKey.asHex() }) + methodLogger.debug(`before DbFedCommunity.findOneByOrFail()...`, { publicKey: argsPublicKey.asHex() }) const fedComA = await DbFedCommunity.findOneByOrFail({ publicKey: argsPublicKey.asBuffer() }) - methodLogger.debug(`nach DbFedCommunity.findOneByOrFail()...`, fedComA) - methodLogger.debug('fedComA', new FederatedCommunityLoggingView(fedComA)) + methodLogger.debug(`after DbFedCommunity.findOneByOrFail()...`, new FederatedCommunityLoggingView(fedComA)) if (!openConnectionJwtPayload.url.startsWith(fedComA.endPoint)) { throw new Error(`invalid url of community with publicKey ${argsPublicKey.asHex()}`) } diff --git a/federation/src/index.ts b/federation/src/index.ts index 4492f24fb..ad0df6b0f 100644 --- a/federation/src/index.ts +++ b/federation/src/index.ts @@ -6,6 +6,7 @@ import { getLogger } from 'log4js' // config import { CONFIG } from './config' import { LOG4JS_BASE_CATEGORY_NAME } from './config/const' +import { onShutdown, ShutdownReason } from 'shared' async function main() { // init logger @@ -27,6 +28,13 @@ async function main() { `GraphIQL available at ${CONFIG.FEDERATION_COMMUNITY_URL}/api/${CONFIG.FEDERATION_API}`, ) } + onShutdown(async (reason, details) => { + if (ShutdownReason.SIGINT === reason || ShutdownReason.SIGTERM === reason) { + logger.info(`graceful shutdown: ${reason}`) + } else { + logger.error(`crash: ${reason} ${details}`) + } + }) }) } diff --git a/shared/src/helper/BinaryData.ts b/shared/src/helper/BinaryData.ts index c21fad9dd..afffd5bfb 100644 --- a/shared/src/helper/BinaryData.ts +++ b/shared/src/helper/BinaryData.ts @@ -17,7 +17,13 @@ export class BinaryData { } logging.debug(`constructor() input type: ${typeof input}`) logging.debug(`constructor() input isBuffer: ${Buffer.isBuffer(input)}`) - logging.debug(`constructor() input: ${input}`) + if (typeof input === 'string') { + logging.debug(`constructor() input: ${input}`) + } else if (Buffer.isBuffer(input)) { + logging.debug(`constructor() input: ${input.toString('hex')}`) + } else { + logging.debug(`constructor() unexpected input type: ${typeof input}`) + } if (typeof input === 'string') { this.buf = Buffer.from(input, 'hex') this.hex = input @@ -38,6 +44,8 @@ export class BinaryData { } isSame(other: BinaryData): boolean { + logging.debug(`isSame() this: ${this.buf}`) + logging.debug(`isSame() other: ${other.buf}`) return this.buf.compare(other.buf) === 0 } } diff --git a/shared/src/helper/index.ts b/shared/src/helper/index.ts index 39f988ba4..9170aad49 100644 --- a/shared/src/helper/index.ts +++ b/shared/src/helper/index.ts @@ -1,2 +1,3 @@ export * from './updateField' -export * from './BinaryData' \ No newline at end of file +export * from './BinaryData' +export * from './onShutdown' \ No newline at end of file diff --git a/shared/src/helper/onShutdown.ts b/shared/src/helper/onShutdown.ts new file mode 100644 index 000000000..ff74699d8 --- /dev/null +++ b/shared/src/helper/onShutdown.ts @@ -0,0 +1,43 @@ +export enum ShutdownReason { + SIGINT = 'SIGINT', + SIGTERM = 'SIGTERM', + UNCAUGHT_EXCEPTION = 'UNCAUGHT_EXCEPTION', + UNCAUGHT_REJECTION = 'UNCAUGHT_REJECTION', +} + + +/** + * Setup graceful shutdown for the process + * @param gracefulShutdown will be called if process is terminated + */ +export function onShutdown(shutdownHandler: (reason: ShutdownReason, details?: string) => Promise) { + const signals: NodeJS.Signals[] = ['SIGINT', 'SIGTERM'] + signals.forEach(sig => { + process.on(sig, async () => { + await shutdownHandler(sig as ShutdownReason) + process.exit(0) + }) + }) + + process.on('uncaughtException', async (err, origin) => { + await shutdownHandler(ShutdownReason.UNCAUGHT_EXCEPTION, `${origin}: ${err}`) + process.exit(1) + }) + + process.on('unhandledRejection', async (reason, promise) => { + await shutdownHandler(ShutdownReason.UNCAUGHT_REJECTION, `${promise}: ${reason}`) + process.exit(1) + }) + + if (process.platform === "win32") { + const rl = require("readline").createInterface({ + input: process.stdin, + output: process.stdout, + }) + rl.on("SIGINT", () => { + process.emit("SIGINT" as any) + }) + } + + +} \ No newline at end of file From f54b4e67d125385dd67a1dddd98ba09418307c3d Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Tue, 14 Oct 2025 08:43:39 +0200 Subject: [PATCH 19/40] handle undefined case --- shared/src/helper/BinaryData.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/shared/src/helper/BinaryData.ts b/shared/src/helper/BinaryData.ts index afffd5bfb..d4af1142b 100644 --- a/shared/src/helper/BinaryData.ts +++ b/shared/src/helper/BinaryData.ts @@ -30,8 +30,11 @@ export class BinaryData { } else if (Buffer.isBuffer(input)) { this.buf = input this.hex = input.toString('hex') + } else if (input === undefined) { + this.buf = Buffer.from('') + this.hex = '' } else { - throw new Error('Either valid hex string or Buffer expected') + throw new Error(`Either valid hex string or Buffer expected: ${input}`) } } From 75ed95a54879b943e2bfc473ef8171b3b07f547b Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Tue, 14 Oct 2025 08:59:04 +0200 Subject: [PATCH 20/40] more precise error logging --- federation/src/index.ts | 4 ++-- shared/src/helper/BinaryData.ts | 18 ++++-------------- shared/src/helper/onShutdown.ts | 10 +++++----- 3 files changed, 11 insertions(+), 21 deletions(-) diff --git a/federation/src/index.ts b/federation/src/index.ts index ad0df6b0f..d2249badb 100644 --- a/federation/src/index.ts +++ b/federation/src/index.ts @@ -28,11 +28,11 @@ async function main() { `GraphIQL available at ${CONFIG.FEDERATION_COMMUNITY_URL}/api/${CONFIG.FEDERATION_API}`, ) } - onShutdown(async (reason, details) => { + onShutdown(async (reason, error) => { if (ShutdownReason.SIGINT === reason || ShutdownReason.SIGTERM === reason) { logger.info(`graceful shutdown: ${reason}`) } else { - logger.error(`crash: ${reason} ${details}`) + logger.error(`crash: ${reason}`, error) } }) }) diff --git a/shared/src/helper/BinaryData.ts b/shared/src/helper/BinaryData.ts index d4af1142b..f1d16c4a0 100644 --- a/shared/src/helper/BinaryData.ts +++ b/shared/src/helper/BinaryData.ts @@ -12,18 +12,6 @@ export class BinaryData { private hex: string constructor(input: Buffer | string | undefined) { - if (!input) { - logging.debug('constructor() with undefined input') - } - logging.debug(`constructor() input type: ${typeof input}`) - logging.debug(`constructor() input isBuffer: ${Buffer.isBuffer(input)}`) - if (typeof input === 'string') { - logging.debug(`constructor() input: ${input}`) - } else if (Buffer.isBuffer(input)) { - logging.debug(`constructor() input: ${input.toString('hex')}`) - } else { - logging.debug(`constructor() unexpected input type: ${typeof input}`) - } if (typeof input === 'string') { this.buf = Buffer.from(input, 'hex') this.hex = input @@ -47,8 +35,10 @@ export class BinaryData { } isSame(other: BinaryData): boolean { - logging.debug(`isSame() this: ${this.buf}`) - logging.debug(`isSame() other: ${other.buf}`) + if (other === undefined || !(other instanceof BinaryData) || other.buf === undefined || !Buffer.isBuffer(other.buf)) { + logging.error('other is invalid', other) + return false + } return this.buf.compare(other.buf) === 0 } } diff --git a/shared/src/helper/onShutdown.ts b/shared/src/helper/onShutdown.ts index ff74699d8..c4d046071 100644 --- a/shared/src/helper/onShutdown.ts +++ b/shared/src/helper/onShutdown.ts @@ -10,7 +10,7 @@ export enum ShutdownReason { * Setup graceful shutdown for the process * @param gracefulShutdown will be called if process is terminated */ -export function onShutdown(shutdownHandler: (reason: ShutdownReason, details?: string) => Promise) { +export function onShutdown(shutdownHandler: (reason: ShutdownReason, error?: Error | any) => Promise) { const signals: NodeJS.Signals[] = ['SIGINT', 'SIGTERM'] signals.forEach(sig => { process.on(sig, async () => { @@ -19,13 +19,13 @@ export function onShutdown(shutdownHandler: (reason: ShutdownReason, details?: s }) }) - process.on('uncaughtException', async (err, origin) => { - await shutdownHandler(ShutdownReason.UNCAUGHT_EXCEPTION, `${origin}: ${err}`) + process.on('uncaughtException', async (err) => { + await shutdownHandler(ShutdownReason.UNCAUGHT_EXCEPTION, err) process.exit(1) }) - process.on('unhandledRejection', async (reason, promise) => { - await shutdownHandler(ShutdownReason.UNCAUGHT_REJECTION, `${promise}: ${reason}`) + process.on('unhandledRejection', async (err) => { + await shutdownHandler(ShutdownReason.UNCAUGHT_REJECTION, err) process.exit(1) }) From e1dd78716699cc5e1734090e75d2a70f021ff4fd Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Tue, 14 Oct 2025 09:16:17 +0200 Subject: [PATCH 21/40] try to make even more robust --- shared/src/helper/BinaryData.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/shared/src/helper/BinaryData.ts b/shared/src/helper/BinaryData.ts index f1d16c4a0..70e4954fd 100644 --- a/shared/src/helper/BinaryData.ts +++ b/shared/src/helper/BinaryData.ts @@ -27,6 +27,12 @@ export class BinaryData { } asBuffer(): Buffer { + if (this.buf === undefined || !Buffer.isBuffer(this.buf)) { + if (this.buf) { + logging.error('invalid buf: ', this.buf) + } + throw new Error('buf is invalid') + } return this.buf } @@ -35,11 +41,14 @@ export class BinaryData { } isSame(other: BinaryData): boolean { - if (other === undefined || !(other instanceof BinaryData) || other.buf === undefined || !Buffer.isBuffer(other.buf)) { + if (other === undefined || !(other instanceof BinaryData)) { logging.error('other is invalid', other) return false } - return this.buf.compare(other.buf) === 0 + if (logging.isDebugEnabled()) { + logging.debug('compare hex: ', this.hex, other.asHex(), this.hex === other.asHex()) + } + return this.buf.compare(other.asBuffer()) === 0 } } From 97e503b954ee372a99c4f789562b91495ce49375 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Tue, 14 Oct 2025 09:28:37 +0200 Subject: [PATCH 22/40] compare via hex --- shared/src/helper/BinaryData.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/shared/src/helper/BinaryData.ts b/shared/src/helper/BinaryData.ts index 70e4954fd..aeff97029 100644 --- a/shared/src/helper/BinaryData.ts +++ b/shared/src/helper/BinaryData.ts @@ -27,12 +27,6 @@ export class BinaryData { } asBuffer(): Buffer { - if (this.buf === undefined || !Buffer.isBuffer(this.buf)) { - if (this.buf) { - logging.error('invalid buf: ', this.buf) - } - throw new Error('buf is invalid') - } return this.buf } @@ -45,10 +39,10 @@ export class BinaryData { logging.error('other is invalid', other) return false } - if (logging.isDebugEnabled()) { - logging.debug('compare hex: ', this.hex, other.asHex(), this.hex === other.asHex()) - } - return this.buf.compare(other.asBuffer()) === 0 + return this.asHex() === other.asHex() + // don't work reliable, in specific cases fail with: + // The "otherBuffer" argument must be an instance of Buffer or Uint8Array. Received an instance of Object + // return this.buf.compare(other.asBuffer()) === 0 } } From 7f74c03fc5dbb1b85617f7fa5b36d07f206984bd Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Tue, 14 Oct 2025 09:38:34 +0200 Subject: [PATCH 23/40] log --- .../src/graphql/api/1_0/resolver/AuthenticationResolver.ts | 1 + shared/src/helper/BinaryData.ts | 5 +---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts index 6712c47d5..2c5853d17 100644 --- a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts @@ -171,6 +171,7 @@ export class AuthenticationResolver { } authCom.communityUuid = communityUuid.data authCom.authenticatedAt = new Date() + methodLogger.debug('try to save: ', authCom) await authCom.save() methodLogger.debug('store authCom.uuid successfully:', new CommunityLoggingView(authCom)) const homeComB = await getHomeCommunity() diff --git a/shared/src/helper/BinaryData.ts b/shared/src/helper/BinaryData.ts index aeff97029..756d3ed3c 100644 --- a/shared/src/helper/BinaryData.ts +++ b/shared/src/helper/BinaryData.ts @@ -39,10 +39,7 @@ export class BinaryData { logging.error('other is invalid', other) return false } - return this.asHex() === other.asHex() - // don't work reliable, in specific cases fail with: - // The "otherBuffer" argument must be an instance of Buffer or Uint8Array. Received an instance of Object - // return this.buf.compare(other.asBuffer()) === 0 + return this.buf.compare(other.asBuffer()) === 0 } } From 5876e7db13984ec12140975ed122ad292f914c90 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Tue, 14 Oct 2025 09:55:19 +0200 Subject: [PATCH 24/40] try hack --- .../src/graphql/api/1_0/resolver/AuthenticationResolver.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts index 2c5853d17..ae916b251 100644 --- a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts @@ -169,6 +169,7 @@ export class AuthenticationResolver { `invalid uuid: ${authArgs.uuid} for community with publicKey ${authComPublicKey.asHex()}` ) } + authCom.publicKey = authComPublicKey.asBuffer() authCom.communityUuid = communityUuid.data authCom.authenticatedAt = new Date() methodLogger.debug('try to save: ', authCom) From 51b28edc98ec56a5022818b8841f313f951c9bd0 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Tue, 14 Oct 2025 10:01:12 +0200 Subject: [PATCH 25/40] load community entity fresh --- .../api/1_0/resolver/AuthenticationResolver.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts index ae916b251..c29ea7391 100644 --- a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts @@ -10,6 +10,7 @@ import { FederatedCommunityLoggingView, getHomeCommunity, findPendingCommunityHandshakeOrFailByOneTimeCode, + getCommunityByPublicKeyOrFail, } from 'database' import { getLogger } from 'log4js' import { @@ -169,11 +170,13 @@ export class AuthenticationResolver { `invalid uuid: ${authArgs.uuid} for community with publicKey ${authComPublicKey.asHex()}` ) } - authCom.publicKey = authComPublicKey.asBuffer() - authCom.communityUuid = communityUuid.data - authCom.authenticatedAt = new Date() + const authComFresh = await getCommunityByPublicKeyOrFail(authComPublicKey) + authComFresh.communityUuid = communityUuid.data + authComFresh.authenticatedAt = new Date() methodLogger.debug('try to save: ', authCom) - await authCom.save() + await authComFresh.save().catch((err) => { + methodLogger.fatal('failed to save authCom:', err) + }) methodLogger.debug('store authCom.uuid successfully:', new CommunityLoggingView(authCom)) const homeComB = await getHomeCommunity() if (homeComB?.communityUuid) { From 83f29652997484deb1e601732a5b71e6ff580938 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Tue, 14 Oct 2025 10:06:09 +0200 Subject: [PATCH 26/40] update log --- .../src/graphql/api/1_0/resolver/AuthenticationResolver.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts index c29ea7391..948a8dc28 100644 --- a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts @@ -170,10 +170,12 @@ export class AuthenticationResolver { `invalid uuid: ${authArgs.uuid} for community with publicKey ${authComPublicKey.asHex()}` ) } + methodLogger.debug('before loading auth community again from db') const authComFresh = await getCommunityByPublicKeyOrFail(authComPublicKey) authComFresh.communityUuid = communityUuid.data authComFresh.authenticatedAt = new Date() - methodLogger.debug('try to save: ', authCom) + methodLogger.debug('after loading auth community again from db') + methodLogger.debug('try to save: ', authComFresh) await authComFresh.save().catch((err) => { methodLogger.fatal('failed to save authCom:', err) }) From 412557b76d4fead30517778f83d3c3ea31382592 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Tue, 14 Oct 2025 10:11:22 +0200 Subject: [PATCH 27/40] try workaround --- .../src/graphql/api/1_0/resolver/AuthenticationResolver.ts | 2 +- shared/src/helper/BinaryData.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts index 948a8dc28..930563e87 100644 --- a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts @@ -171,7 +171,7 @@ export class AuthenticationResolver { ) } methodLogger.debug('before loading auth community again from db') - const authComFresh = await getCommunityByPublicKeyOrFail(authComPublicKey) + const authComFresh = await getCommunityByPublicKeyOrFail(argsPublicKey) authComFresh.communityUuid = communityUuid.data authComFresh.authenticatedAt = new Date() methodLogger.debug('after loading auth community again from db') diff --git a/shared/src/helper/BinaryData.ts b/shared/src/helper/BinaryData.ts index 756d3ed3c..583dccd4e 100644 --- a/shared/src/helper/BinaryData.ts +++ b/shared/src/helper/BinaryData.ts @@ -27,6 +27,10 @@ export class BinaryData { } asBuffer(): Buffer { + if (!this.buf || !Buffer.isBuffer(this.buf)) { + logging.warn('BinaryData.buf is invalid, try to create fresh buffer from hex') + this.buf = Buffer.from(this.hex, 'hex') + } return this.buf } From 0ce9eaab3db4516cc7631a2a71f224cbc5b71ab0 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Tue, 14 Oct 2025 10:53:07 +0200 Subject: [PATCH 28/40] make server crash more visible in logs, try fixing bug with query builder --- database/src/queries/communities.test.ts | 15 +++- .../src/queries/communityHandshakes.test.ts | 79 +++++++++++++++++++ .../1_0/resolver/AuthenticationResolver.ts | 26 +++--- federation/src/index.ts | 8 +- shared/src/helper/onShutdown.ts | 9 ++- 5 files changed, 122 insertions(+), 15 deletions(-) create mode 100644 database/src/queries/communityHandshakes.test.ts diff --git a/database/src/queries/communities.test.ts b/database/src/queries/communities.test.ts index 2d208a224..b435c3649 100644 --- a/database/src/queries/communities.test.ts +++ b/database/src/queries/communities.test.ts @@ -1,8 +1,9 @@ import { Community as DbCommunity, FederatedCommunity as DbFederatedCommunity } from '..' import { AppDatabase } from '../AppDatabase' -import { getHomeCommunity, getHomeCommunityWithFederatedCommunityOrFail, getReachableCommunities } from './communities' +import { getCommunityByPublicKeyOrFail, getHomeCommunity, getHomeCommunityWithFederatedCommunityOrFail, getReachableCommunities } from './communities' import { describe, expect, it, beforeEach, beforeAll, afterAll } from 'vitest' import { createCommunity, createVerifiedFederatedCommunity } from '../seeds/community' +import { Ed25519PublicKey } from 'shared' const db = AppDatabase.getInstance() @@ -56,6 +57,18 @@ describe('community.queries', () => { await createCommunity(false) expect(() => getHomeCommunityWithFederatedCommunityOrFail('1_0')).rejects.toThrow() }) + + it('load community by public key returned from getHomeCommunityWithFederatedCommunityOrFail', async () => { + const homeCom = await createCommunity(false) + await createVerifiedFederatedCommunity('1_0', 100, homeCom) + const community = await getHomeCommunityWithFederatedCommunityOrFail('1_0') + expect(community).toBeDefined() + expect(community?.federatedCommunities).toHaveLength(1) + const ed25519PublicKey = new Ed25519PublicKey(community.federatedCommunities![0].publicKey) + const communityByPublicKey = await getCommunityByPublicKeyOrFail(ed25519PublicKey) + expect(communityByPublicKey).toBeDefined() + expect(communityByPublicKey?.communityUuid).toBe(homeCom.communityUuid) + }) }) describe('getReachableCommunities', () => { it('home community counts also to reachable communities', async () => { diff --git a/database/src/queries/communityHandshakes.test.ts b/database/src/queries/communityHandshakes.test.ts new file mode 100644 index 000000000..badf5ad96 --- /dev/null +++ b/database/src/queries/communityHandshakes.test.ts @@ -0,0 +1,79 @@ +import { AppDatabase } from '../AppDatabase' +import { + CommunityHandshakeState as DbCommunityHandshakeState, + Community as DbCommunity, + FederatedCommunity as DbFederatedCommunity, + getHomeCommunityWithFederatedCommunityOrFail, + getCommunityByPublicKeyOrFail, + findPendingCommunityHandshake, + CommunityHandshakeStateType +} from '..' +import { describe, expect, it, beforeEach, beforeAll, afterAll } from 'vitest' +import { createCommunity, createVerifiedFederatedCommunity } from '../seeds/community' +import { Ed25519PublicKey } from 'shared' + +const db = AppDatabase.getInstance() + +beforeAll(async () => { + await db.init() +}) +afterAll(async () => { + await db.destroy() +}) + +describe('communityHandshakes', () => { + // clean db for every test case + beforeEach(async () => { + await DbCommunity.clear() + await DbFederatedCommunity.clear() + await DbCommunityHandshakeState.clear() + }) + + it('should find pending community handshake by public key', async () => { + const com1 = await createCommunity(false) + await createVerifiedFederatedCommunity('1_0', 100, com1) + const state = new DbCommunityHandshakeState() + state.publicKey = com1.publicKey + state.apiVersion = '1_0' + state.status = CommunityHandshakeStateType.OPEN_CONNECTION_CALLBACK + state.handshakeId = 1 + await state.save() + const communityHandshakeState = await findPendingCommunityHandshake(new Ed25519PublicKey(com1.publicKey), '1_0') + expect(communityHandshakeState).toBeDefined() + expect(communityHandshakeState).toMatchObject({ + publicKey: com1.publicKey, + apiVersion: '1_0', + status: CommunityHandshakeStateType.OPEN_CONNECTION_CALLBACK, + handshakeId: 1 + }) + }) + + it('try to use returned public key for loading community', async () => { + // test this explicit case, because in federation.authentication it lead to server crash + const com1 = await createCommunity(false) + await createVerifiedFederatedCommunity('1_0', 100, com1) + const state = new DbCommunityHandshakeState() + state.publicKey = com1.publicKey + state.apiVersion = '1_0' + state.status = CommunityHandshakeStateType.OPEN_CONNECTION_CALLBACK + state.handshakeId = 1 + await state.save() + const communityHandshakeState = await findPendingCommunityHandshake(new Ed25519PublicKey(com1.publicKey), '1_0') + expect(communityHandshakeState).toBeDefined() + expect(communityHandshakeState?.federatedCommunity?.community).toBeDefined() + const ed25519PublicKey = new Ed25519PublicKey(communityHandshakeState?.federatedCommunity?.community?.publicKey) + const community = await DbCommunity.findOneBy({ publicKey: ed25519PublicKey.asBuffer() }) + expect(community).toBeDefined() + expect(community).toMatchObject({ + communityUuid: com1.communityUuid, + name: com1.name, + description: com1.description, + url: com1.url, + creationDate: com1.creationDate, + authenticatedAt: com1.authenticatedAt, + foreign: com1.foreign, + publicKey: com1.publicKey, + privateKey: com1.privateKey + }) + }) +}) \ No newline at end of file diff --git a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts index 930563e87..4e3c0eaeb 100644 --- a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts @@ -10,7 +10,7 @@ import { FederatedCommunityLoggingView, getHomeCommunity, findPendingCommunityHandshakeOrFailByOneTimeCode, - getCommunityByPublicKeyOrFail, + Community as DbCommunity, } from 'database' import { getLogger } from 'log4js' import { @@ -170,16 +170,20 @@ export class AuthenticationResolver { `invalid uuid: ${authArgs.uuid} for community with publicKey ${authComPublicKey.asHex()}` ) } - methodLogger.debug('before loading auth community again from db') - const authComFresh = await getCommunityByPublicKeyOrFail(argsPublicKey) - authComFresh.communityUuid = communityUuid.data - authComFresh.authenticatedAt = new Date() - methodLogger.debug('after loading auth community again from db') - methodLogger.debug('try to save: ', authComFresh) - await authComFresh.save().catch((err) => { - methodLogger.fatal('failed to save authCom:', err) - }) - methodLogger.debug('store authCom.uuid successfully:', new CommunityLoggingView(authCom)) + methodLogger.debug('before updating auth community again from db') + // need to use query builder, loading from db, changing and save lead to server crash with this error: + // TypeError [ERR_INVALID_ARG_TYPE]: The "otherBuffer" argument must be of type Buffer or Uint8Array. Received an instance of Object + // seems to be a typeorm problem with Buffer, even if I give a freshly created Buffer for public_key + await DbCommunity.createQueryBuilder() + .update(DbCommunity) + .set({ + communityUuid: communityUuid.data, + authenticatedAt: new Date(), + }) + .where({ id: authCom.id }) + .execute() + methodLogger.debug('update authCom.uuid successfully') + const homeComB = await getHomeCommunity() if (homeComB?.communityUuid) { const responseArgs = new AuthenticationResponseJwtPayloadType(args.handshakeID,homeComB.communityUuid) diff --git a/federation/src/index.ts b/federation/src/index.ts index d2249badb..a4247a2cc 100644 --- a/federation/src/index.ts +++ b/federation/src/index.ts @@ -6,9 +6,10 @@ import { getLogger } from 'log4js' // config import { CONFIG } from './config' import { LOG4JS_BASE_CATEGORY_NAME } from './config/const' -import { onShutdown, ShutdownReason } from 'shared' +import { onShutdown, printServerCrashAsciiArt, ShutdownReason } from 'shared' async function main() { + const startTime = new Date() // init logger const log4jsConfigFileName = CONFIG.LOG4JS_CONFIG_PLACEHOLDER.replace('%v', CONFIG.FEDERATION_API) initLogger( @@ -32,7 +33,10 @@ async function main() { if (ShutdownReason.SIGINT === reason || ShutdownReason.SIGTERM === reason) { logger.info(`graceful shutdown: ${reason}`) } else { - logger.error(`crash: ${reason}`, error) + const endTime = new Date() + const duration = endTime.getTime() - startTime.getTime() + printServerCrashAsciiArt(logger, `reason: ${reason}`, `duration: ${duration}ms`, '') + logger.error(error) } }) }) diff --git a/shared/src/helper/onShutdown.ts b/shared/src/helper/onShutdown.ts index c4d046071..c6e166d43 100644 --- a/shared/src/helper/onShutdown.ts +++ b/shared/src/helper/onShutdown.ts @@ -1,3 +1,5 @@ +import { Logger } from "log4js" + export enum ShutdownReason { SIGINT = 'SIGINT', SIGTERM = 'SIGTERM', @@ -38,6 +40,11 @@ export function onShutdown(shutdownHandler: (reason: ShutdownReason, error?: Err process.emit("SIGINT" as any) }) } +} - +export function printServerCrashAsciiArt(logger: Logger, msg1: string, msg2: string, msg3: string) { + logger.error(` /\\_/\\ ${msg1}`) + logger.error(`( x.x ) ${msg2}`) + logger.error(`> < ${msg3}`) + logger.error('') } \ No newline at end of file From c0a60f4ca6bce16988a60ecae29c1e3681c9e17b Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Tue, 14 Oct 2025 11:04:12 +0200 Subject: [PATCH 29/40] skip compare altogether --- .../api/1_0/resolver/AuthenticationResolver.ts | 4 ++-- federation/src/index.ts | 2 +- shared/package.json | 3 ++- shared/src/helper/onShutdown.ts | 13 +++++++------ 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts index 4e3c0eaeb..b6e7ca5c2 100644 --- a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts @@ -159,11 +159,11 @@ export class AuthenticationResolver { const authComPublicKey = new Ed25519PublicKey(authCom.publicKey) methodLogger.debug('authCom.publicKey', authComPublicKey.asHex()) methodLogger.debug('args.publicKey', argsPublicKey.asHex()) - if (!authComPublicKey.isSame(argsPublicKey)) { + /*if (!authComPublicKey.isSame(argsPublicKey)) { throw new Error( `corrupt authentication call detected, oneTimeCode: ${authArgs.oneTimeCode} doesn't belong to caller: ${argsPublicKey.asHex()}` ) - } + }*/ const communityUuid = uuidv4Schema.safeParse(authArgs.uuid) if (!communityUuid.success) { throw new Error( diff --git a/federation/src/index.ts b/federation/src/index.ts index a4247a2cc..9e8738b80 100644 --- a/federation/src/index.ts +++ b/federation/src/index.ts @@ -35,7 +35,7 @@ async function main() { } else { const endTime = new Date() const duration = endTime.getTime() - startTime.getTime() - printServerCrashAsciiArt(logger, `reason: ${reason}`, `duration: ${duration}ms`, '') + printServerCrashAsciiArt(`reason: ${reason}`, `duration: ${duration}ms`, '') logger.error(error) } }) diff --git a/shared/package.json b/shared/package.json index 9f15c67bc..647baa1bb 100644 --- a/shared/package.json +++ b/shared/package.json @@ -34,9 +34,10 @@ }, "dependencies": { "decimal.js-light": "^2.5.1", - "esbuild": "^0.25.2", + "esbuild": "^0.25.2", "jose": "^4.14.4", "log4js": "^6.9.1", + "yoctocolors-cjs": "^2.1.2", "zod": "^3.25.61" }, "engines": { diff --git a/shared/src/helper/onShutdown.ts b/shared/src/helper/onShutdown.ts index c6e166d43..395c4274b 100644 --- a/shared/src/helper/onShutdown.ts +++ b/shared/src/helper/onShutdown.ts @@ -1,4 +1,5 @@ -import { Logger } from "log4js" +import { Logger } from 'log4js' +import colors from 'yoctocolors-cjs' export enum ShutdownReason { SIGINT = 'SIGINT', @@ -42,9 +43,9 @@ export function onShutdown(shutdownHandler: (reason: ShutdownReason, error?: Err } } -export function printServerCrashAsciiArt(logger: Logger, msg1: string, msg2: string, msg3: string) { - logger.error(` /\\_/\\ ${msg1}`) - logger.error(`( x.x ) ${msg2}`) - logger.error(`> < ${msg3}`) - logger.error('') +export function printServerCrashAsciiArt(msg1: string, msg2: string, msg3: string) { + console.error(colors.redBright(` /\\_/\\ ${msg1}`)) + console.error(colors.redBright(`( x.x ) ${msg2}`)) + console.error(colors.redBright(`> < ${msg3}`)) + console.error(colors.redBright('')) } \ No newline at end of file From b7f34ace84d5ad4bbc5e879adb3b8b9f7a76b709 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Tue, 14 Oct 2025 11:08:04 +0200 Subject: [PATCH 30/40] fix crash log, uncomment code which may lead to crash --- .../graphql/api/1_0/resolver/AuthenticationResolver.ts | 9 +++++---- federation/src/index.ts | 2 +- shared/src/helper/onShutdown.ts | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts index b6e7ca5c2..38089a9d3 100644 --- a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts @@ -159,11 +159,11 @@ export class AuthenticationResolver { const authComPublicKey = new Ed25519PublicKey(authCom.publicKey) methodLogger.debug('authCom.publicKey', authComPublicKey.asHex()) methodLogger.debug('args.publicKey', argsPublicKey.asHex()) - /*if (!authComPublicKey.isSame(argsPublicKey)) { + if (!authComPublicKey.isSame(argsPublicKey)) { throw new Error( `corrupt authentication call detected, oneTimeCode: ${authArgs.oneTimeCode} doesn't belong to caller: ${argsPublicKey.asHex()}` ) - }*/ + } const communityUuid = uuidv4Schema.safeParse(authArgs.uuid) if (!communityUuid.success) { throw new Error( @@ -174,7 +174,7 @@ export class AuthenticationResolver { // need to use query builder, loading from db, changing and save lead to server crash with this error: // TypeError [ERR_INVALID_ARG_TYPE]: The "otherBuffer" argument must be of type Buffer or Uint8Array. Received an instance of Object // seems to be a typeorm problem with Buffer, even if I give a freshly created Buffer for public_key - await DbCommunity.createQueryBuilder() + /*await DbCommunity.createQueryBuilder() .update(DbCommunity) .set({ communityUuid: communityUuid.data, @@ -183,7 +183,8 @@ export class AuthenticationResolver { .where({ id: authCom.id }) .execute() methodLogger.debug('update authCom.uuid successfully') - + */ + methodLogger.debug('skipped community update') const homeComB = await getHomeCommunity() if (homeComB?.communityUuid) { const responseArgs = new AuthenticationResponseJwtPayloadType(args.handshakeID,homeComB.communityUuid) diff --git a/federation/src/index.ts b/federation/src/index.ts index 9e8738b80..a113ef5a4 100644 --- a/federation/src/index.ts +++ b/federation/src/index.ts @@ -35,7 +35,7 @@ async function main() { } else { const endTime = new Date() const duration = endTime.getTime() - startTime.getTime() - printServerCrashAsciiArt(`reason: ${reason}`, `duration: ${duration}ms`, '') + printServerCrashAsciiArt('Server Crash', `reason: ${reason}`, `duration: ${duration}ms`) logger.error(error) } }) diff --git a/shared/src/helper/onShutdown.ts b/shared/src/helper/onShutdown.ts index 395c4274b..a27c931a6 100644 --- a/shared/src/helper/onShutdown.ts +++ b/shared/src/helper/onShutdown.ts @@ -46,6 +46,6 @@ export function onShutdown(shutdownHandler: (reason: ShutdownReason, error?: Err export function printServerCrashAsciiArt(msg1: string, msg2: string, msg3: string) { console.error(colors.redBright(` /\\_/\\ ${msg1}`)) console.error(colors.redBright(`( x.x ) ${msg2}`)) - console.error(colors.redBright(`> < ${msg3}`)) + console.error(colors.redBright(` > < ${msg3}`)) console.error(colors.redBright('')) } \ No newline at end of file From 8f40342665ba5c6b21663e5c214437666cdc4969 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Tue, 14 Oct 2025 11:13:09 +0200 Subject: [PATCH 31/40] update bun lock file --- bun.lock | 17 +++++++++-------- federation/src/index.ts | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/bun.lock b/bun.lock index 5a76238fe..c7810fb83 100644 --- a/bun.lock +++ b/bun.lock @@ -449,6 +449,7 @@ "esbuild": "^0.25.2", "jose": "^4.14.4", "log4js": "^6.9.1", + "yoctocolors-cjs": "^2.1.2", "zod": "^3.25.61", }, "devDependencies": { @@ -1780,7 +1781,7 @@ "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], - "convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="], + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], "cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="], @@ -2816,7 +2817,7 @@ "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], - "ohash": ["ohash@1.1.6", "", {}, "sha512-TBu7PtV8YkAZn0tSxobKY2n2aAQva936lhRrj6957aDaCf9IEtqsKbgMzXE/F/sjqYOwmrukeORHNLe5glk7Cg=="], + "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], @@ -3636,8 +3637,6 @@ "@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - "@babel/core/convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], - "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], @@ -3724,20 +3723,22 @@ "@jest/source-map/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "@jest/transform/convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="], + "@jest/transform/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "@jest/transform/write-file-atomic": ["write-file-atomic@3.0.3", "", { "dependencies": { "imurmurhash": "^0.1.4", "is-typedarray": "^1.0.0", "signal-exit": "^3.0.2", "typedarray-to-buffer": "^3.1.5" } }, "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q=="], "@jest/types/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + "@morev/utils/ohash": ["ohash@1.1.6", "", {}, "sha512-TBu7PtV8YkAZn0tSxobKY2n2aAQva936lhRrj6957aDaCf9IEtqsKbgMzXE/F/sjqYOwmrukeORHNLe5glk7Cg=="], + "@morev/utils/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], "@nuxt/kit/consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], "@nuxt/kit/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], - "@nuxt/kit/ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], - "@nuxt/kit/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "@nuxt/kit/pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], @@ -3886,8 +3887,6 @@ "c12/dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="], - "c12/ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], - "c12/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "c12/pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], @@ -4248,6 +4247,8 @@ "unplugin-vue-components/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "v8-to-istanbul/convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="], + "vee-validate/@vue/devtools-api": ["@vue/devtools-api@7.7.7", "", { "dependencies": { "@vue/devtools-kit": "^7.7.7" } }, "sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg=="], "vee-validate/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], diff --git a/federation/src/index.ts b/federation/src/index.ts index a113ef5a4..c9f58b0e5 100644 --- a/federation/src/index.ts +++ b/federation/src/index.ts @@ -35,7 +35,7 @@ async function main() { } else { const endTime = new Date() const duration = endTime.getTime() - startTime.getTime() - printServerCrashAsciiArt('Server Crash', `reason: ${reason}`, `duration: ${duration}ms`) + printServerCrashAsciiArt('Server Crash', `reason: ${reason}`, `server was ${duration}ms online`) logger.error(error) } }) From d11a794dd5fa260f559102f675482640f9427718 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Tue, 14 Oct 2025 11:16:45 +0200 Subject: [PATCH 32/40] try another culprint --- .../src/graphql/api/1_0/resolver/AuthenticationResolver.ts | 3 ++- shared/package.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts index 38089a9d3..0c9e5e2e5 100644 --- a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts @@ -150,7 +150,8 @@ export class AuthenticationResolver { throw new Error('No valid pending community handshake found') } state.status = CommunityHandshakeStateType.SUCCESS - stateSaveResolver = state.save() + // stateSaveResolver = state.save() + await state.save() methodLogger.debug(`search community per oneTimeCode:`, authArgs.oneTimeCode) const authCom = state.federatedCommunity.community diff --git a/shared/package.json b/shared/package.json index 647baa1bb..d18fa29cf 100644 --- a/shared/package.json +++ b/shared/package.json @@ -34,7 +34,7 @@ }, "dependencies": { "decimal.js-light": "^2.5.1", - "esbuild": "^0.25.2", + "esbuild": "^0.25.2", "jose": "^4.14.4", "log4js": "^6.9.1", "yoctocolors-cjs": "^2.1.2", From 1c347b7ff71def8943c1899579b0ab00942272d4 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Tue, 14 Oct 2025 12:32:57 +0200 Subject: [PATCH 33/40] remove join from communityHandshakeState, await save direct, to prevent drag away errors --- .../src/federation/authenticateCommunities.ts | 5 +- .../src/entity/CommunityHandshakeState.ts | 7 +-- database/src/entity/FederatedCommunity.ts | 6 -- .../CommunityHandshakeStateLogging.view.ts | 6 +- .../src/queries/communityHandshakes.test.ts | 60 ++++++++----------- database/src/queries/communityHandshakes.ts | 8 +-- .../1_0/resolver/AuthenticationResolver.ts | 32 +++------- .../api/1_0/util/authenticateCommunity.ts | 29 +++------ 8 files changed, 49 insertions(+), 104 deletions(-) diff --git a/backend/src/federation/authenticateCommunities.ts b/backend/src/federation/authenticateCommunities.ts index c548be8e7..9226d652c 100644 --- a/backend/src/federation/authenticateCommunities.ts +++ b/backend/src/federation/authenticateCommunities.ts @@ -55,7 +55,7 @@ export async function startCommunityAuthentication( ) // check if a authentication is already in progress - const existingState = await findPendingCommunityHandshake(fedComBPublicKey, fedComB.apiVersion, false) + const existingState = await findPendingCommunityHandshake(fedComBPublicKey, fedComB.apiVersion) if (existingState) { const stateLogic = new CommunityHandshakeStateLogic(existingState) // retry on timeout or failure @@ -77,7 +77,7 @@ export async function startCommunityAuthentication( state.apiVersion = fedComB.apiVersion state.status = CommunityHandshakeStateType.START_COMMUNITY_AUTHENTICATION state.handshakeId = parseInt(handshakeID) - const stateSaveResolver = state.save() + await state.save() //create JWT with url in payload encrypted by foreignCom.publicJwtKey and signed with homeCom.privateJwtKey const payload = new OpenConnectionJwtPayloadType(handshakeID, @@ -92,7 +92,6 @@ export async function startCommunityAuthentication( args.publicKey = homeComAPublicKey.asHex() args.jwt = jws args.handshakeID = handshakeID - await stateSaveResolver methodLogger.debug('before client.openConnection() args:', args) const result = await client.openConnection(args) if (result) { diff --git a/database/src/entity/CommunityHandshakeState.ts b/database/src/entity/CommunityHandshakeState.ts index f3a27b834..eca9c07e7 100644 --- a/database/src/entity/CommunityHandshakeState.ts +++ b/database/src/entity/CommunityHandshakeState.ts @@ -1,6 +1,5 @@ import { CommunityHandshakeStateType } from '../enum' -import { BaseEntity, Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm' -import { FederatedCommunity } from './FederatedCommunity' +import { BaseEntity, Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm' @Entity('community_handshake_states') export class CommunityHandshakeState extends BaseEntity { @@ -35,8 +34,4 @@ export class CommunityHandshakeState extends BaseEntity { @UpdateDateColumn({ name: 'updated_at', type: 'datetime', precision: 3 }) updatedAt: Date - - @ManyToOne(() => FederatedCommunity, (federatedCommunity) => federatedCommunity.communityHandshakeStates) - @JoinColumn({ name: 'public_key', referencedColumnName: 'publicKey' }) - federatedCommunity: FederatedCommunity } \ No newline at end of file diff --git a/database/src/entity/FederatedCommunity.ts b/database/src/entity/FederatedCommunity.ts index 8993f0663..a6eaee80f 100644 --- a/database/src/entity/FederatedCommunity.ts +++ b/database/src/entity/FederatedCommunity.ts @@ -5,12 +5,10 @@ import { Entity, JoinColumn, ManyToOne, - OneToMany, PrimaryGeneratedColumn, UpdateDateColumn, } from 'typeorm' import { Community } from './Community' -import { CommunityHandshakeState } from './CommunityHandshakeState' @Entity('federated_communities') export class FederatedCommunity extends BaseEntity { @@ -62,8 +60,4 @@ export class FederatedCommunity extends BaseEntity { ) @JoinColumn({ name: 'public_key', referencedColumnName: 'publicKey' }) community?: Community - - @OneToMany(() => CommunityHandshakeState, (communityHandshakeState) => communityHandshakeState.federatedCommunity) - @JoinColumn({ name: 'public_key', referencedColumnName: 'publicKey' }) - communityHandshakeStates: CommunityHandshakeState[] } diff --git a/database/src/logging/CommunityHandshakeStateLogging.view.ts b/database/src/logging/CommunityHandshakeStateLogging.view.ts index 5345474a0..b7df2452d 100644 --- a/database/src/logging/CommunityHandshakeStateLogging.view.ts +++ b/database/src/logging/CommunityHandshakeStateLogging.view.ts @@ -1,6 +1,5 @@ import { CommunityHandshakeState } from '..' import { AbstractLoggingView } from './AbstractLogging.view' -import { FederatedCommunityLoggingView } from './FederatedCommunityLogging.view' export class CommunityHandshakeStateLoggingView extends AbstractLoggingView { public constructor(private self: CommunityHandshakeState) { @@ -16,10 +15,7 @@ export class CommunityHandshakeStateLoggingView extends AbstractLoggingView { status: this.self.status, lastError: this.self.lastError, createdAt: this.dateToString(this.self.createdAt), - updatedAt: this.dateToString(this.self.updatedAt), - federatedCommunity: this.self.federatedCommunity - ? new FederatedCommunityLoggingView(this.self.federatedCommunity) - : undefined, + updatedAt: this.dateToString(this.self.updatedAt), } } } \ No newline at end of file diff --git a/database/src/queries/communityHandshakes.test.ts b/database/src/queries/communityHandshakes.test.ts index badf5ad96..32eb31c4b 100644 --- a/database/src/queries/communityHandshakes.test.ts +++ b/database/src/queries/communityHandshakes.test.ts @@ -3,14 +3,13 @@ import { CommunityHandshakeState as DbCommunityHandshakeState, Community as DbCommunity, FederatedCommunity as DbFederatedCommunity, - getHomeCommunityWithFederatedCommunityOrFail, - getCommunityByPublicKeyOrFail, findPendingCommunityHandshake, CommunityHandshakeStateType } from '..' import { describe, expect, it, beforeEach, beforeAll, afterAll } from 'vitest' import { createCommunity, createVerifiedFederatedCommunity } from '../seeds/community' import { Ed25519PublicKey } from 'shared' +import { randomBytes } from 'node:crypto' const db = AppDatabase.getInstance() @@ -21,6 +20,15 @@ afterAll(async () => { await db.destroy() }) +async function createCommunityHandshakeState(publicKey: Buffer) { + const state = new DbCommunityHandshakeState() + state.publicKey = publicKey + state.apiVersion = '1_0' + state.status = CommunityHandshakeStateType.START_COMMUNITY_AUTHENTICATION + state.handshakeId = 1 + await state.save() +} + describe('communityHandshakes', () => { // clean db for every test case beforeEach(async () => { @@ -32,48 +40,32 @@ describe('communityHandshakes', () => { it('should find pending community handshake by public key', async () => { const com1 = await createCommunity(false) await createVerifiedFederatedCommunity('1_0', 100, com1) - const state = new DbCommunityHandshakeState() - state.publicKey = com1.publicKey - state.apiVersion = '1_0' - state.status = CommunityHandshakeStateType.OPEN_CONNECTION_CALLBACK - state.handshakeId = 1 - await state.save() + await createCommunityHandshakeState(com1.publicKey) const communityHandshakeState = await findPendingCommunityHandshake(new Ed25519PublicKey(com1.publicKey), '1_0') expect(communityHandshakeState).toBeDefined() expect(communityHandshakeState).toMatchObject({ publicKey: com1.publicKey, apiVersion: '1_0', - status: CommunityHandshakeStateType.OPEN_CONNECTION_CALLBACK, + status: CommunityHandshakeStateType.START_COMMUNITY_AUTHENTICATION, handshakeId: 1 }) }) - it('try to use returned public key for loading community', async () => { - // test this explicit case, because in federation.authentication it lead to server crash - const com1 = await createCommunity(false) - await createVerifiedFederatedCommunity('1_0', 100, com1) - const state = new DbCommunityHandshakeState() - state.publicKey = com1.publicKey - state.apiVersion = '1_0' - state.status = CommunityHandshakeStateType.OPEN_CONNECTION_CALLBACK - state.handshakeId = 1 - await state.save() - const communityHandshakeState = await findPendingCommunityHandshake(new Ed25519PublicKey(com1.publicKey), '1_0') + it('update state', async () => { + const publicKey = new Ed25519PublicKey(randomBytes(32)) + await createCommunityHandshakeState(publicKey.asBuffer()) + const communityHandshakeState = await findPendingCommunityHandshake(publicKey, '1_0') expect(communityHandshakeState).toBeDefined() - expect(communityHandshakeState?.federatedCommunity?.community).toBeDefined() - const ed25519PublicKey = new Ed25519PublicKey(communityHandshakeState?.federatedCommunity?.community?.publicKey) - const community = await DbCommunity.findOneBy({ publicKey: ed25519PublicKey.asBuffer() }) - expect(community).toBeDefined() - expect(community).toMatchObject({ - communityUuid: com1.communityUuid, - name: com1.name, - description: com1.description, - url: com1.url, - creationDate: com1.creationDate, - authenticatedAt: com1.authenticatedAt, - foreign: com1.foreign, - publicKey: com1.publicKey, - privateKey: com1.privateKey + communityHandshakeState!.status = CommunityHandshakeStateType.OPEN_CONNECTION_CALLBACK + await communityHandshakeState!.save() + const communityHandshakeState2 = await findPendingCommunityHandshake(publicKey, '1_0') + const states = await DbCommunityHandshakeState.find() + expect(communityHandshakeState2).toBeDefined() + expect(communityHandshakeState2).toMatchObject({ + publicKey: publicKey.asBuffer(), + apiVersion: '1_0', + status: CommunityHandshakeStateType.OPEN_CONNECTION_CALLBACK, + handshakeId: 1 }) }) }) \ No newline at end of file diff --git a/database/src/queries/communityHandshakes.ts b/database/src/queries/communityHandshakes.ts index 9181cfbb3..23fe4f0d1 100644 --- a/database/src/queries/communityHandshakes.ts +++ b/database/src/queries/communityHandshakes.ts @@ -5,12 +5,10 @@ import { Ed25519PublicKey } from 'shared' /** * Find a pending community handshake by public key. * @param publicKey The public key of the community. - * @param withRelations Whether to include the federated community and community in the result, default true. + * @param apiVersion The API version of the community. * @returns The CommunityHandshakeState with associated federated community and community. */ -export function findPendingCommunityHandshake( - publicKey: Ed25519PublicKey, apiVersion: string, withRelations = true -): Promise { +export function findPendingCommunityHandshake(publicKey: Ed25519PublicKey, apiVersion: string): Promise { return CommunityHandshakeState.findOne({ where: { publicKey: publicKey.asBuffer(), @@ -21,7 +19,6 @@ export function findPendingCommunityHandshake( CommunityHandshakeStateType.SUCCESS ])) }, - relations: withRelations ? { federatedCommunity: { community: true } } : undefined, }) } @@ -30,7 +27,6 @@ export function findPendingCommunityHandshakeOrFailByOneTimeCode( ): Promise { return CommunityHandshakeState.findOneOrFail({ where: { oneTimeCode }, - relations: { federatedCommunity: { community: true } }, }) } \ No newline at end of file diff --git a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts index 0c9e5e2e5..0df45f794 100644 --- a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts @@ -11,6 +11,7 @@ import { getHomeCommunity, findPendingCommunityHandshakeOrFailByOneTimeCode, Community as DbCommunity, + getCommunityByPublicKeyOrFail, } from 'database' import { getLogger } from 'log4js' import { @@ -126,7 +127,6 @@ export class AuthenticationResolver { methodLogger.addContext('handshakeID', args.handshakeID) methodLogger.debug(`authenticate() via apiVersion=1_0 ...`, args) let state: DbCommunityHandshakeState | null = null - let stateSaveResolver: Promise | undefined = undefined const argsPublicKey = new Ed25519PublicKey(args.publicKey) try { const authArgs = await interpretEncryptedTransferArgs(args) as AuthenticationJwtPayloadType @@ -150,11 +150,10 @@ export class AuthenticationResolver { throw new Error('No valid pending community handshake found') } state.status = CommunityHandshakeStateType.SUCCESS - // stateSaveResolver = state.save() await state.save() - + methodLogger.debug(`search community per oneTimeCode:`, authArgs.oneTimeCode) - const authCom = state.federatedCommunity.community + const authCom = await getCommunityByPublicKeyOrFail(argsPublicKey) if (authCom) { methodLogger.debug('found authCom:', new CommunityLoggingView(authCom)) const authComPublicKey = new Ed25519PublicKey(authCom.publicKey) @@ -171,21 +170,11 @@ export class AuthenticationResolver { `invalid uuid: ${authArgs.uuid} for community with publicKey ${authComPublicKey.asHex()}` ) } - methodLogger.debug('before updating auth community again from db') - // need to use query builder, loading from db, changing and save lead to server crash with this error: - // TypeError [ERR_INVALID_ARG_TYPE]: The "otherBuffer" argument must be of type Buffer or Uint8Array. Received an instance of Object - // seems to be a typeorm problem with Buffer, even if I give a freshly created Buffer for public_key - /*await DbCommunity.createQueryBuilder() - .update(DbCommunity) - .set({ - communityUuid: communityUuid.data, - authenticatedAt: new Date(), - }) - .where({ id: authCom.id }) - .execute() + authCom.communityUuid = communityUuid.data + authCom.authenticatedAt = new Date() + await authCom.save() methodLogger.debug('update authCom.uuid successfully') - */ - methodLogger.debug('skipped community update') + const homeComB = await getHomeCommunity() if (homeComB?.communityUuid) { const responseArgs = new AuthenticationResponseJwtPayloadType(args.handshakeID,homeComB.communityUuid) @@ -205,16 +194,11 @@ export class AuthenticationResolver { methodLogger.info(`state: ${new CommunityHandshakeStateLoggingView(state)}`) state.status = CommunityHandshakeStateType.FAILED state.lastError = errorString - stateSaveResolver = state.save() + await state.save() } methodLogger.error(`failed: ${errorString}`) // no infos to the caller return null - } finally { - if (stateSaveResolver) { - await stateSaveResolver - } } - } } diff --git a/federation/src/graphql/api/1_0/util/authenticateCommunity.ts b/federation/src/graphql/api/1_0/util/authenticateCommunity.ts index 00c140d9c..a2c9136b0 100644 --- a/federation/src/graphql/api/1_0/util/authenticateCommunity.ts +++ b/federation/src/graphql/api/1_0/util/authenticateCommunity.ts @@ -51,7 +51,7 @@ export async function startOpenConnectionCallback( methodLogger.debug(`Authentication: startOpenConnectionCallback() with:`, { publicKey: publicKey.asHex(), }) - const pendingState = await findPendingCommunityHandshake(publicKey, api, false) + const pendingState = await findPendingCommunityHandshake(publicKey, api) if (pendingState) { const stateLogic = new CommunityHandshakeStateLogic(pendingState) // retry on timeout or failure @@ -61,7 +61,6 @@ export async function startOpenConnectionCallback( return } } - let stateSaveResolver: Promise | undefined = undefined const state = new DbCommunityHandshakeState() try { const [homeComB, comA] = await Promise.all([ @@ -84,7 +83,7 @@ export async function startOpenConnectionCallback( state.status = CommunityHandshakeStateType.START_OPEN_CONNECTION_CALLBACK state.handshakeId = parseInt(handshakeID) state.oneTimeCode = oneTimeCode - stateSaveResolver = state.save() + await state.save() methodLogger.debug( `Authentication: store oneTimeCode in CommunityHandshakeState:`, new CommunityHandshakeStateLoggingView(state), @@ -103,13 +102,12 @@ export async function startOpenConnectionCallback( args.publicKey = new Ed25519PublicKey(homeComB.publicKey).asHex() args.jwt = jwt args.handshakeID = handshakeID - await stateSaveResolver const result = await client.openConnectionCallback(args) if (result) { methodLogger.debug(`startOpenConnectionCallback() successful: ${jwt}`) } else { methodLogger.debug(`jwt: ${jwt}`) - stateSaveResolver = errorState('startOpenConnectionCallback() failed', methodLogger, state) + await errorState('startOpenConnectionCallback() failed', methodLogger, state) } } } catch (err) { @@ -119,11 +117,7 @@ export async function startOpenConnectionCallback( } else { errorString = String(err) } - stateSaveResolver = errorState(`error in startOpenConnectionCallback: ${errorString}`, methodLogger, state) - } finally { - if (stateSaveResolver) { - await stateSaveResolver - } + await errorState(`error in startOpenConnectionCallback: ${errorString}`, methodLogger, state) } } @@ -139,7 +133,6 @@ export async function startAuthentication( fedComB: new FederatedCommunityLoggingView(fedComB), }) let state: DbCommunityHandshakeState | null = null - let stateSaveResolver: Promise | undefined = undefined const fedComBPublicKey = new Ed25519PublicKey(fedComB.publicKey) try { const homeComA = await getHomeCommunity() @@ -150,7 +143,7 @@ export async function startAuthentication( if (!comB.publicJwtKey) { throw new Error('Public JWT key still not exist for foreign community') } - state = await findPendingCommunityHandshake(fedComBPublicKey, fedComB.apiVersion, false) + state = await findPendingCommunityHandshake(fedComBPublicKey, fedComB.apiVersion) if (!state) { throw new Error('No pending community handshake found') } @@ -163,7 +156,7 @@ export async function startAuthentication( throw new Error('No valid pending community handshake found') } state.status = CommunityHandshakeStateType.START_AUTHENTICATION - stateSaveResolver = state.save() + await state.save() const client = AuthenticationClientFactory.getInstance(fedComB) @@ -196,12 +189,12 @@ export async function startAuthentication( comB.authenticatedAt = new Date() await DbCommunity.save(comB) state.status = CommunityHandshakeStateType.SUCCESS - stateSaveResolver = state.save() + await state.save() methodLogger.debug('Community Authentication successful:', new CommunityLoggingView(comB)) } else { state.status = CommunityHandshakeStateType.FAILED state.lastError = 'Community Authentication failed, empty response' - stateSaveResolver = state.save() + await state.save() methodLogger.error('Community Authentication failed:', authenticationArgs) } } @@ -215,12 +208,8 @@ export async function startAuthentication( if (state) { state.status = CommunityHandshakeStateType.FAILED state.lastError = errorString - stateSaveResolver = state.save() + await state.save() } methodLogger.error('error in startAuthentication:', errorString) - } finally { - if (stateSaveResolver) { - await stateSaveResolver - } } } From 2a7e97319bf9029933d3a915ad8afcf4f5ac17a2 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Tue, 14 Oct 2025 14:32:01 +0200 Subject: [PATCH 34/40] update logging, reduce logging redundant informations --- .../logic/interpretEncryptedTransferArgs.ts | 11 +- core/src/logic/Community.logic.ts | 13 -- core/src/logic/community.logic.ts | 12 ++ core/src/logic/index.ts | 4 +- .../1_0/resolver/AuthenticationResolver.ts | 87 +++++------ .../api/1_0/util/authenticateCommunity.ts | 135 +++++++++--------- shared/src/helper/BinaryData.ts | 8 +- 7 files changed, 126 insertions(+), 144 deletions(-) delete mode 100644 core/src/logic/Community.logic.ts create mode 100644 core/src/logic/community.logic.ts diff --git a/core/src/graphql/logic/interpretEncryptedTransferArgs.ts b/core/src/graphql/logic/interpretEncryptedTransferArgs.ts index 31bf47a56..576a2b3b8 100644 --- a/core/src/graphql/logic/interpretEncryptedTransferArgs.ts +++ b/core/src/graphql/logic/interpretEncryptedTransferArgs.ts @@ -1,5 +1,5 @@ import { EncryptedTransferArgs } from '../model/EncryptedTransferArgs' -import { JwtPayloadType } from 'shared' +import { Ed25519PublicKey, JwtPayloadType } from 'shared' import { Community as DbCommunity } from 'database' import { getLogger } from 'log4js' import { CommunityLoggingView, getHomeCommunity } from 'database' @@ -12,16 +12,17 @@ export const interpretEncryptedTransferArgs = async (args: EncryptedTransferArgs const methodLogger = createLogger('interpretEncryptedTransferArgs') methodLogger.addContext('handshakeID', args.handshakeID) methodLogger.debug('interpretEncryptedTransferArgs()... args:', args) + const argsPublicKey = new Ed25519PublicKey(args.publicKey) // first find with args.publicKey the community 'requestingCom', which starts the request // TODO: maybe use community from caller instead of loading it separately - const requestingCom = await DbCommunity.findOneBy({ publicKey: Buffer.from(args.publicKey, 'hex') }) + const requestingCom = await DbCommunity.findOneBy({ publicKey: argsPublicKey.asBuffer() }) if (!requestingCom) { - const errmsg = `unknown requesting community with publicKey ${args.publicKey}` + const errmsg = `unknown requesting community with publicKey ${argsPublicKey.asHex()}` methodLogger.error(errmsg) throw new Error(errmsg) } if (!requestingCom.publicJwtKey) { - const errmsg = `missing publicJwtKey of requesting community with publicKey ${args.publicKey}` + const errmsg = `missing publicJwtKey of requesting community with publicKey ${argsPublicKey.asHex()}` methodLogger.error(errmsg) throw new Error(errmsg) } @@ -31,7 +32,7 @@ export const interpretEncryptedTransferArgs = async (args: EncryptedTransferArgs const homeCom = await getHomeCommunity() const jwtPayload = await verifyAndDecrypt(args.handshakeID, args.jwt, homeCom!.privateJwtKey!, requestingCom.publicJwtKey) as JwtPayloadType if (!jwtPayload) { - const errmsg = `invalid payload of community with publicKey ${args.publicKey}` + const errmsg = `invalid payload of community with publicKey ${argsPublicKey.asHex()}` methodLogger.error(errmsg) throw new Error(errmsg) } diff --git a/core/src/logic/Community.logic.ts b/core/src/logic/Community.logic.ts deleted file mode 100644 index 40779c703..000000000 --- a/core/src/logic/Community.logic.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Community as DbCommunity, FederatedCommunity as DbFederatedCommunity } from 'database' - -export class CommunityLogic { - public constructor(private self: DbCommunity) {} - - public getFederatedCommunityWithApiOrFail(apiVersion: string): DbFederatedCommunity { - const fedCom = this.self.federatedCommunities?.find((fedCom) => fedCom.apiVersion === apiVersion) - if (!fedCom) { - throw new Error(`Missing federated community with api version ${apiVersion}`) - } - return fedCom - } -} \ No newline at end of file diff --git a/core/src/logic/community.logic.ts b/core/src/logic/community.logic.ts new file mode 100644 index 000000000..8fb9cd59b --- /dev/null +++ b/core/src/logic/community.logic.ts @@ -0,0 +1,12 @@ +import { Community as DbCommunity, FederatedCommunity as DbFederatedCommunity } from 'database' + +export function getFederatedCommunityWithApiOrFail( + community: DbCommunity, + apiVersion: string +): DbFederatedCommunity { + const fedCom = community.federatedCommunities?.find((fedCom) => fedCom.apiVersion === apiVersion) + if (!fedCom) { + throw new Error(`Missing federated community with api version ${apiVersion}`) + } + return fedCom +} diff --git a/core/src/logic/index.ts b/core/src/logic/index.ts index 809465850..7d7a943bf 100644 --- a/core/src/logic/index.ts +++ b/core/src/logic/index.ts @@ -1,2 +1,2 @@ -export { CommunityHandshakeStateLogic } from './CommunityHandshakeState.logic' -export { CommunityLogic } from './Community.logic' \ No newline at end of file +export * from './CommunityHandshakeState.logic' +export * from './community.logic' \ No newline at end of file diff --git a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts index 0df45f794..9a8008a3c 100644 --- a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts @@ -10,7 +10,6 @@ import { FederatedCommunityLoggingView, getHomeCommunity, findPendingCommunityHandshakeOrFailByOneTimeCode, - Community as DbCommunity, getCommunityByPublicKeyOrFail, } from 'database' import { getLogger } from 'log4js' @@ -27,11 +26,12 @@ import { import { Arg, Mutation, Resolver } from 'type-graphql' import { startAuthentication, startOpenConnectionCallback } from '../util/authenticateCommunity' -const createLogger = (method: string ) => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.resolver.AuthenticationResolver.${method}`) +// TODO: think about the case, when we have a higher api version, which still use this resolver +const apiVersion = '1_0' +const createLogger = (method: string ) => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.${apiVersion}.resolver.AuthenticationResolver.${method}`) @Resolver() export class AuthenticationResolver { - @Mutation(() => Boolean) async openConnection( @Arg('data') @@ -39,39 +39,37 @@ export class AuthenticationResolver { ): Promise { const methodLogger = createLogger('openConnection') methodLogger.addContext('handshakeID', args.handshakeID) - methodLogger.debug(`openConnection() via apiVersion=1_0:`, args) const argsPublicKey = new Ed25519PublicKey(args.publicKey) + methodLogger.debug(`start via apiVersion=${apiVersion}, public key: ${argsPublicKey.asHex()}`) try { const openConnectionJwtPayload = await interpretEncryptedTransferArgs(args) as OpenConnectionJwtPayloadType - methodLogger.debug('openConnectionJwtPayload', openConnectionJwtPayload) + methodLogger.debug(`openConnectionJwtPayload url: ${openConnectionJwtPayload.url}`) if (!openConnectionJwtPayload) { throw new Error(`invalid OpenConnection payload of requesting community with publicKey ${argsPublicKey.asHex()}`) } if (openConnectionJwtPayload.tokentype !== OpenConnectionJwtPayloadType.OPEN_CONNECTION_TYPE) { - throw new Error(`invalid tokentype of community with publicKey ${argsPublicKey.asHex()}`) + throw new Error(`invalid tokentype: ${openConnectionJwtPayload.tokentype} of community with publicKey ${argsPublicKey.asHex()}`) } if (!openConnectionJwtPayload.url) { throw new Error(`invalid url of community with publicKey ${argsPublicKey.asHex()}`) } - methodLogger.debug(`before DbFedCommunity.findOneByOrFail()...`, { publicKey: argsPublicKey.asHex() }) + // methodLogger.debug(`before DbFedCommunity.findOneByOrFail()...`, { publicKey: argsPublicKey.asHex() }) const fedComA = await DbFedCommunity.findOneByOrFail({ publicKey: argsPublicKey.asBuffer() }) - methodLogger.debug(`after DbFedCommunity.findOneByOrFail()...`, new FederatedCommunityLoggingView(fedComA)) + // methodLogger.debug(`after DbFedCommunity.findOneByOrFail()...`, new FederatedCommunityLoggingView(fedComA)) if (!openConnectionJwtPayload.url.startsWith(fedComA.endPoint)) { throw new Error(`invalid url of community with publicKey ${argsPublicKey.asHex()}`) } + if (fedComA.apiVersion !== apiVersion) { + throw new Error(`invalid apiVersion: ${fedComA.apiVersion} of community with publicKey ${argsPublicKey.asHex()}`) + } // no await to respond immediately and invoke callback-request asynchronously - void startOpenConnectionCallback(args.handshakeID, argsPublicKey, CONFIG.FEDERATION_API) + // important: startOpenConnectionCallback must catch all exceptions them self, or server will crash! + void startOpenConnectionCallback(args.handshakeID, argsPublicKey, fedComA) methodLogger.debug('openConnection() successfully initiated callback and returns true immediately...') return true } catch (err) { - let errorText = '' - if (err instanceof Error) { - errorText = err.message - } else { - errorText = String(err) - } - methodLogger.error('invalid jwt token:', errorText) + methodLogger.error('invalid jwt token:', err) // no infos to the caller return true } @@ -84,7 +82,7 @@ export class AuthenticationResolver { ): Promise { const methodLogger = createLogger('openConnectionCallback') methodLogger.addContext('handshakeID', args.handshakeID) - methodLogger.debug(`openConnectionCallback() via apiVersion=1_0 ...`, args) + methodLogger.debug(`start via apiVersion=${apiVersion}, public key: ${args.publicKey}`) try { // decrypt args.url with homeCom.privateJwtKey and verify signing with callbackFedCom.publicKey const openConnectionCallbackJwtPayload = await interpretEncryptedTransferArgs(args) as OpenConnectionCallbackJwtPayloadType @@ -92,27 +90,20 @@ export class AuthenticationResolver { throw new Error(`invalid OpenConnectionCallback payload of requesting community with publicKey ${args.publicKey}`) } const { endPoint, apiVersion } = splitUrlInEndPointAndApiVersion(openConnectionCallbackJwtPayload.url) - methodLogger.debug(`search fedComB per:`, endPoint, apiVersion) + // methodLogger.debug(`search fedComB per:`, endPoint, apiVersion) const fedComB = await DbFedCommunity.findOneBy({ endPoint, apiVersion }) if (!fedComB) { - throw new Error(`unknown callback community with url ${openConnectionCallbackJwtPayload.url}`) + throw new Error(`unknown callback community for ${endPoint}${apiVersion}`) } methodLogger.debug( - `found fedComB and start authentication:`, - new FederatedCommunityLoggingView(fedComB), + `found fedComB and start authentication: ${fedComB.endPoint}${fedComB.apiVersion}`, ) // no await to respond immediately and invoke authenticate-request asynchronously void startAuthentication(args.handshakeID, openConnectionCallbackJwtPayload.oneTimeCode, fedComB) - methodLogger.debug('openConnectionCallback() successfully initiated authentication and returns true immediately...') + // methodLogger.debug('openConnectionCallback() successfully initiated authentication and returns true immediately...') return true } catch (err) { - let errorText = '' - if (err instanceof Error) { - errorText = err.message - } else { - errorText = String(err) - } - methodLogger.error('invalid jwt token:', errorText) + methodLogger.error('invalid jwt token:', err) // no infos to the caller return true } @@ -125,13 +116,14 @@ export class AuthenticationResolver { ): Promise { const methodLogger = createLogger('authenticate') methodLogger.addContext('handshakeID', args.handshakeID) - methodLogger.debug(`authenticate() via apiVersion=1_0 ...`, args) + methodLogger.debug(`start via apiVersion=${apiVersion}, public key: ${args.publicKey}`) let state: DbCommunityHandshakeState | null = null const argsPublicKey = new Ed25519PublicKey(args.publicKey) try { const authArgs = await interpretEncryptedTransferArgs(args) as AuthenticationJwtPayloadType - methodLogger.debug(`interpreted authentication payload...authArgs:`, authArgs) + // methodLogger.debug(`interpreted authentication payload...authArgs:`, authArgs) if (!authArgs) { + methodLogger.debug(`interpretEncryptedTransferArgs was called with`, args) throw new Error(`invalid authentication payload of requesting community with publicKey ${argsPublicKey.asHex()}`) } const validOneTimeCode = uint32Schema.safeParse(Number(authArgs.oneTimeCode)) @@ -152,13 +144,13 @@ export class AuthenticationResolver { state.status = CommunityHandshakeStateType.SUCCESS await state.save() - methodLogger.debug(`search community per oneTimeCode:`, authArgs.oneTimeCode) + // methodLogger.debug(`search community per oneTimeCode:`, authArgs.oneTimeCode) const authCom = await getCommunityByPublicKeyOrFail(argsPublicKey) if (authCom) { - methodLogger.debug('found authCom:', new CommunityLoggingView(authCom)) + methodLogger.debug(`found authCom ${authCom.name}`) const authComPublicKey = new Ed25519PublicKey(authCom.publicKey) - methodLogger.debug('authCom.publicKey', authComPublicKey.asHex()) - methodLogger.debug('args.publicKey', argsPublicKey.asHex()) + // methodLogger.debug('authCom.publicKey', authComPublicKey.asHex()) + // methodLogger.debug('args.publicKey', argsPublicKey.asHex()) if (!authComPublicKey.isSame(argsPublicKey)) { throw new Error( `corrupt authentication call detected, oneTimeCode: ${authArgs.oneTimeCode} doesn't belong to caller: ${argsPublicKey.asHex()}` @@ -173,30 +165,29 @@ export class AuthenticationResolver { authCom.communityUuid = communityUuid.data authCom.authenticatedAt = new Date() await authCom.save() - methodLogger.debug('update authCom.uuid successfully') - + methodLogger.debug(`update authCom.uuid successfully with ${authCom.communityUuid} at ${authCom.authenticatedAt}`) + const homeComB = await getHomeCommunity() if (homeComB?.communityUuid) { const responseArgs = new AuthenticationResponseJwtPayloadType(args.handshakeID,homeComB.communityUuid) const responseJwt = await encryptAndSign(responseArgs, homeComB.privateJwtKey!, authCom.publicJwtKey!) return responseJwt } + } else { + throw new Error(`community with publicKey ${argsPublicKey.asHex()} not found`) } return null } catch (err) { - let errorString = '' - if (err instanceof Error) { - errorString = err.message - } else { - errorString = String(err) - } if (state) { - methodLogger.info(`state: ${new CommunityHandshakeStateLoggingView(state)}`) - state.status = CommunityHandshakeStateType.FAILED - state.lastError = errorString - await state.save() + try { + state.status = CommunityHandshakeStateType.FAILED + state.lastError = String(err) + await state.save() + } catch (err) { + methodLogger.error(`failed to save state`, new CommunityHandshakeStateLoggingView(state), err) + } } - methodLogger.error(`failed: ${errorString}`) + methodLogger.error(`failed`, err) // no infos to the caller return null } diff --git a/federation/src/graphql/api/1_0/util/authenticateCommunity.ts b/federation/src/graphql/api/1_0/util/authenticateCommunity.ts index a2c9136b0..efff863fa 100644 --- a/federation/src/graphql/api/1_0/util/authenticateCommunity.ts +++ b/federation/src/graphql/api/1_0/util/authenticateCommunity.ts @@ -1,4 +1,4 @@ -import { CommunityHandshakeStateLogic, CommunityLogic, EncryptedTransferArgs, ensureUrlEndsWithSlash } from 'core' +import { CommunityHandshakeStateLogic, EncryptedTransferArgs, ensureUrlEndsWithSlash } from 'core' import { CommunityHandshakeStateLoggingView, CommunityLoggingView, @@ -6,6 +6,7 @@ import { FederatedCommunity as DbFedCommunity, FederatedCommunityLoggingView, findPendingCommunityHandshake, + getCommunityByPublicKeyOrFail, getCommunityWithFederatedCommunityWithApiOrFail, getHomeCommunity, getHomeCommunityWithFederatedCommunityOrFail, @@ -27,67 +28,55 @@ import { verifyAndDecrypt } from 'shared' import { CommunityHandshakeState as DbCommunityHandshakeState, CommunityHandshakeStateType } from 'database' +import { getFederatedCommunityWithApiOrFail } from 'core' const createLogger = (method: string ) => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.util.authenticateCommunity.${method}`) -async function errorState( - error: string, - methodLogger: Logger, - state: DbCommunityHandshakeState, -): Promise { - methodLogger.error(error) - state.status = CommunityHandshakeStateType.FAILED - state.lastError = error - return state.save() -} - export async function startOpenConnectionCallback( handshakeID: string, publicKey: Ed25519PublicKey, - api: string, + fedComA: DbFedCommunity, ): Promise { const methodLogger = createLogger('startOpenConnectionCallback') methodLogger.addContext('handshakeID', handshakeID) - methodLogger.debug(`Authentication: startOpenConnectionCallback() with:`, { - publicKey: publicKey.asHex(), - }) - const pendingState = await findPendingCommunityHandshake(publicKey, api) - if (pendingState) { - const stateLogic = new CommunityHandshakeStateLogic(pendingState) - // retry on timeout or failure - if (!(await stateLogic.isTimeoutUpdate())) { - // authentication with community and api version is still in progress and it is not timeout yet - methodLogger.debug('existingState, so we exit here', new CommunityHandshakeStateLoggingView(pendingState)) - return - } - } - const state = new DbCommunityHandshakeState() + methodLogger.debug(`start`) + const api = fedComA.apiVersion + + let state: DbCommunityHandshakeState | null = null try { + const pendingState = await findPendingCommunityHandshake(publicKey, api) + if (pendingState) { + const stateLogic = new CommunityHandshakeStateLogic(pendingState) + // retry on timeout or failure + if (!(await stateLogic.isTimeoutUpdate())) { + // authentication with community and api version is still in progress and it is not timeout yet + methodLogger.debug('existingState, so we exit here', new CommunityHandshakeStateLoggingView(pendingState)) + return + } + } + // load comA and comB parallel + // load with joined federated community of given api version const [homeComB, comA] = await Promise.all([ getHomeCommunityWithFederatedCommunityOrFail(api), - getCommunityWithFederatedCommunityWithApiOrFail(publicKey, api), + getCommunityByPublicKeyOrFail(publicKey), ]) - // load helpers - const homeComBLogic = new CommunityLogic(homeComB) - const comALogic = new CommunityLogic(comA) // get federated communities with correct api version - const homeFedComB = homeComBLogic.getFederatedCommunityWithApiOrFail(api) - const fedComA = comALogic.getFederatedCommunityWithApiOrFail(api) + // simply check and extract federated community from community of given api version or throw error if not found + const homeFedComB = getFederatedCommunityWithApiOrFail(homeComB, api) // TODO: make sure it is unique const oneTimeCode = randombytes_random() const oneTimeCodeString = oneTimeCode.toString() + // Create new community handshake state + state = new DbCommunityHandshakeState() state.publicKey = publicKey.asBuffer() state.apiVersion = api state.status = CommunityHandshakeStateType.START_OPEN_CONNECTION_CALLBACK state.handshakeId = parseInt(handshakeID) state.oneTimeCode = oneTimeCode - await state.save() - methodLogger.debug( - `Authentication: store oneTimeCode in CommunityHandshakeState:`, - new CommunityHandshakeStateLoggingView(state), - ) + state = await state.save() + methodLogger.debug('[START_OPEN_CONNECTION_CALLBACK] community handshake state created') const client = AuthenticationClientFactory.getInstance(fedComA) @@ -95,29 +84,37 @@ export async function startOpenConnectionCallback( const url = ensureUrlEndsWithSlash(homeFedComB.endPoint) + homeFedComB.apiVersion const callbackArgs = new OpenConnectionCallbackJwtPayloadType(handshakeID, oneTimeCodeString, url) - methodLogger.debug(`Authentication: start openConnectionCallback with args:`, callbackArgs) + // methodLogger.debug(`Authentication: start openConnectionCallback with args:`, callbackArgs) // encrypt callbackArgs with requestedCom.publicJwtKey and sign it with homeCom.privateJwtKey const jwt = await encryptAndSign(callbackArgs, homeComB.privateJwtKey!, comA.publicJwtKey!) const args = new EncryptedTransferArgs() args.publicKey = new Ed25519PublicKey(homeComB.publicKey).asHex() args.jwt = jwt args.handshakeID = handshakeID + methodLogger.debug(`invoke openConnectionCallback(), oneTimeCode: ${oneTimeCodeString}`) const result = await client.openConnectionCallback(args) if (result) { - methodLogger.debug(`startOpenConnectionCallback() successful: ${jwt}`) + methodLogger.debug(`startOpenConnectionCallback() successful`) } else { methodLogger.debug(`jwt: ${jwt}`) - await errorState('startOpenConnectionCallback() failed', methodLogger, state) + const errorString = 'startOpenConnectionCallback() failed' + methodLogger.error(errorString) + state.status = CommunityHandshakeStateType.FAILED + state.lastError = errorString + state = await state.save() } } } catch (err) { - let errorString: string = '' - if (err instanceof Error) { - errorString = err.message - } else { - errorString = String(err) + methodLogger.error('error in startOpenConnectionCallback', err) + if (state) { + try { + state.status = CommunityHandshakeStateType.FAILED + state.lastError = String(err) + state = await state.save() + } catch(e) { + methodLogger.error('error on saving CommunityHandshakeState', e) + } } - await errorState(`error in startOpenConnectionCallback: ${errorString}`, methodLogger, state) } } @@ -128,13 +125,10 @@ export async function startAuthentication( ): Promise { const methodLogger = createLogger('startAuthentication') methodLogger.addContext('handshakeID', handshakeID) - methodLogger.debug(`startAuthentication()...`, { - oneTimeCode, - fedComB: new FederatedCommunityLoggingView(fedComB), - }) + methodLogger.debug(`startAuthentication()... oneTimeCode: ${oneTimeCode}`) let state: DbCommunityHandshakeState | null = null - const fedComBPublicKey = new Ed25519PublicKey(fedComB.publicKey) try { + const fedComBPublicKey = new Ed25519PublicKey(fedComB.publicKey) const homeComA = await getHomeCommunity() const comB = await DbCommunity.findOneByOrFail({ foreign: true, @@ -168,29 +162,33 @@ export async function startAuthentication( args.publicKey = new Ed25519PublicKey(homeComA!.publicKey).asHex() args.jwt = jwt args.handshakeID = handshakeID - methodLogger.debug(`invoke authenticate() with:`, args) + methodLogger.debug(`invoke authenticate(), publicKey: ${args.publicKey}`) const responseJwt = await client.authenticate(args) - methodLogger.debug(`response of authenticate():`, responseJwt) + // methodLogger.debug(`response of authenticate():`, responseJwt) if (responseJwt !== null) { const payload = await verifyAndDecrypt(handshakeID, responseJwt, homeComA!.privateJwtKey!, comB.publicJwtKey!) as AuthenticationResponseJwtPayloadType - methodLogger.debug( + /*methodLogger.debug( `received payload from authenticate ComB:`, payload, new FederatedCommunityLoggingView(fedComB), - ) + )*/ if (payload.tokentype !== AuthenticationResponseJwtPayloadType.AUTHENTICATION_RESPONSE_TYPE) { throw new Error(`Invalid tokentype in authenticate-response of community with publicKey ${fedComBPublicKey.asHex()}`) } - if (!uuidv4Schema.safeParse(payload.uuid).success) { + const parsedUuidv4 = uuidv4Schema.safeParse(payload.uuid) + if (!parsedUuidv4.success) { throw new Error(`Invalid uuid in authenticate-response of community with publicKey ${fedComBPublicKey.asHex()}`) } - comB.communityUuid = payload.uuid + methodLogger.debug('received uuid from authenticate ComB:', parsedUuidv4.data) + comB.communityUuid = parsedUuidv4.data comB.authenticatedAt = new Date() await DbCommunity.save(comB) state.status = CommunityHandshakeStateType.SUCCESS await state.save() - methodLogger.debug('Community Authentication successful:', new CommunityLoggingView(comB)) + const endTime = new Date() + const duration = endTime.getTime() - state.createdAt.getTime() + methodLogger.debug(`Community Authentication successful in ${duration} ms`) } else { state.status = CommunityHandshakeStateType.FAILED state.lastError = 'Community Authentication failed, empty response' @@ -199,17 +197,16 @@ export async function startAuthentication( } } } catch (err) { - let errorString: string = '' - if (err instanceof Error) { - errorString = err.message - } else { - errorString = String(err) - } + methodLogger.error('error in startAuthentication:', err) if (state) { - state.status = CommunityHandshakeStateType.FAILED - state.lastError = errorString - await state.save() + try { + state.status = CommunityHandshakeStateType.FAILED + state.lastError = String(err) + await state.save() + } catch(e) { + methodLogger.error('error on saving CommunityHandshakeState', e) + } } - methodLogger.error('error in startAuthentication:', errorString) + } } diff --git a/shared/src/helper/BinaryData.ts b/shared/src/helper/BinaryData.ts index 583dccd4e..37d63b156 100644 --- a/shared/src/helper/BinaryData.ts +++ b/shared/src/helper/BinaryData.ts @@ -18,19 +18,13 @@ export class BinaryData { } else if (Buffer.isBuffer(input)) { this.buf = input this.hex = input.toString('hex') - } else if (input === undefined) { + } else { this.buf = Buffer.from('') this.hex = '' - } else { - throw new Error(`Either valid hex string or Buffer expected: ${input}`) } } asBuffer(): Buffer { - if (!this.buf || !Buffer.isBuffer(this.buf)) { - logging.warn('BinaryData.buf is invalid, try to create fresh buffer from hex') - this.buf = Buffer.from(this.hex, 'hex') - } return this.buf } From b78a627844624eea90dd2f453f88e032eb740c39 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Tue, 14 Oct 2025 14:50:05 +0200 Subject: [PATCH 35/40] more logging optimizations --- .../src/federation/authenticateCommunities.ts | 42 ++++++++++--------- backend/src/federation/validateCommunities.ts | 21 +++------- .../1_0/resolver/AuthenticationResolver.ts | 1 + .../api/1_0/util/authenticateCommunity.ts | 1 + 4 files changed, 31 insertions(+), 34 deletions(-) diff --git a/backend/src/federation/authenticateCommunities.ts b/backend/src/federation/authenticateCommunities.ts index 9226d652c..5c314ab93 100644 --- a/backend/src/federation/authenticateCommunities.ts +++ b/backend/src/federation/authenticateCommunities.ts @@ -7,12 +7,12 @@ import { findPendingCommunityHandshake, getHomeCommunityWithFederatedCommunityOrFail, CommunityHandshakeStateType, - getCommunityByPublicKeyOrFail + getCommunityByPublicKeyOrFail, } from 'database' import { randombytes_random } from 'sodium-native' import { AuthenticationClient as V1_0_AuthenticationClient } from '@/federation/client/1_0/AuthenticationClient' -import { ensureUrlEndsWithSlash } from 'core' +import { ensureUrlEndsWithSlash, getFederatedCommunityWithApiOrFail } from 'core' import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { communityAuthenticatedSchema, encryptAndSign, OpenConnectionJwtPayloadType } from 'shared' @@ -20,27 +20,29 @@ import { getLogger } from 'log4js' import { AuthenticationClientFactory } from './client/AuthenticationClientFactory' import { EncryptedTransferArgs } from 'core' import { CommunityHandshakeStateLogic } from 'core' -import { CommunityLogic } from 'core' import { Ed25519PublicKey } from 'shared' const createLogger = (functionName: string) => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.authenticateCommunities.${functionName}`) +export enum StartCommunityAuthenticationResult { + ALREADY_AUTHENTICATED = 'already authenticated', + ALREADY_IN_PROGRESS = 'already in progress', + SUCCESSFULLY_STARTED = 'successfully started', +} + export async function startCommunityAuthentication( fedComB: DbFederatedCommunity, -): Promise { +): Promise { const methodLogger = createLogger('startCommunityAuthentication') const handshakeID = randombytes_random().toString() methodLogger.addContext('handshakeID', handshakeID) - methodLogger.debug(`startCommunityAuthentication()...`, { - fedComB: new FederatedCommunityLoggingView(fedComB), - }) + methodLogger.debug(`start with public key ${fedComB.publicKey}`) const homeComA = await getHomeCommunityWithFederatedCommunityOrFail(fedComB.apiVersion) - methodLogger.debug('homeComA', new CommunityLoggingView(homeComA)) - const homeComALogic = new CommunityLogic(homeComA) - const homeFedComA = homeComALogic.getFederatedCommunityWithApiOrFail(fedComB.apiVersion) + // methodLogger.debug('homeComA', new CommunityLoggingView(homeComA)) + const homeFedComA = getFederatedCommunityWithApiOrFail(homeComA, fedComB.apiVersion) const fedComBPublicKey = new Ed25519PublicKey(fedComB.publicKey) const comB = await getCommunityByPublicKeyOrFail(fedComBPublicKey) - methodLogger.debug('started with comB:', new CommunityLoggingView(comB)) + // methodLogger.debug('started with comB:', new CommunityLoggingView(comB)) // check if communityUuid is not a valid v4Uuid // communityAuthenticatedSchema.safeParse return true @@ -48,11 +50,11 @@ export async function startCommunityAuthentication( // - if authenticatedAt is a valid date if (communityAuthenticatedSchema.safeParse(comB).success) { methodLogger.debug(`comB.communityUuid is already a valid v4Uuid ${ comB.communityUuid || 'null' } and was authenticated at ${ comB.authenticatedAt || 'null'}`) - return + return StartCommunityAuthenticationResult.ALREADY_AUTHENTICATED } - methodLogger.debug('comB.uuid is null or is a not valid v4Uuid...', + /*methodLogger.debug('comB.uuid is null or is a not valid v4Uuid...', comB.communityUuid || 'null', comB.authenticatedAt || 'null' - ) + )*/ // check if a authentication is already in progress const existingState = await findPendingCommunityHandshake(fedComBPublicKey, fedComB.apiVersion) @@ -62,7 +64,7 @@ export async function startCommunityAuthentication( if (!(await stateLogic.isTimeoutUpdate())) { // authentication with community and api version is still in progress and it is not timeout yet methodLogger.debug('existingState, so we exit here', new CommunityHandshakeStateLoggingView(existingState)) - return + return StartCommunityAuthenticationResult.ALREADY_IN_PROGRESS } } @@ -78,24 +80,25 @@ export async function startCommunityAuthentication( state.status = CommunityHandshakeStateType.START_COMMUNITY_AUTHENTICATION state.handshakeId = parseInt(handshakeID) await state.save() + methodLogger.debug('[START_COMMUNITY_AUTHENTICATION] community handshake state created') //create JWT with url in payload encrypted by foreignCom.publicJwtKey and signed with homeCom.privateJwtKey const payload = new OpenConnectionJwtPayloadType(handshakeID, ensureUrlEndsWithSlash(homeFedComA.endPoint).concat(homeFedComA.apiVersion), ) - methodLogger.debug('payload', payload) + // methodLogger.debug('payload', payload) const jws = await encryptAndSign(payload, homeComA!.privateJwtKey!, comB.publicJwtKey!) - methodLogger.debug('jws', jws) + // methodLogger.debug('jws', jws) // prepare the args for the client invocation const args = new EncryptedTransferArgs() const homeComAPublicKey = new Ed25519PublicKey(homeComA!.publicKey) args.publicKey = homeComAPublicKey.asHex() args.jwt = jws args.handshakeID = handshakeID - methodLogger.debug('before client.openConnection() args:', args) + // methodLogger.debug('before client.openConnection() args:', args) const result = await client.openConnection(args) if (result) { - methodLogger.info(`successful initiated at community:`, fedComB.endPoint) + methodLogger.debug(`successful initiated at community:`, fedComB.endPoint) } else { const errorMsg = `can't initiate at community: ${fedComB.endPoint}` methodLogger.error(errorMsg) @@ -104,4 +107,5 @@ export async function startCommunityAuthentication( } await state.save() } + return StartCommunityAuthenticationResult.SUCCESSFULLY_STARTED } diff --git a/backend/src/federation/validateCommunities.ts b/backend/src/federation/validateCommunities.ts index 8d8972ed5..bb5905aa3 100644 --- a/backend/src/federation/validateCommunities.ts +++ b/backend/src/federation/validateCommunities.ts @@ -12,7 +12,7 @@ import { FederationClient as V1_0_FederationClient } from '@/federation/client/1 import { PublicCommunityInfo } from '@/federation/client/1_0/model/PublicCommunityInfo' import { FederationClientFactory } from '@/federation/client/FederationClientFactory' import { LogError } from '@/server/LogError' -import { buffer32Schema, createKeyPair, Ed25519PublicKey, hex64Schema, uint32Schema } from 'shared' +import { createKeyPair, Ed25519PublicKey, uint32Schema } from 'shared' import { getLogger } from 'log4js' import { startCommunityAuthentication } from './authenticateCommunities' import { PublicCommunityInfoLoggingView } from './client/1_0/logging/PublicCommunityInfoLogging.view' @@ -28,16 +28,6 @@ export async function startValidateCommunities(timerInterval: number): Promise see https://javascript.info/settimeout-setinterval setTimeout(async function run() { @@ -56,11 +46,11 @@ export async function validateCommunities(): Promise { logger.debug(`found ${dbFederatedCommunities.length} dbCommunities`) for (const dbFedComB of dbFederatedCommunities) { - logger.debug('dbFedComB', new FederatedCommunityLoggingView(dbFedComB)) + logger.debug(`verify federation community: ${dbFedComB.endPoint}${dbFedComB.apiVersion}`) const apiValueStrings: string[] = Object.values(ApiVersionType) - logger.debug(`suppported ApiVersions=`, apiValueStrings) if (!apiValueStrings.includes(dbFedComB.apiVersion)) { logger.debug('dbFedComB with unsupported apiVersion', dbFedComB.endPoint, dbFedComB.apiVersion) + logger.debug(`supported ApiVersions=`, apiValueStrings) continue } try { @@ -73,13 +63,14 @@ export async function validateCommunities(): Promise { const fedComBPublicKey = new Ed25519PublicKey(dbFedComB.publicKey) if (clientPublicKey.isSame(fedComBPublicKey)) { await DbFederatedCommunity.update({ id: dbFedComB.id }, { verifiedAt: new Date() }) - logger.debug(`verified dbFedComB with:`, dbFedComB.endPoint) + // logger.debug(`verified dbFedComB with:`, dbFedComB.endPoint) const pubComInfo = await client.getPublicCommunityInfo() if (pubComInfo) { await writeForeignCommunity(dbFedComB, pubComInfo) logger.debug(`wrote response of getPublicCommunityInfo in dbFedComB ${dbFedComB.endPoint}`) try { - await startCommunityAuthentication(dbFedComB) + const result = await startCommunityAuthentication(dbFedComB) + logger.info(`${dbFedComB.endPoint}${dbFedComB.apiVersion} verified, authentication state: ${result}`) } catch (err) { logger.warn(`Warning: Authentication of community ${dbFedComB.endPoint} still ongoing:`, err) } diff --git a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts index 9a8008a3c..7a709d3f7 100644 --- a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts @@ -143,6 +143,7 @@ export class AuthenticationResolver { } state.status = CommunityHandshakeStateType.SUCCESS await state.save() + methodLogger.debug('[SUCCESS] community handshake state updated') // methodLogger.debug(`search community per oneTimeCode:`, authArgs.oneTimeCode) const authCom = await getCommunityByPublicKeyOrFail(argsPublicKey) diff --git a/federation/src/graphql/api/1_0/util/authenticateCommunity.ts b/federation/src/graphql/api/1_0/util/authenticateCommunity.ts index efff863fa..b6766f73a 100644 --- a/federation/src/graphql/api/1_0/util/authenticateCommunity.ts +++ b/federation/src/graphql/api/1_0/util/authenticateCommunity.ts @@ -186,6 +186,7 @@ export async function startAuthentication( await DbCommunity.save(comB) state.status = CommunityHandshakeStateType.SUCCESS await state.save() + methodLogger.debug('[SUCCESS] community handshake state updated') const endTime = new Date() const duration = endTime.getTime() - state.createdAt.getTime() methodLogger.debug(`Community Authentication successful in ${duration} ms`) From 2f69771e720db61f4733a9838db10c58a769699d Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Tue, 14 Oct 2025 14:53:58 +0200 Subject: [PATCH 36/40] cleanup headers --- backend/src/federation/authenticateCommunities.ts | 2 -- backend/src/federation/validateCommunities.ts | 4 +--- .../src/graphql/api/1_0/resolver/AuthenticationResolver.ts | 3 --- federation/src/graphql/api/1_0/util/authenticateCommunity.ts | 5 +---- 4 files changed, 2 insertions(+), 12 deletions(-) diff --git a/backend/src/federation/authenticateCommunities.ts b/backend/src/federation/authenticateCommunities.ts index 5c314ab93..980fabf6f 100644 --- a/backend/src/federation/authenticateCommunities.ts +++ b/backend/src/federation/authenticateCommunities.ts @@ -1,9 +1,7 @@ import { CommunityHandshakeState as DbCommunityHandshakeState, CommunityHandshakeStateLoggingView, - CommunityLoggingView, FederatedCommunity as DbFederatedCommunity, - FederatedCommunityLoggingView, findPendingCommunityHandshake, getHomeCommunityWithFederatedCommunityOrFail, CommunityHandshakeStateType, diff --git a/backend/src/federation/validateCommunities.ts b/backend/src/federation/validateCommunities.ts index bb5905aa3..10088cf35 100644 --- a/backend/src/federation/validateCommunities.ts +++ b/backend/src/federation/validateCommunities.ts @@ -1,9 +1,7 @@ import { Community as DbCommunity, FederatedCommunity as DbFederatedCommunity, - FederatedCommunityLoggingView, getHomeCommunity, - getNotReachableCommunities, } from 'database' import { IsNull } from 'typeorm' @@ -12,7 +10,7 @@ import { FederationClient as V1_0_FederationClient } from '@/federation/client/1 import { PublicCommunityInfo } from '@/federation/client/1_0/model/PublicCommunityInfo' import { FederationClientFactory } from '@/federation/client/FederationClientFactory' import { LogError } from '@/server/LogError' -import { createKeyPair, Ed25519PublicKey, uint32Schema } from 'shared' +import { createKeyPair, Ed25519PublicKey } from 'shared' import { getLogger } from 'log4js' import { startCommunityAuthentication } from './authenticateCommunities' import { PublicCommunityInfoLoggingView } from './client/1_0/logging/PublicCommunityInfoLogging.view' diff --git a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts index 7a709d3f7..d22c816ff 100644 --- a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts @@ -1,13 +1,10 @@ -import { CONFIG } from '@/config' import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { CommunityHandshakeStateLogic, EncryptedTransferArgs, interpretEncryptedTransferArgs, splitUrlInEndPointAndApiVersion } from 'core' import { - CommunityLoggingView, CommunityHandshakeStateLoggingView, CommunityHandshakeState as DbCommunityHandshakeState, CommunityHandshakeStateType, FederatedCommunity as DbFedCommunity, - FederatedCommunityLoggingView, getHomeCommunity, findPendingCommunityHandshakeOrFailByOneTimeCode, getCommunityByPublicKeyOrFail, diff --git a/federation/src/graphql/api/1_0/util/authenticateCommunity.ts b/federation/src/graphql/api/1_0/util/authenticateCommunity.ts index b6766f73a..23f336800 100644 --- a/federation/src/graphql/api/1_0/util/authenticateCommunity.ts +++ b/federation/src/graphql/api/1_0/util/authenticateCommunity.ts @@ -1,17 +1,14 @@ import { CommunityHandshakeStateLogic, EncryptedTransferArgs, ensureUrlEndsWithSlash } from 'core' import { CommunityHandshakeStateLoggingView, - CommunityLoggingView, Community as DbCommunity, FederatedCommunity as DbFedCommunity, - FederatedCommunityLoggingView, findPendingCommunityHandshake, getCommunityByPublicKeyOrFail, - getCommunityWithFederatedCommunityWithApiOrFail, getHomeCommunity, getHomeCommunityWithFederatedCommunityOrFail, } from 'database' -import { getLogger, Logger } from 'log4js' +import { getLogger } from 'log4js' import { AuthenticationClientFactory } from '@/client/AuthenticationClientFactory' import { randombytes_random } from 'sodium-native' From 044ff3358c204fcb239e565638f2b0a18a6048c6 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Tue, 14 Oct 2025 14:57:17 +0200 Subject: [PATCH 37/40] remove not used enum value --- database/src/enum/CommunityHandshakeStateType.ts | 1 - database/src/queries/communityHandshakes.test.ts | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/database/src/enum/CommunityHandshakeStateType.ts b/database/src/enum/CommunityHandshakeStateType.ts index 8b811da61..e41913cc0 100644 --- a/database/src/enum/CommunityHandshakeStateType.ts +++ b/database/src/enum/CommunityHandshakeStateType.ts @@ -2,7 +2,6 @@ export enum CommunityHandshakeStateType { START_COMMUNITY_AUTHENTICATION = 'START_COMMUNITY_AUTHENTICATION', START_OPEN_CONNECTION_CALLBACK = 'START_OPEN_CONNECTION_CALLBACK', START_AUTHENTICATION = 'START_AUTHENTICATION', - OPEN_CONNECTION_CALLBACK = 'OPEN_CONNECTION_CALLBACK', SUCCESS = 'SUCCESS', FAILED = 'FAILED', diff --git a/database/src/queries/communityHandshakes.test.ts b/database/src/queries/communityHandshakes.test.ts index 32eb31c4b..372fb1293 100644 --- a/database/src/queries/communityHandshakes.test.ts +++ b/database/src/queries/communityHandshakes.test.ts @@ -56,7 +56,7 @@ describe('communityHandshakes', () => { await createCommunityHandshakeState(publicKey.asBuffer()) const communityHandshakeState = await findPendingCommunityHandshake(publicKey, '1_0') expect(communityHandshakeState).toBeDefined() - communityHandshakeState!.status = CommunityHandshakeStateType.OPEN_CONNECTION_CALLBACK + communityHandshakeState!.status = CommunityHandshakeStateType.START_OPEN_CONNECTION_CALLBACK await communityHandshakeState!.save() const communityHandshakeState2 = await findPendingCommunityHandshake(publicKey, '1_0') const states = await DbCommunityHandshakeState.find() @@ -64,7 +64,7 @@ describe('communityHandshakes', () => { expect(communityHandshakeState2).toMatchObject({ publicKey: publicKey.asBuffer(), apiVersion: '1_0', - status: CommunityHandshakeStateType.OPEN_CONNECTION_CALLBACK, + status: CommunityHandshakeStateType.START_OPEN_CONNECTION_CALLBACK, handshakeId: 1 }) }) From dd0ce21a15d157651ffd3813945c00715231d7b4 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Tue, 14 Oct 2025 15:00:38 +0200 Subject: [PATCH 38/40] removed not used schemas --- shared/src/schema/base.schema.test.ts | 17 +---------------- shared/src/schema/base.schema.ts | 7 +------ 2 files changed, 2 insertions(+), 22 deletions(-) diff --git a/shared/src/schema/base.schema.test.ts b/shared/src/schema/base.schema.test.ts index db0a07d52..d12f2a3b4 100644 --- a/shared/src/schema/base.schema.test.ts +++ b/shared/src/schema/base.schema.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from 'bun:test' -import { generateKeyPairSync } from 'node:crypto' -import { uuidv4Schema, uint32Schema, buffer32Schema } from './base.schema' +import { uuidv4Schema, uint32Schema } from './base.schema' import { v4 as uuidv4 } from 'uuid' describe('uuidv4 schema', () => { @@ -23,17 +22,3 @@ describe('uint32 schema', () => { expect(uint32Schema.safeParse(2092352810).success).toBeTruthy() }) }) - -describe('buffer32 schema', () => { - it('should validate buffer', () => { - const { publicKey } = generateKeyPairSync('ed25519') - const buffer = publicKey.export({ type: 'spki', format: 'der' }).slice(-32) - expect(Buffer.isBuffer(buffer)).toBeTruthy() - expect(buffer.length).toBe(32) - expect(buffer32Schema.safeParse(buffer).success).toBeTruthy() - }) - - it("shouldn't validate string", () => { - expect(buffer32Schema.safeParse('3e1a2eecc95c48fedf47a522a8c77b91').success).toBeFalsy() - }) -}) diff --git a/shared/src/schema/base.schema.ts b/shared/src/schema/base.schema.ts index 0dcdf09a9..2f158b1b0 100644 --- a/shared/src/schema/base.schema.ts +++ b/shared/src/schema/base.schema.ts @@ -1,12 +1,7 @@ -import { string, number, custom } from 'zod' +import { string, number } from 'zod' import { validate, version } from 'uuid' export const uuidv4Schema = string().refine((val: string) => validate(val) && version(val) === 4, 'Invalid uuid') export const emailSchema = string().email() export const urlSchema = string().url() export const uint32Schema = number().positive().lte(4294967295) -export const buffer32Schema = custom( - (val: Buffer) => Buffer.isBuffer(val) && val.length === 32, - 'Invalid buffer' -) -export const hex64Schema = string().length(64).regex(/^[0-9A-Fa-f]$/) \ No newline at end of file From 6293eff1de4e3b3b0d76601c776961bf69ef57b9 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Tue, 14 Oct 2025 15:24:39 +0200 Subject: [PATCH 39/40] fix public key logging as hex --- backend/src/federation/authenticateCommunities.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/src/federation/authenticateCommunities.ts b/backend/src/federation/authenticateCommunities.ts index 980fabf6f..9c87da3c0 100644 --- a/backend/src/federation/authenticateCommunities.ts +++ b/backend/src/federation/authenticateCommunities.ts @@ -33,12 +33,13 @@ export async function startCommunityAuthentication( ): Promise { const methodLogger = createLogger('startCommunityAuthentication') const handshakeID = randombytes_random().toString() + const fedComBPublicKey = new Ed25519PublicKey(fedComB.publicKey) methodLogger.addContext('handshakeID', handshakeID) - methodLogger.debug(`start with public key ${fedComB.publicKey}`) + methodLogger.debug(`start with public key ${fedComBPublicKey.asHex()}`) const homeComA = await getHomeCommunityWithFederatedCommunityOrFail(fedComB.apiVersion) // methodLogger.debug('homeComA', new CommunityLoggingView(homeComA)) const homeFedComA = getFederatedCommunityWithApiOrFail(homeComA, fedComB.apiVersion) - const fedComBPublicKey = new Ed25519PublicKey(fedComB.publicKey) + const comB = await getCommunityByPublicKeyOrFail(fedComBPublicKey) // methodLogger.debug('started with comB:', new CommunityLoggingView(comB)) // check if communityUuid is not a valid v4Uuid From c8eeac2c02e9b762a6eb4355f7614f2589cf13f5 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Tue, 14 Oct 2025 15:52:39 +0200 Subject: [PATCH 40/40] react to case, if both communities try to authenticate each other at the same time --- database/src/queries/communityHandshakes.ts | 7 +++++-- .../src/graphql/api/1_0/util/authenticateCommunity.ts | 9 +++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/database/src/queries/communityHandshakes.ts b/database/src/queries/communityHandshakes.ts index 23fe4f0d1..9dc83118b 100644 --- a/database/src/queries/communityHandshakes.ts +++ b/database/src/queries/communityHandshakes.ts @@ -6,14 +6,17 @@ import { Ed25519PublicKey } from 'shared' * Find a pending community handshake by public key. * @param publicKey The public key of the community. * @param apiVersion The API version of the community. + * @param status The status of the community handshake. Optional, if not set, it will find a pending community handshake. * @returns The CommunityHandshakeState with associated federated community and community. */ -export function findPendingCommunityHandshake(publicKey: Ed25519PublicKey, apiVersion: string): Promise { +export function findPendingCommunityHandshake( + publicKey: Ed25519PublicKey, apiVersion: string, status?: CommunityHandshakeStateType +): Promise { return CommunityHandshakeState.findOne({ where: { publicKey: publicKey.asBuffer(), apiVersion, - status: Not(In([ + status: status || Not(In([ CommunityHandshakeStateType.EXPIRED, CommunityHandshakeStateType.FAILED, CommunityHandshakeStateType.SUCCESS diff --git a/federation/src/graphql/api/1_0/util/authenticateCommunity.ts b/federation/src/graphql/api/1_0/util/authenticateCommunity.ts index 23f336800..4b95898ba 100644 --- a/federation/src/graphql/api/1_0/util/authenticateCommunity.ts +++ b/federation/src/graphql/api/1_0/util/authenticateCommunity.ts @@ -41,7 +41,7 @@ export async function startOpenConnectionCallback( let state: DbCommunityHandshakeState | null = null try { - const pendingState = await findPendingCommunityHandshake(publicKey, api) + const pendingState = await findPendingCommunityHandshake(publicKey, api, CommunityHandshakeStateType.START_OPEN_CONNECTION_CALLBACK) if (pendingState) { const stateLogic = new CommunityHandshakeStateLogic(pendingState) // retry on timeout or failure @@ -134,15 +134,12 @@ export async function startAuthentication( if (!comB.publicJwtKey) { throw new Error('Public JWT key still not exist for foreign community') } - state = await findPendingCommunityHandshake(fedComBPublicKey, fedComB.apiVersion) + state = await findPendingCommunityHandshake(fedComBPublicKey, fedComB.apiVersion, CommunityHandshakeStateType.START_COMMUNITY_AUTHENTICATION) if (!state) { throw new Error('No pending community handshake found') } const stateLogic = new CommunityHandshakeStateLogic(state) - if ( - (await stateLogic.isTimeoutUpdate()) || - state.status !== CommunityHandshakeStateType.START_COMMUNITY_AUTHENTICATION - ) { + if ((await stateLogic.isTimeoutUpdate())) { methodLogger.debug('invalid state', new CommunityHandshakeStateLoggingView(state)) throw new Error('No valid pending community handshake found') }