From 49010af54fa1b856c8f4c84717f1f9bde79eff89 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Mon, 13 Oct 2025 17:12:03 +0200 Subject: [PATCH] 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