Merge pull request #3555 from gradido/federation_handshake_own_table

feat(federation): use own table for handshake state
This commit is contained in:
einhornimmond 2025-10-23 09:02:26 +02:00 committed by GitHub
commit 974fe032e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 884 additions and 313 deletions

View File

@ -1,78 +1,110 @@
import { CommunityLoggingView, Community as DbCommunity, FederatedCommunity as DbFederatedCommunity, FederatedCommunityLoggingView, getHomeCommunity } from 'database' import {
import { validate as validateUUID, version as versionUUID } from 'uuid' CommunityHandshakeState as DbCommunityHandshakeState,
CommunityHandshakeStateLoggingView,
FederatedCommunity as DbFederatedCommunity,
findPendingCommunityHandshake,
getHomeCommunityWithFederatedCommunityOrFail,
CommunityHandshakeStateType,
getCommunityByPublicKeyOrFail,
} from 'database'
import { randombytes_random } from 'sodium-native' 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 { 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 { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
import { encryptAndSign, OpenConnectionJwtPayloadType } from 'shared' import { communityAuthenticatedSchema, encryptAndSign, OpenConnectionJwtPayloadType } from 'shared'
import { getLogger } from 'log4js' import { getLogger } from 'log4js'
import { AuthenticationClientFactory } from './client/AuthenticationClientFactory' import { AuthenticationClientFactory } from './client/AuthenticationClientFactory'
import { EncryptedTransferArgs } from 'core' import { EncryptedTransferArgs } from 'core'
import { CommunityHandshakeStateLogic } from 'core'
import { Ed25519PublicKey } from 'shared'
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.authenticateCommunities`) 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( export async function startCommunityAuthentication(
fedComB: DbFederatedCommunity, fedComB: DbFederatedCommunity,
): Promise<void> { ): Promise<StartCommunityAuthenticationResult> {
const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.authenticateCommunities.startCommunityAuthentication`) const methodLogger = createLogger('startCommunityAuthentication')
const handshakeID = randombytes_random().toString() const handshakeID = randombytes_random().toString()
const fedComBPublicKey = new Ed25519PublicKey(fedComB.publicKey)
methodLogger.addContext('handshakeID', handshakeID) methodLogger.addContext('handshakeID', handshakeID)
methodLogger.debug(`startCommunityAuthentication()...`, { methodLogger.debug(`start with public key ${fedComBPublicKey.asHex()}`)
fedComB: new FederatedCommunityLoggingView(fedComB), const homeComA = await getHomeCommunityWithFederatedCommunityOrFail(fedComB.apiVersion)
}) // methodLogger.debug('homeComA', new CommunityLoggingView(homeComA))
const homeComA = await getHomeCommunity() const homeFedComA = getFederatedCommunityWithApiOrFail(homeComA, fedComB.apiVersion)
methodLogger.debug('homeComA', new CommunityLoggingView(homeComA!))
const homeFedComA = await DbFederatedCommunity.findOneByOrFail({ const comB = await getCommunityByPublicKeyOrFail(fedComBPublicKey)
foreign: false, // methodLogger.debug('started with comB:', new CommunityLoggingView(comB))
apiVersion: CONFIG_CORE.FEDERATION_BACKEND_SEND_ON_API,
})
methodLogger.debug('homeFedComA', new FederatedCommunityLoggingView(homeFedComA))
const comB = await DbCommunity.findOneByOrFail({ publicKey: fedComB.publicKey })
methodLogger.debug('started with comB:', new CommunityLoggingView(comB))
// check if communityUuid is not a valid v4Uuid // check if communityUuid is not a valid v4Uuid
try {
if ( // communityAuthenticatedSchema.safeParse return true
comB && // - if communityUuid is a valid v4Uuid and
((comB.communityUuid === null && comB.authenticatedAt === null) || // - if authenticatedAt is a valid date
(comB.communityUuid !== null && if (communityAuthenticatedSchema.safeParse(comB).success) {
(!validateUUID(comB.communityUuid) || methodLogger.debug(`comB.communityUuid is already a valid v4Uuid ${ comB.communityUuid || 'null' } and was authenticated at ${ comB.authenticatedAt || 'null'}`)
versionUUID(comB.communityUuid!) !== 4))) return StartCommunityAuthenticationResult.ALREADY_AUTHENTICATED
) {
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)
} }
methodLogger.removeContext('handshakeID') /*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)
if (existingState) {
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, so we exit here', new CommunityHandshakeStateLoggingView(existingState))
return StartCommunityAuthenticationResult.ALREADY_IN_PROGRESS
}
}
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}`)
}
const state = new DbCommunityHandshakeState()
state.publicKey = fedComBPublicKey.asBuffer()
state.apiVersion = fedComB.apiVersion
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)
const jws = await encryptAndSign(payload, homeComA!.privateJwtKey!, comB.publicJwtKey!)
// 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)
const result = await client.openConnection(args)
if (result) {
methodLogger.debug(`successful initiated at community:`, fedComB.endPoint)
} else {
const errorMsg = `can't initiate at community: ${fedComB.endPoint}`
methodLogger.error(errorMsg)
state.status = CommunityHandshakeStateType.FAILED
state.lastError = errorMsg
}
await state.save()
}
return StartCommunityAuthenticationResult.SUCCESSFULLY_STARTED
} }

View File

@ -103,7 +103,7 @@ describe('validate Communities', () => {
return { return {
data: { data: {
getPublicKey: { getPublicKey: {
publicKey: 'somePubKey', publicKey: '2222222222222222222222222222222222222222222222222222222222222222',
}, },
}, },
} as Response<unknown> } as Response<unknown>
@ -170,8 +170,8 @@ describe('validate Communities', () => {
it('logs not matching publicKeys', () => { it('logs not matching publicKeys', () => {
expect(logger.debug).toBeCalledWith( expect(logger.debug).toBeCalledWith(
'received not matching publicKey:', 'received not matching publicKey:',
'somePubKey', '2222222222222222222222222222222222222222222222222222222222222222',
expect.stringMatching('11111111111111111111111111111111'), expect.stringMatching('1111111111111111111111111111111100000000000000000000000000000000'),
) )
}) })
}) })

View File

@ -1,7 +1,6 @@
import { import {
Community as DbCommunity, Community as DbCommunity,
FederatedCommunity as DbFederatedCommunity, FederatedCommunity as DbFederatedCommunity,
FederatedCommunityLoggingView,
getHomeCommunity, getHomeCommunity,
} from 'database' } from 'database'
import { IsNull } from 'typeorm' import { IsNull } from 'typeorm'
@ -11,7 +10,7 @@ import { FederationClient as V1_0_FederationClient } from '@/federation/client/1
import { PublicCommunityInfo } from '@/federation/client/1_0/model/PublicCommunityInfo' import { PublicCommunityInfo } from '@/federation/client/1_0/model/PublicCommunityInfo'
import { FederationClientFactory } from '@/federation/client/FederationClientFactory' import { FederationClientFactory } from '@/federation/client/FederationClientFactory'
import { LogError } from '@/server/LogError' import { LogError } from '@/server/LogError'
import { createKeyPair } from 'shared' import { createKeyPair, Ed25519PublicKey } from 'shared'
import { getLogger } from 'log4js' import { getLogger } from 'log4js'
import { startCommunityAuthentication } from './authenticateCommunities' import { startCommunityAuthentication } from './authenticateCommunities'
import { PublicCommunityInfoLoggingView } from './client/1_0/logging/PublicCommunityInfoLogging.view' import { PublicCommunityInfoLoggingView } from './client/1_0/logging/PublicCommunityInfoLogging.view'
@ -45,27 +44,31 @@ export async function validateCommunities(): Promise<void> {
logger.debug(`found ${dbFederatedCommunities.length} dbCommunities`) logger.debug(`found ${dbFederatedCommunities.length} dbCommunities`)
for (const dbFedComB of dbFederatedCommunities) { 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) const apiValueStrings: string[] = Object.values(ApiVersionType)
logger.debug(`suppported ApiVersions=`, apiValueStrings)
if (!apiValueStrings.includes(dbFedComB.apiVersion)) { if (!apiValueStrings.includes(dbFedComB.apiVersion)) {
logger.debug('dbFedComB with unsupported apiVersion', dbFedComB.endPoint, dbFedComB.apiVersion) logger.debug('dbFedComB with unsupported apiVersion', dbFedComB.endPoint, dbFedComB.apiVersion)
logger.debug(`supported ApiVersions=`, apiValueStrings)
continue continue
} }
try { try {
const client = FederationClientFactory.getInstance(dbFedComB) const client = FederationClientFactory.getInstance(dbFedComB)
if (client instanceof V1_0_FederationClient) { if (client instanceof V1_0_FederationClient) {
const pubKey = await client.getPublicKey() // throw if key isn't valid hex with length 64
if (pubKey && pubKey === dbFedComB.publicKey.toString('hex')) { 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() }) 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() const pubComInfo = await client.getPublicCommunityInfo()
if (pubComInfo) { if (pubComInfo) {
await writeForeignCommunity(dbFedComB, pubComInfo) await writeForeignCommunity(dbFedComB, pubComInfo)
logger.debug(`wrote response of getPublicCommunityInfo in dbFedComB ${dbFedComB.endPoint}`) logger.debug(`wrote response of getPublicCommunityInfo in dbFedComB ${dbFedComB.endPoint}`)
try { try {
await startCommunityAuthentication(dbFedComB) const result = await startCommunityAuthentication(dbFedComB)
logger.info(`${dbFedComB.endPoint}${dbFedComB.apiVersion} verified, authentication state: ${result}`)
} catch (err) { } catch (err) {
logger.warn(`Warning: Authentication of community ${dbFedComB.endPoint} still ongoing:`, err) logger.warn(`Warning: Authentication of community ${dbFedComB.endPoint} still ongoing:`, err)
} }
@ -73,7 +76,7 @@ export async function validateCommunities(): Promise<void> {
logger.debug('missing result of getPublicCommunityInfo') logger.debug('missing result of getPublicCommunityInfo')
} }
} else { } else {
logger.debug('received not matching publicKey:', pubKey, dbFedComB.publicKey.toString('hex')) logger.debug('received not matching publicKey:', clientPublicKey.asHex(), fedComBPublicKey.asHex())
} }
} }
} catch (err) { } catch (err) {

View File

@ -449,6 +449,7 @@
"esbuild": "^0.25.2", "esbuild": "^0.25.2",
"jose": "^4.14.4", "jose": "^4.14.4",
"log4js": "^6.9.1", "log4js": "^6.9.1",
"yoctocolors-cjs": "^2.1.2",
"zod": "^3.25.61", "zod": "^3.25.61",
}, },
"devDependencies": { "devDependencies": {
@ -1780,7 +1781,7 @@
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], "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=="], "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=="], "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=="], "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/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/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=="], "@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/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/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/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=="], "@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=="], "@morev/utils/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
"@nuxt/kit/consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], "@nuxt/kit/consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
"@nuxt/kit/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "@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/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=="], "@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/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/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=="], "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=="], "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/@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=="], "vee-validate/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],

View File

@ -1,42 +1,41 @@
import { EncryptedTransferArgs } from '../model/EncryptedTransferArgs' import { EncryptedTransferArgs } from '../model/EncryptedTransferArgs'
import { JwtPayloadType } from 'shared' import { Ed25519PublicKey, JwtPayloadType } from 'shared'
import { Community as DbCommunity } from 'database' import { Community as DbCommunity } from 'database'
import { getLogger } from 'log4js' import { getLogger } from 'log4js'
import { CommunityLoggingView, getHomeCommunity } from 'database' import { CommunityLoggingView, getHomeCommunity } from 'database'
import { verifyAndDecrypt } from 'shared' import { verifyAndDecrypt } from 'shared'
import { LOG4JS_BASE_CATEGORY_NAME } from '../../config/const' 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<JwtPayloadType | null> => { export const interpretEncryptedTransferArgs = async (args: EncryptedTransferArgs): Promise<JwtPayloadType | null> => {
const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.logic.interpretEncryptedTransferArgs-method`) const methodLogger = createLogger('interpretEncryptedTransferArgs')
methodLogger.addContext('handshakeID', args.handshakeID) methodLogger.addContext('handshakeID', args.handshakeID)
methodLogger.debug('interpretEncryptedTransferArgs()... args:', args) methodLogger.debug('interpretEncryptedTransferArgs()... args:', args)
const argsPublicKey = new Ed25519PublicKey(args.publicKey)
// first find with args.publicKey the community 'requestingCom', which starts the request // first find with args.publicKey the community 'requestingCom', which starts the request
const requestingCom = await DbCommunity.findOneBy({ publicKey: Buffer.from(args.publicKey, 'hex') }) // TODO: maybe use community from caller instead of loading it separately
const requestingCom = await DbCommunity.findOneBy({ publicKey: argsPublicKey.asBuffer() })
if (!requestingCom) { if (!requestingCom) {
const errmsg = `unknown requesting community with publicKey ${Buffer.from(args.publicKey, 'hex')}` const errmsg = `unknown requesting community with publicKey ${argsPublicKey.asHex()}`
methodLogger.error(errmsg) methodLogger.error(errmsg)
methodLogger.removeContext('handshakeID')
throw new Error(errmsg) throw new Error(errmsg)
} }
if (!requestingCom.publicJwtKey) { 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 ${argsPublicKey.asHex()}`
methodLogger.error(errmsg) methodLogger.error(errmsg)
methodLogger.removeContext('handshakeID')
throw new Error(errmsg) throw new Error(errmsg)
} }
methodLogger.debug(`found requestingCom:`, new CommunityLoggingView(requestingCom)) methodLogger.debug(`found requestingCom:`, new CommunityLoggingView(requestingCom))
// verify the signing of args.jwt with homeCom.privateJwtKey and decrypt args.jwt with requestingCom.publicJwtKey // 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 homeCom = await getHomeCommunity()
const jwtPayload = await verifyAndDecrypt(args.handshakeID, args.jwt, homeCom!.privateJwtKey!, requestingCom.publicJwtKey) as JwtPayloadType const jwtPayload = await verifyAndDecrypt(args.handshakeID, args.jwt, homeCom!.privateJwtKey!, requestingCom.publicJwtKey) as JwtPayloadType
if (!jwtPayload) { if (!jwtPayload) {
const errmsg = `invalid payload of community with publicKey ${Buffer.from(args.publicKey, 'hex')}` const errmsg = `invalid payload of community with publicKey ${argsPublicKey.asHex()}`
methodLogger.error(errmsg) methodLogger.error(errmsg)
methodLogger.removeContext('handshakeID')
throw new Error(errmsg) throw new Error(errmsg)
} }
methodLogger.debug('jwtPayload', jwtPayload) methodLogger.debug('jwtPayload', jwtPayload)
methodLogger.removeContext('handshakeID')
return jwtPayload return jwtPayload
} }

View File

@ -22,4 +22,5 @@ export * from './util/calculateSenderBalance'
export * from './util/utilities' export * from './util/utilities'
export * from './validation/user' export * from './validation/user'
export * from './config/index' export * from './config/index'
export * from './logic'

View File

@ -0,0 +1,33 @@
import { CommunityHandshakeState, CommunityHandshakeStateType } from 'database'
import { FEDERATION_AUTHENTICATION_TIMEOUT_MS } from 'shared'
export class CommunityHandshakeStateLogic {
public constructor(private self: 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<boolean> {
const timeout = this.isTimeout()
if (timeout && this.self.status !== CommunityHandshakeStateType.EXPIRED) {
this.self.status = CommunityHandshakeStateType.EXPIRED
await this.self.save()
}
return timeout
}
public isTimeout(): boolean {
if (this.self.status === CommunityHandshakeStateType.EXPIRED) {
return true
}
if ((Date.now() - this.self.updatedAt.getTime()) > FEDERATION_AUTHENTICATION_TIMEOUT_MS) {
return true
}
return false
}
public isFailed(): boolean {
return this.self.status === CommunityHandshakeStateType.FAILED
}
}

View File

@ -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)
})
})

View File

@ -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
}

2
core/src/logic/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from './CommunityHandshakeState.logic'
export * from './community.logic'

View File

@ -36,6 +36,11 @@ export const delay = promisify(setTimeout)
export const ensureUrlEndsWithSlash = (url: string): string => { export const ensureUrlEndsWithSlash = (url: string): string => {
return url.endsWith('/') ? url : url.concat('/') 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. * Calculates the date representing the first day of the month, a specified number of months prior to a given date.
* *

View File

@ -0,0 +1,20 @@
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
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 NULL DEFAULT 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)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`)
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(`DROP TABLE community_handshake_states;`)
}

View File

@ -0,0 +1,37 @@
import { CommunityHandshakeStateType } from '../enum'
import { BaseEntity, Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'
@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, default: null, nullable: true })
oneTimeCode?: number
@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.START_COMMUNITY_AUTHENTICATION,
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
}

View File

@ -7,6 +7,7 @@ import { Event } from './Event'
import { FederatedCommunity } from './FederatedCommunity' import { FederatedCommunity } from './FederatedCommunity'
import { LoginElopageBuys } from './LoginElopageBuys' import { LoginElopageBuys } from './LoginElopageBuys'
import { Migration } from './Migration' import { Migration } from './Migration'
import { CommunityHandshakeState } from './CommunityHandshakeState'
import { OpenaiThreads } from './OpenaiThreads' import { OpenaiThreads } from './OpenaiThreads'
import { PendingTransaction } from './PendingTransaction' import { PendingTransaction } from './PendingTransaction'
import { ProjectBranding } from './ProjectBranding' import { ProjectBranding } from './ProjectBranding'
@ -18,6 +19,7 @@ import { UserRole } from './UserRole'
export { export {
Community, Community,
CommunityHandshakeState,
Contribution, Contribution,
ContributionLink, ContributionLink,
ContributionMessage, ContributionMessage,
@ -25,7 +27,7 @@ export {
Event, Event,
FederatedCommunity, FederatedCommunity,
LoginElopageBuys, LoginElopageBuys,
Migration, Migration,
ProjectBranding, ProjectBranding,
OpenaiThreads, OpenaiThreads,
PendingTransaction, PendingTransaction,
@ -38,6 +40,7 @@ export {
export const entities = [ export const entities = [
Community, Community,
CommunityHandshakeState,
Contribution, Contribution,
ContributionLink, ContributionLink,
ContributionMessage, ContributionMessage,

View File

@ -0,0 +1,9 @@
export enum CommunityHandshakeStateType {
START_COMMUNITY_AUTHENTICATION = 'START_COMMUNITY_AUTHENTICATION',
START_OPEN_CONNECTION_CALLBACK = 'START_OPEN_CONNECTION_CALLBACK',
START_AUTHENTICATION = 'START_AUTHENTICATION',
SUCCESS = 'SUCCESS',
FAILED = 'FAILED',
EXPIRED = 'EXPIRED'
}

View File

@ -0,0 +1 @@
export * from './CommunityHandshakeStateType'

View File

@ -1,64 +1,9 @@
import { latestDbVersion } from './detectLastDBVersion' 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 { latestDbVersion }
export * from './entity'
export * from './logging' export * from './logging'
export * from './queries' export * from './queries'
export * from './util' export * from './util'
export * from './enum'
export { AppDatabase } from './AppDatabase' export { AppDatabase } from './AppDatabase'

View File

@ -0,0 +1,21 @@
import { CommunityHandshakeState } from '..'
import { AbstractLoggingView } from './AbstractLogging.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),
}
}
}

View File

@ -1,5 +1,5 @@
import { Community } from '../entity' import { Community } from '../entity'
import { FederatedCommunityLoggingView } from './FederatedCommunityLogging.view'
import { AbstractLoggingView } from './AbstractLogging.view' import { AbstractLoggingView } from './AbstractLogging.view'
export class CommunityLoggingView extends AbstractLoggingView { export class CommunityLoggingView extends AbstractLoggingView {
@ -21,6 +21,9 @@ export class CommunityLoggingView extends AbstractLoggingView {
creationDate: this.dateToString(this.self.creationDate), creationDate: this.dateToString(this.self.creationDate),
createdAt: this.dateToString(this.self.createdAt), createdAt: this.dateToString(this.self.createdAt),
updatedAt: this.dateToString(this.self.updatedAt), updatedAt: this.dateToString(this.self.updatedAt),
federatedCommunities: this.self.federatedCommunities?.map(
(federatedCommunity) => new FederatedCommunityLoggingView(federatedCommunity)
),
} }
} }
} }

View File

@ -11,6 +11,7 @@ import { TransactionLoggingView } from './TransactionLogging.view'
import { UserContactLoggingView } from './UserContactLogging.view' import { UserContactLoggingView } from './UserContactLogging.view'
import { UserLoggingView } from './UserLogging.view' import { UserLoggingView } from './UserLogging.view'
import { UserRoleLoggingView } from './UserRoleLogging.view' import { UserRoleLoggingView } from './UserRoleLogging.view'
import { CommunityHandshakeStateLoggingView } from './CommunityHandshakeStateLogging.view'
export { export {
AbstractLoggingView, AbstractLoggingView,
@ -24,6 +25,7 @@ export {
UserContactLoggingView, UserContactLoggingView,
UserLoggingView, UserLoggingView,
UserRoleLoggingView, UserRoleLoggingView,
CommunityHandshakeStateLoggingView,
} }
export const logger = getLogger(LOG4JS_BASE_CATEGORY_NAME) export const logger = getLogger(LOG4JS_BASE_CATEGORY_NAME)

View File

@ -1,8 +1,9 @@
import { Community as DbCommunity, FederatedCommunity as DbFederatedCommunity } from '..' import { Community as DbCommunity, FederatedCommunity as DbFederatedCommunity } from '..'
import { AppDatabase } from '../AppDatabase' import { AppDatabase } from '../AppDatabase'
import { getHomeCommunity, getReachableCommunities } from './communities' import { getCommunityByPublicKeyOrFail, getHomeCommunity, getHomeCommunityWithFederatedCommunityOrFail, getReachableCommunities } from './communities'
import { describe, expect, it, beforeEach, beforeAll, afterAll } from 'vitest' import { describe, expect, it, beforeEach, beforeAll, afterAll } from 'vitest'
import { createCommunity, createVerifiedFederatedCommunity } from '../seeds/community' import { createCommunity, createVerifiedFederatedCommunity } from '../seeds/community'
import { Ed25519PublicKey } from 'shared'
const db = AppDatabase.getInstance() const db = AppDatabase.getInstance()
@ -39,6 +40,36 @@ describe('community.queries', () => {
expect(community?.privateKey).toStrictEqual(homeCom.privateKey) 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()
})
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', () => { describe('getReachableCommunities', () => {
it('home community counts also to reachable communities', async () => { it('home community counts also to reachable communities', async () => {
await createCommunity(false) await createCommunity(false)

View File

@ -1,6 +1,6 @@
import { FindOptionsOrder, FindOptionsWhere, IsNull, MoreThanOrEqual, Not } from 'typeorm' import { FindOptionsOrder, FindOptionsWhere, IsNull, MoreThanOrEqual, Not } from 'typeorm'
import { Community as DbCommunity } from '../entity' 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. * Retrieves the home community, i.e., a community that is not foreign.
@ -10,7 +10,14 @@ export async function getHomeCommunity(): Promise<DbCommunity | null> {
// TODO: Put in Cache, it is needed nearly always // 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 // TODO: return only DbCommunity or throw to reduce unnecessary checks, because there should be always a home community
return await DbCommunity.findOne({ return await DbCommunity.findOne({
where: { foreign: false }, where: { foreign: false }
})
}
export async function getHomeCommunityWithFederatedCommunityOrFail(apiVersion: string): Promise<DbCommunity> {
return await DbCommunity.findOneOrFail({
where: { foreign: false, federatedCommunities: { apiVersion } },
relations: { federatedCommunities: true },
}) })
} }
@ -42,6 +49,22 @@ export async function getCommunityWithFederatedCommunityByIdentifier(
}) })
} }
export async function getCommunityWithFederatedCommunityWithApiOrFail(
publicKey: Ed25519PublicKey,
apiVersion: string
): Promise<DbCommunity> {
return await DbCommunity.findOneOrFail({
where: { foreign: true, publicKey: publicKey.asBuffer(), federatedCommunities: { apiVersion } },
relations: { federatedCommunities: true },
})
}
export async function getCommunityByPublicKeyOrFail(publicKey: Ed25519PublicKey): Promise<DbCommunity> {
return await DbCommunity.findOneOrFail({
where: { publicKey: publicKey.asBuffer() },
})
}
// returns all reachable communities // returns all reachable communities
// home community and all federated communities which have been verified within the last authenticationTimeoutMs // home community and all federated communities which have been verified within the last authenticationTimeoutMs
export async function getReachableCommunities( export async function getReachableCommunities(
@ -60,4 +83,13 @@ export async function getReachableCommunities(
], ],
order, order,
}) })
}
export async function getNotReachableCommunities(
order?: FindOptionsOrder<DbCommunity>
): Promise<DbCommunity[]> {
return await DbCommunity.find({
where: { authenticatedAt: IsNull(), foreign: true },
order,
})
} }

View File

@ -0,0 +1,71 @@
import { AppDatabase } from '../AppDatabase'
import {
CommunityHandshakeState as DbCommunityHandshakeState,
Community as DbCommunity,
FederatedCommunity as DbFederatedCommunity,
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()
beforeAll(async () => {
await db.init()
})
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 () => {
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)
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.START_COMMUNITY_AUTHENTICATION,
handshakeId: 1
})
})
it('update state', async () => {
const publicKey = new Ed25519PublicKey(randomBytes(32))
await createCommunityHandshakeState(publicKey.asBuffer())
const communityHandshakeState = await findPendingCommunityHandshake(publicKey, '1_0')
expect(communityHandshakeState).toBeDefined()
communityHandshakeState!.status = CommunityHandshakeStateType.START_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.START_OPEN_CONNECTION_CALLBACK,
handshakeId: 1
})
})
})

View File

@ -0,0 +1,35 @@
import { Not, In } from 'typeorm'
import { CommunityHandshakeState, CommunityHandshakeStateType} from '..'
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, status?: CommunityHandshakeStateType
): Promise<CommunityHandshakeState | null> {
return CommunityHandshakeState.findOne({
where: {
publicKey: publicKey.asBuffer(),
apiVersion,
status: status || Not(In([
CommunityHandshakeStateType.EXPIRED,
CommunityHandshakeStateType.FAILED,
CommunityHandshakeStateType.SUCCESS
]))
},
})
}
export function findPendingCommunityHandshakeOrFailByOneTimeCode(
oneTimeCode: number
): Promise<CommunityHandshakeState> {
return CommunityHandshakeState.findOneOrFail({
where: { oneTimeCode },
})
}

View File

@ -5,5 +5,6 @@ export * from './communities'
export * from './pendingTransactions' export * from './pendingTransactions'
export * from './transactions' export * from './transactions'
export * from './transactionLinks' export * from './transactionLinks'
export * from './communityHandshakes'
export const LOG4JS_QUERIES_CATEGORY_NAME = `${LOG4JS_BASE_CATEGORY_NAME}.queries` export const LOG4JS_QUERIES_CATEGORY_NAME = `${LOG4JS_BASE_CATEGORY_NAME}.queries`

View File

@ -2,7 +2,13 @@ import { Community, FederatedCommunity } from '../entity'
import { randomBytes } from 'node:crypto' import { randomBytes } from 'node:crypto'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
export async function createCommunity(foreign: boolean, save: boolean = true): Promise<Community> { /**
* 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<Community> {
const community = new Community() const community = new Community()
community.publicKey = randomBytes(32) community.publicKey = randomBytes(32)
community.communityUuid = uuidv4() community.communityUuid = uuidv4()
@ -23,14 +29,22 @@ export async function createCommunity(foreign: boolean, save: boolean = true): P
community.description = 'HomeCommunity-description' community.description = 'HomeCommunity-description'
community.url = 'http://localhost/api' 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( export async function createVerifiedFederatedCommunity(
apiVersion: string, apiVersion: string,
verifiedBeforeMs: number, verifiedBeforeMs: number,
community: Community, community: Community,
save: boolean = true store: boolean = true
): Promise<FederatedCommunity> { ): Promise<FederatedCommunity> {
const federatedCommunity = new FederatedCommunity() const federatedCommunity = new FederatedCommunity()
federatedCommunity.apiVersion = apiVersion federatedCommunity.apiVersion = apiVersion
@ -38,5 +52,5 @@ export async function createVerifiedFederatedCommunity(
federatedCommunity.publicKey = community.publicKey federatedCommunity.publicKey = community.publicKey
federatedCommunity.community = community federatedCommunity.community = community
federatedCommunity.verifiedAt = new Date(Date.now() - verifiedBeforeMs) federatedCommunity.verifiedAt = new Date(Date.now() - verifiedBeforeMs)
return save ? await federatedCommunity.save() : federatedCommunity return store ? await federatedCommunity.save() : federatedCommunity
} }

View File

@ -1,19 +1,31 @@
import { CONFIG } from '@/config'
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
import { EncryptedTransferArgs, interpretEncryptedTransferArgs } from 'core' import { CommunityHandshakeStateLogic, EncryptedTransferArgs, interpretEncryptedTransferArgs, splitUrlInEndPointAndApiVersion } from 'core'
import { import {
CommunityLoggingView, CommunityHandshakeStateLoggingView,
Community as DbCommunity, CommunityHandshakeState as DbCommunityHandshakeState,
CommunityHandshakeStateType,
FederatedCommunity as DbFedCommunity, FederatedCommunity as DbFedCommunity,
FederatedCommunityLoggingView,
getHomeCommunity, getHomeCommunity,
findPendingCommunityHandshakeOrFailByOneTimeCode,
getCommunityByPublicKeyOrFail,
} from 'database' } from 'database'
import { getLogger } from 'log4js' import { getLogger } from 'log4js'
import { AuthenticationJwtPayloadType, AuthenticationResponseJwtPayloadType, encryptAndSign, OpenConnectionCallbackJwtPayloadType, OpenConnectionJwtPayloadType, uint32Schema, uuidv4Schema } from 'shared' import {
AuthenticationJwtPayloadType,
AuthenticationResponseJwtPayloadType,
Ed25519PublicKey,
encryptAndSign,
OpenConnectionCallbackJwtPayloadType,
OpenConnectionJwtPayloadType,
uint32Schema,
uuidv4Schema
} from 'shared'
import { Arg, Mutation, Resolver } from 'type-graphql' import { Arg, Mutation, Resolver } from 'type-graphql'
import { startAuthentication, startOpenConnectionCallback } from '../util/authenticateCommunity' 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() @Resolver()
export class AuthenticationResolver { export class AuthenticationResolver {
@ -24,45 +36,38 @@ export class AuthenticationResolver {
): Promise<boolean> { ): Promise<boolean> {
const methodLogger = createLogger('openConnection') const methodLogger = createLogger('openConnection')
methodLogger.addContext('handshakeID', args.handshakeID) 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 { try {
const openConnectionJwtPayload = await interpretEncryptedTransferArgs(args) as OpenConnectionJwtPayloadType const openConnectionJwtPayload = await interpretEncryptedTransferArgs(args) as OpenConnectionJwtPayloadType
methodLogger.debug('openConnectionJwtPayload', openConnectionJwtPayload) methodLogger.debug(`openConnectionJwtPayload url: ${openConnectionJwtPayload.url}`)
if (!openConnectionJwtPayload) { if (!openConnectionJwtPayload) {
const errmsg = `invalid OpenConnection payload of requesting community with publicKey` + args.publicKey throw new Error(`invalid OpenConnection payload of requesting community with publicKey ${argsPublicKey.asHex()}`)
methodLogger.error(errmsg)
// no infos to the caller
return true
} }
if (openConnectionJwtPayload.tokentype !== OpenConnectionJwtPayloadType.OPEN_CONNECTION_TYPE) { if (openConnectionJwtPayload.tokentype !== OpenConnectionJwtPayloadType.OPEN_CONNECTION_TYPE) {
const errmsg = `invalid tokentype of community with publicKey` + args.publicKey throw new Error(`invalid tokentype: ${openConnectionJwtPayload.tokentype} of community with publicKey ${argsPublicKey.asHex()}`)
methodLogger.error(errmsg)
// no infos to the caller
return true
} }
if (!openConnectionJwtPayload.url) { if (!openConnectionJwtPayload.url) {
const errmsg = `invalid url of community with publicKey` + args.publicKey throw new Error(`invalid url of community with publicKey ${argsPublicKey.asHex()}`)
methodLogger.error(errmsg)
// no infos to the caller
return true
} }
methodLogger.debug(`vor DbFedCommunity.findOneByOrFail()...`, { publicKey: args.publicKey }) // methodLogger.debug(`before DbFedCommunity.findOneByOrFail()...`, { publicKey: argsPublicKey.asHex() })
const fedComA = await DbFedCommunity.findOneByOrFail({ publicKey: Buffer.from(args.publicKey, 'hex') }) const fedComA = await DbFedCommunity.findOneByOrFail({ publicKey: argsPublicKey.asBuffer() })
methodLogger.debug(`nach DbFedCommunity.findOneByOrFail()...`, fedComA) // methodLogger.debug(`after DbFedCommunity.findOneByOrFail()...`, new FederatedCommunityLoggingView(fedComA))
methodLogger.debug('fedComA', new FederatedCommunityLoggingView(fedComA))
if (!openConnectionJwtPayload.url.startsWith(fedComA.endPoint)) { if (!openConnectionJwtPayload.url.startsWith(fedComA.endPoint)) {
const errmsg = `invalid url of community with publicKey` + args.publicKey throw new Error(`invalid url of community with publicKey ${argsPublicKey.asHex()}`)
methodLogger.error(errmsg) }
// no infos to the caller if (fedComA.apiVersion !== apiVersion) {
return true throw new Error(`invalid apiVersion: ${fedComA.apiVersion} of community with publicKey ${argsPublicKey.asHex()}`)
} }
// no await to respond immediately and invoke callback-request asynchronously // no await to respond immediately and invoke callback-request asynchronously
void startOpenConnectionCallback(args.handshakeID, args.publicKey, 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...') methodLogger.debug('openConnection() successfully initiated callback and returns true immediately...')
return true return true
} catch (err) { } catch (err) {
methodLogger.error('invalid jwt token:', err) methodLogger.error('invalid jwt token:', err)
// no infos to the caller
return true return true
} }
} }
@ -74,37 +79,29 @@ export class AuthenticationResolver {
): Promise<boolean> { ): Promise<boolean> {
const methodLogger = createLogger('openConnectionCallback') const methodLogger = createLogger('openConnectionCallback')
methodLogger.addContext('handshakeID', args.handshakeID) methodLogger.addContext('handshakeID', args.handshakeID)
methodLogger.debug(`openConnectionCallback() via apiVersion=1_0 ...`, args) methodLogger.debug(`start via apiVersion=${apiVersion}, public key: ${args.publicKey}`)
try { try {
// decrypt args.url with homeCom.privateJwtKey and verify signing with callbackFedCom.publicKey // decrypt args.url with homeCom.privateJwtKey and verify signing with callbackFedCom.publicKey
const openConnectionCallbackJwtPayload = await interpretEncryptedTransferArgs(args) as OpenConnectionCallbackJwtPayloadType const openConnectionCallbackJwtPayload = await interpretEncryptedTransferArgs(args) as OpenConnectionCallbackJwtPayloadType
if (!openConnectionCallbackJwtPayload) { if (!openConnectionCallbackJwtPayload) {
const errmsg = `invalid OpenConnectionCallback payload of requesting community with publicKey` + args.publicKey throw new Error(`invalid OpenConnectionCallback payload of requesting community with publicKey ${args.publicKey}`)
methodLogger.error(errmsg)
// no infos to the caller
return true
} }
const { endPoint, apiVersion } = splitUrlInEndPointAndApiVersion(openConnectionCallbackJwtPayload.url)
const endPoint = openConnectionCallbackJwtPayload.url.slice(0, openConnectionCallbackJwtPayload.url.lastIndexOf('/') + 1) // methodLogger.debug(`search fedComB per:`, endPoint, apiVersion)
const apiVersion = openConnectionCallbackJwtPayload.url.slice(openConnectionCallbackJwtPayload.url.lastIndexOf('/') + 1, openConnectionCallbackJwtPayload.url.length)
methodLogger.debug(`search fedComB per:`, endPoint, apiVersion)
const fedComB = await DbFedCommunity.findOneBy({ endPoint, apiVersion }) const fedComB = await DbFedCommunity.findOneBy({ endPoint, apiVersion })
if (!fedComB) { if (!fedComB) {
const errmsg = `unknown callback community with url` + openConnectionCallbackJwtPayload.url throw new Error(`unknown callback community for ${endPoint}${apiVersion}`)
methodLogger.error(errmsg)
// no infos to the caller
return true
} }
methodLogger.debug( methodLogger.debug(
`found fedComB and start authentication:`, `found fedComB and start authentication: ${fedComB.endPoint}${fedComB.apiVersion}`,
new FederatedCommunityLoggingView(fedComB),
) )
// no await to respond immediately and invoke authenticate-request asynchronously // no await to respond immediately and invoke authenticate-request asynchronously
void startAuthentication(args.handshakeID, openConnectionCallbackJwtPayload.oneTimeCode, fedComB) 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 return true
} catch (err) { } catch (err) {
methodLogger.error('invalid jwt token:', err) methodLogger.error('invalid jwt token:', err)
// no infos to the caller
return true return true
} }
} }
@ -116,51 +113,80 @@ export class AuthenticationResolver {
): Promise<string | null> { ): Promise<string | null> {
const methodLogger = createLogger('authenticate') const methodLogger = createLogger('authenticate')
methodLogger.addContext('handshakeID', args.handshakeID) 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 { try {
const authArgs = await interpretEncryptedTransferArgs(args) as AuthenticationJwtPayloadType const authArgs = await interpretEncryptedTransferArgs(args) as AuthenticationJwtPayloadType
// methodLogger.debug(`interpreted authentication payload...authArgs:`, authArgs)
if (!authArgs) { if (!authArgs) {
const errmsg = `invalid authentication payload of requesting community with publicKey` + args.publicKey methodLogger.debug(`interpretEncryptedTransferArgs was called with`, args)
methodLogger.error(errmsg) throw new Error(`invalid authentication payload of requesting community with publicKey ${argsPublicKey.asHex()}`)
// no infos to the caller
return null
} }
if (!uint32Schema.safeParse(Number(authArgs.oneTimeCode)).success) { const validOneTimeCode = uint32Schema.safeParse(Number(authArgs.oneTimeCode))
const errmsg = `invalid oneTimeCode: ${authArgs.oneTimeCode} for community with publicKey ${authArgs.publicKey}, expect uint32` if (!validOneTimeCode.success) {
methodLogger.error(errmsg) throw new Error(
// no infos to the caller `invalid oneTimeCode: ${authArgs.oneTimeCode} for community with publicKey ${argsPublicKey.asHex()}, expect uint32`
return null )
} }
const authCom = await DbCommunity.findOneByOrFail({ communityUuid: authArgs.oneTimeCode })
state = await findPendingCommunityHandshakeOrFailByOneTimeCode(validOneTimeCode.data)
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
await state.save()
methodLogger.debug('[SUCCESS] community handshake state updated')
// methodLogger.debug(`search community per oneTimeCode:`, authArgs.oneTimeCode)
const authCom = await getCommunityByPublicKeyOrFail(argsPublicKey)
if (authCom) { if (authCom) {
methodLogger.debug('found authCom:', new CommunityLoggingView(authCom)) methodLogger.debug(`found authCom ${authCom.name}`)
if (authCom.publicKey !== authArgs.publicKey) { const authComPublicKey = new Ed25519PublicKey(authCom.publicKey)
const errmsg = `corrupt authentication call detected, oneTimeCode: ${authArgs.oneTimeCode} doesn't belong to caller: ${authArgs.publicKey}` // methodLogger.debug('authCom.publicKey', authComPublicKey.asHex())
methodLogger.error(errmsg) // methodLogger.debug('args.publicKey', argsPublicKey.asHex())
// no infos to the caller if (!authComPublicKey.isSame(argsPublicKey)) {
return null throw new Error(
`corrupt authentication call detected, oneTimeCode: ${authArgs.oneTimeCode} doesn't belong to caller: ${argsPublicKey.asHex()}`
)
} }
const communityUuid = uuidv4Schema.safeParse(authArgs.uuid) const communityUuid = uuidv4Schema.safeParse(authArgs.uuid)
if (!communityUuid.success) { if (!communityUuid.success) {
const errmsg = `invalid uuid: ${authArgs.uuid} for community with publicKey ${authArgs.publicKey}` throw new Error(
methodLogger.error(errmsg) `invalid uuid: ${authArgs.uuid} for community with publicKey ${authComPublicKey.asHex()}`
// no infos to the caller )
return null
} }
authCom.communityUuid = communityUuid.data authCom.communityUuid = communityUuid.data
authCom.authenticatedAt = new Date() authCom.authenticatedAt = new Date()
await DbCommunity.save(authCom) await authCom.save()
methodLogger.debug('store authCom.uuid successfully:', new CommunityLoggingView(authCom)) methodLogger.debug(`update authCom.uuid successfully with ${authCom.communityUuid} at ${authCom.authenticatedAt}`)
const homeComB = await getHomeCommunity() const homeComB = await getHomeCommunity()
if (homeComB?.communityUuid) { if (homeComB?.communityUuid) {
const responseArgs = new AuthenticationResponseJwtPayloadType(args.handshakeID,homeComB.communityUuid) const responseArgs = new AuthenticationResponseJwtPayloadType(args.handshakeID,homeComB.communityUuid)
const responseJwt = await encryptAndSign(responseArgs, homeComB.privateJwtKey!, authCom.publicJwtKey!) const responseJwt = await encryptAndSign(responseArgs, homeComB.privateJwtKey!, authCom.publicJwtKey!)
return responseJwt return responseJwt
} }
} else {
throw new Error(`community with publicKey ${argsPublicKey.asHex()} not found`)
} }
return null return null
} catch (err) { } catch (err) {
methodLogger.error('invalid jwt token:', err) if (state) {
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`, err)
// no infos to the caller
return null return null
} }
} }

View File

@ -1,85 +1,118 @@
import { EncryptedTransferArgs } from 'core' import { CommunityHandshakeStateLogic, EncryptedTransferArgs, ensureUrlEndsWithSlash } from 'core'
import { import {
CommunityLoggingView, CommunityHandshakeStateLoggingView,
Community as DbCommunity, Community as DbCommunity,
FederatedCommunity as DbFedCommunity, FederatedCommunity as DbFedCommunity,
FederatedCommunityLoggingView, findPendingCommunityHandshake,
getCommunityByPublicKeyOrFail,
getHomeCommunity, getHomeCommunity,
getHomeCommunityWithFederatedCommunityOrFail,
} from 'database' } from 'database'
import { getLogger } from 'log4js' import { getLogger } from 'log4js'
import { validate as validateUUID, version as versionUUID } from 'uuid'
import { AuthenticationClientFactory } from '@/client/AuthenticationClientFactory' import { AuthenticationClientFactory } from '@/client/AuthenticationClientFactory'
import { randombytes_random } from 'sodium-native' import { randombytes_random } from 'sodium-native'
import { AuthenticationClient as V1_0_AuthenticationClient } from '@/client/1_0/AuthenticationClient' import { AuthenticationClient as V1_0_AuthenticationClient } from '@/client/1_0/AuthenticationClient'
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
import { AuthenticationJwtPayloadType, AuthenticationResponseJwtPayloadType, encryptAndSign, OpenConnectionCallbackJwtPayloadType, uuidv4Schema, verifyAndDecrypt } from 'shared' import {
AuthenticationJwtPayloadType,
AuthenticationResponseJwtPayloadType,
Ed25519PublicKey,
encryptAndSign,
OpenConnectionCallbackJwtPayloadType,
uuidv4Schema,
verifyAndDecrypt
} from 'shared'
import { CommunityHandshakeState as DbCommunityHandshakeState, CommunityHandshakeStateType } from 'database'
import { getFederatedCommunityWithApiOrFail } from 'core'
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}`)
export async function startOpenConnectionCallback( export async function startOpenConnectionCallback(
handshakeID: string, handshakeID: string,
publicKey: string, publicKey: Ed25519PublicKey,
api: string, fedComA: DbFedCommunity,
): Promise<void> { ): Promise<void> {
const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.util.authenticateCommunity.startOpenConnectionCallback`) const methodLogger = createLogger('startOpenConnectionCallback')
methodLogger.addContext('handshakeID', handshakeID) methodLogger.addContext('handshakeID', handshakeID)
methodLogger.debug(`Authentication: startOpenConnectionCallback() with:`, { methodLogger.debug(`start`)
publicKey, const api = fedComA.apiVersion
})
let state: DbCommunityHandshakeState | null = null
try { try {
const homeComB = await getHomeCommunity() const pendingState = await findPendingCommunityHandshake(publicKey, api, CommunityHandshakeStateType.START_OPEN_CONNECTION_CALLBACK)
const homeFedComB = await DbFedCommunity.findOneByOrFail({ if (pendingState) {
foreign: false, const stateLogic = new CommunityHandshakeStateLogic(pendingState)
apiVersion: api, // retry on timeout or failure
}) if (!(await stateLogic.isTimeoutUpdate())) {
const comA = await DbCommunity.findOneByOrFail({ publicKey: Buffer.from(publicKey, 'hex') }) // authentication with community and api version is still in progress and it is not timeout yet
const fedComA = await DbFedCommunity.findOneByOrFail({ methodLogger.debug('existingState, so we exit here', new CommunityHandshakeStateLoggingView(pendingState))
foreign: true, return
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) {
throw new Error('Community UUID is already a valid UUID')
} }
// load comA and comB parallel
// load with joined federated community of given api version
const [homeComB, comA] = await Promise.all([
getHomeCommunityWithFederatedCommunityOrFail(api),
getCommunityByPublicKeyOrFail(publicKey),
])
// get federated communities with correct api version
// 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 // TODO: make sure it is unique
const oneTimeCode = randombytes_random().toString() const oneTimeCode = randombytes_random()
comA.communityUuid = oneTimeCode const oneTimeCodeString = oneTimeCode.toString()
await DbCommunity.save(comA)
methodLogger.debug( // Create new community handshake state
`Authentication: stored oneTimeCode in requestedCom:`, state = new DbCommunityHandshakeState()
new CommunityLoggingView(comA), state.publicKey = publicKey.asBuffer()
) state.apiVersion = api
state.status = CommunityHandshakeStateType.START_OPEN_CONNECTION_CALLBACK
state.handshakeId = parseInt(handshakeID)
state.oneTimeCode = oneTimeCode
state = await state.save()
methodLogger.debug('[START_OPEN_CONNECTION_CALLBACK] community handshake state created')
const client = AuthenticationClientFactory.getInstance(fedComA) const client = AuthenticationClientFactory.getInstance(fedComA)
if (client instanceof V1_0_AuthenticationClient) { if (client instanceof V1_0_AuthenticationClient) {
const url = homeFedComB.endPoint.endsWith('/') const url = ensureUrlEndsWithSlash(homeFedComB.endPoint) + homeFedComB.apiVersion
? homeFedComB.endPoint + homeFedComB.apiVersion
: 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) // methodLogger.debug(`Authentication: start openConnectionCallback with args:`, callbackArgs)
// encrypt callbackArgs with requestedCom.publicJwtKey and sign it with homeCom.privateJwtKey // 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() const args = new EncryptedTransferArgs()
args.publicKey = homeComB!.publicKey.toString('hex') args.publicKey = new Ed25519PublicKey(homeComB.publicKey).asHex()
args.jwt = jwt args.jwt = jwt
args.handshakeID = handshakeID args.handshakeID = handshakeID
methodLogger.debug(`invoke openConnectionCallback(), oneTimeCode: ${oneTimeCodeString}`)
const result = await client.openConnectionCallback(args) const result = await client.openConnectionCallback(args)
if (result) { if (result) {
methodLogger.debug('startOpenConnectionCallback() successful:', jwt) methodLogger.debug(`startOpenConnectionCallback() successful`)
} else { } else {
methodLogger.error('startOpenConnectionCallback() failed:', jwt) methodLogger.debug(`jwt: ${jwt}`)
const errorString = 'startOpenConnectionCallback() failed'
methodLogger.error(errorString)
state.status = CommunityHandshakeStateType.FAILED
state.lastError = errorString
state = await state.save()
} }
} }
} catch (err) { } catch (err) {
methodLogger.error('error in startOpenConnectionCallback:', 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)
}
}
} }
methodLogger.removeContext('handshakeID')
} }
export async function startAuthentication( export async function startAuthentication(
@ -87,21 +120,31 @@ export async function startAuthentication(
oneTimeCode: string, oneTimeCode: string,
fedComB: DbFedCommunity, fedComB: DbFedCommunity,
): Promise<void> { ): Promise<void> {
const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.util.authenticateCommunity.startAuthentication`) const methodLogger = createLogger('startAuthentication')
methodLogger.addContext('handshakeID', handshakeID) methodLogger.addContext('handshakeID', handshakeID)
methodLogger.debug(`startAuthentication()...`, { methodLogger.debug(`startAuthentication()... oneTimeCode: ${oneTimeCode}`)
oneTimeCode, let state: DbCommunityHandshakeState | null = null
fedComB: new FederatedCommunityLoggingView(fedComB),
})
try { try {
const fedComBPublicKey = new Ed25519PublicKey(fedComB.publicKey)
const homeComA = await getHomeCommunity() const homeComA = await getHomeCommunity()
const comB = await DbCommunity.findOneByOrFail({ const comB = await DbCommunity.findOneByOrFail({
foreign: true, foreign: true,
publicKey: fedComB.publicKey, publicKey: fedComBPublicKey.asBuffer(),
}) })
if (!comB.publicJwtKey) { if (!comB.publicJwtKey) {
throw new Error('Public JWT key still not exist for foreign community') throw new Error('Public JWT key still not exist for foreign community')
} }
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())) {
methodLogger.debug('invalid state', new CommunityHandshakeStateLoggingView(state))
throw new Error('No valid pending community handshake found')
}
state.status = CommunityHandshakeStateType.START_AUTHENTICATION
await state.save()
const client = AuthenticationClientFactory.getInstance(fedComB) const client = AuthenticationClientFactory.getInstance(fedComB)
@ -110,41 +153,55 @@ export async function startAuthentication(
// encrypt authenticationArgs.uuid with fedComB.publicJwtKey and sign it with homeCom.privateJwtKey // encrypt authenticationArgs.uuid with fedComB.publicJwtKey and sign it with homeCom.privateJwtKey
const jwt = await encryptAndSign(authenticationArgs, homeComA!.privateJwtKey!, comB.publicJwtKey!) const jwt = await encryptAndSign(authenticationArgs, homeComA!.privateJwtKey!, comB.publicJwtKey!)
const args = new EncryptedTransferArgs() const args = new EncryptedTransferArgs()
args.publicKey = homeComA!.publicKey.toString('hex') args.publicKey = new Ed25519PublicKey(homeComA!.publicKey).asHex()
args.jwt = jwt args.jwt = jwt
args.handshakeID = handshakeID args.handshakeID = handshakeID
methodLogger.debug(`invoke authenticate() with:`, args) methodLogger.debug(`invoke authenticate(), publicKey: ${args.publicKey}`)
const responseJwt = await client.authenticate(args) const responseJwt = await client.authenticate(args)
methodLogger.debug(`response of authenticate():`, responseJwt) // methodLogger.debug(`response of authenticate():`, responseJwt)
if (responseJwt !== null) { if (responseJwt !== null) {
const payload = await verifyAndDecrypt(handshakeID, responseJwt, homeComA!.privateJwtKey!, comB.publicJwtKey!) as AuthenticationResponseJwtPayloadType const payload = await verifyAndDecrypt(handshakeID, responseJwt, homeComA!.privateJwtKey!, comB.publicJwtKey!) as AuthenticationResponseJwtPayloadType
methodLogger.debug( /*methodLogger.debug(
`received payload from authenticate ComB:`, `received payload from authenticate ComB:`,
payload, payload,
new FederatedCommunityLoggingView(fedComB), new FederatedCommunityLoggingView(fedComB),
) )*/
if (payload.tokentype !== AuthenticationResponseJwtPayloadType.AUTHENTICATION_RESPONSE_TYPE) { if (payload.tokentype !== AuthenticationResponseJwtPayloadType.AUTHENTICATION_RESPONSE_TYPE) {
const errmsg = `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()}`)
methodLogger.error(errmsg)
methodLogger.removeContext('handshakeID')
throw new Error(errmsg)
} }
if (!payload.uuid || !validateUUID(payload.uuid) || versionUUID(payload.uuid) !== 4) { const parsedUuidv4 = uuidv4Schema.safeParse(payload.uuid)
const errmsg = `Invalid uuid in authenticate-response of community with publicKey` + comB.publicKey if (!parsedUuidv4.success) {
methodLogger.error(errmsg) throw new Error(`Invalid uuid in authenticate-response of community with publicKey ${fedComBPublicKey.asHex()}`)
methodLogger.removeContext('handshakeID')
throw new Error(errmsg)
} }
comB.communityUuid = payload.uuid methodLogger.debug('received uuid from authenticate ComB:', parsedUuidv4.data)
comB.communityUuid = parsedUuidv4.data
comB.authenticatedAt = new Date() comB.authenticatedAt = new Date()
await DbCommunity.save(comB) await DbCommunity.save(comB)
methodLogger.debug('Community Authentication successful:', new CommunityLoggingView(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`)
} else { } else {
state.status = CommunityHandshakeStateType.FAILED
state.lastError = 'Community Authentication failed, empty response'
await state.save()
methodLogger.error('Community Authentication failed:', authenticationArgs) methodLogger.error('Community Authentication failed:', authenticationArgs)
} }
} }
} catch (err) { } catch (err) {
methodLogger.error('error in startAuthentication:', err) methodLogger.error('error in startAuthentication:', err)
if (state) {
try {
state.status = CommunityHandshakeStateType.FAILED
state.lastError = String(err)
await state.save()
} catch(e) {
methodLogger.error('error on saving CommunityHandshakeState', e)
}
}
} }
methodLogger.removeContext('handshakeID')
} }

View File

@ -6,8 +6,10 @@ import { getLogger } from 'log4js'
// config // config
import { CONFIG } from './config' import { CONFIG } from './config'
import { LOG4JS_BASE_CATEGORY_NAME } from './config/const' import { LOG4JS_BASE_CATEGORY_NAME } from './config/const'
import { onShutdown, printServerCrashAsciiArt, ShutdownReason } from 'shared'
async function main() { async function main() {
const startTime = new Date()
// init logger // init logger
const log4jsConfigFileName = CONFIG.LOG4JS_CONFIG_PLACEHOLDER.replace('%v', CONFIG.FEDERATION_API) const log4jsConfigFileName = CONFIG.LOG4JS_CONFIG_PLACEHOLDER.replace('%v', CONFIG.FEDERATION_API)
initLogger( initLogger(
@ -27,6 +29,16 @@ async function main() {
`GraphIQL available at ${CONFIG.FEDERATION_COMMUNITY_URL}/api/${CONFIG.FEDERATION_API}`, `GraphIQL available at ${CONFIG.FEDERATION_COMMUNITY_URL}/api/${CONFIG.FEDERATION_API}`,
) )
} }
onShutdown(async (reason, error) => {
if (ShutdownReason.SIGINT === reason || ShutdownReason.SIGTERM === reason) {
logger.info(`graceful shutdown: ${reason}`)
} else {
const endTime = new Date()
const duration = endTime.getTime() - startTime.getTime()
printServerCrashAsciiArt('Server Crash', `reason: ${reason}`, `server was ${duration}ms online`)
logger.error(error)
}
})
}) })
} }

View File

@ -37,6 +37,7 @@
"esbuild": "^0.25.2", "esbuild": "^0.25.2",
"jose": "^4.14.4", "jose": "^4.14.4",
"log4js": "^6.9.1", "log4js": "^6.9.1",
"yoctocolors-cjs": "^2.1.2",
"zod": "^3.25.61" "zod": "^3.25.61"
}, },
"engines": { "engines": {

View File

@ -1,4 +1,6 @@
export const DECAY_START_TIME = new Date('2021-05-13T17:46:31Z') export const DECAY_START_TIME = new Date('2021-05-13T17:46:31Z')
export const SECONDS_PER_YEAR_GREGORIAN_CALENDER = 31556952.0 export const SECONDS_PER_YEAR_GREGORIAN_CALENDER = 31556952.0
export const LOG4JS_BASE_CATEGORY_NAME = 'shared' export const LOG4JS_BASE_CATEGORY_NAME = 'shared'
export const REDEEM_JWT_TOKEN_EXPIRATION = '10m' export const REDEEM_JWT_TOKEN_EXPIRATION = '10m'
// 10 minutes
export const FEDERATION_AUTHENTICATION_TIMEOUT_MS = 60 * 1000 * 10

View File

@ -0,0 +1,51 @@
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)
*/
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 {
this.buf = Buffer.from('')
this.hex = ''
}
}
asBuffer(): Buffer {
return this.buf
}
asHex(): string {
return this.hex
}
isSame(other: BinaryData): boolean {
if (other === undefined || !(other instanceof BinaryData)) {
logging.error('other is invalid', other)
return false
}
return this.buf.compare(other.asBuffer()) === 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')
}
}
}

View File

@ -1 +1,3 @@
export * from './updateField' export * from './updateField'
export * from './BinaryData'
export * from './onShutdown'

View File

@ -0,0 +1,51 @@
import { Logger } from 'log4js'
import colors from 'yoctocolors-cjs'
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, error?: Error | any) => Promise<void>) {
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) => {
await shutdownHandler(ShutdownReason.UNCAUGHT_EXCEPTION, err)
process.exit(1)
})
process.on('unhandledRejection', async (err) => {
await shutdownHandler(ShutdownReason.UNCAUGHT_REJECTION, err)
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)
})
}
}
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(''))
}

View File

@ -1,5 +1,6 @@
export * from './schema' export * from './schema'
export * from './enum' export * from './enum'
export * from './const'
export * from './helper' export * from './helper'
export * from './logic/decay' export * from './logic/decay'
export * from './jwt/JWT' export * from './jwt/JWT'

View File

@ -43,11 +43,9 @@ export const verify = async (handshakeID: string, token: string, publicKey: stri
}) })
payload.handshakeID = handshakeID payload.handshakeID = handshakeID
methodLogger.debug('verify after jwtVerify... payload=', payload) methodLogger.debug('verify after jwtVerify... payload=', payload)
methodLogger.removeContext('handshakeID')
return payload as JwtPayloadType return payload as JwtPayloadType
} catch (err) { } catch (err) {
methodLogger.error('verify after jwtVerify... error=', err) methodLogger.error('verify after jwtVerify... error=', err)
methodLogger.removeContext('handshakeID')
return null return null
} }
} }
@ -74,11 +72,9 @@ export const encode = async (payload: JwtPayloadType, privatekey: string): Promi
.setExpirationTime(payload.expiration) .setExpirationTime(payload.expiration)
.sign(secret) .sign(secret)
methodLogger.debug('encode... token=', token) methodLogger.debug('encode... token=', token)
methodLogger.removeContext('handshakeID')
return token return token
} catch (e) { } catch (e) {
methodLogger.error('Failed to sign JWT:', e) methodLogger.error('Failed to sign JWT:', e)
methodLogger.removeContext('handshakeID')
throw e throw e
} }
} }
@ -111,11 +107,9 @@ export const encrypt = async (payload: JwtPayloadType, publicKey: string): Promi
.setProtectedHeader({ alg: 'RSA-OAEP-256', enc: 'A256GCM' }) .setProtectedHeader({ alg: 'RSA-OAEP-256', enc: 'A256GCM' })
.encrypt(recipientKey) .encrypt(recipientKey)
methodLogger.debug('encrypt... jwe=', jwe) methodLogger.debug('encrypt... jwe=', jwe)
methodLogger.removeContext('handshakeID')
return jwe.toString() return jwe.toString()
} catch (e) { } catch (e) {
methodLogger.error('Failed to encrypt JWT:', e) methodLogger.error('Failed to encrypt JWT:', e)
methodLogger.removeContext('handshakeID')
throw e throw e
} }
} }
@ -131,11 +125,9 @@ export const decrypt = async(handshakeID: string, jwe: string, privateKey: strin
await compactDecrypt(jwe, decryptKey) await compactDecrypt(jwe, decryptKey)
methodLogger.debug('decrypt... plaintext=', plaintext) methodLogger.debug('decrypt... plaintext=', plaintext)
methodLogger.debug('decrypt... protectedHeader=', protectedHeader) methodLogger.debug('decrypt... protectedHeader=', protectedHeader)
methodLogger.removeContext('handshakeID')
return new TextDecoder().decode(plaintext) return new TextDecoder().decode(plaintext)
} catch (e) { } catch (e) {
methodLogger.error('Failed to decrypt JWT:', e) methodLogger.error('Failed to decrypt JWT:', e)
methodLogger.removeContext('handshakeID')
throw e throw e
} }
} }
@ -147,7 +139,6 @@ export const encryptAndSign = async (payload: JwtPayloadType, privateKey: string
methodLogger.debug('encryptAndSign... jwe=', jwe) methodLogger.debug('encryptAndSign... jwe=', jwe)
const jws = await encode(new EncryptedJWEJwtPayloadType(payload.handshakeID, jwe), privateKey) const jws = await encode(new EncryptedJWEJwtPayloadType(payload.handshakeID, jwe), privateKey)
methodLogger.debug('encryptAndSign... jws=', jws) methodLogger.debug('encryptAndSign... jws=', jws)
methodLogger.removeContext('handshakeID')
return jws return jws
} }
@ -171,6 +162,5 @@ export const verifyAndDecrypt = async (handshakeID: string, token: string, priva
methodLogger.debug('verifyAndDecrypt... jwe=', jwe) methodLogger.debug('verifyAndDecrypt... jwe=', jwe)
const payload = await decrypt(handshakeID, jwe as string, privateKey) const payload = await decrypt(handshakeID, jwe as string, privateKey)
methodLogger.debug('verifyAndDecrypt... payload=', payload) methodLogger.debug('verifyAndDecrypt... payload=', payload)
methodLogger.removeContext('handshakeID')
return JSON.parse(payload) as JwtPayloadType return JSON.parse(payload) as JwtPayloadType
} }

View File

@ -4,4 +4,4 @@ import { validate, version } from 'uuid'
export const uuidv4Schema = string().refine((val: string) => validate(val) && version(val) === 4, 'Invalid uuid') export const uuidv4Schema = string().refine((val: string) => validate(val) && version(val) === 4, 'Invalid uuid')
export const emailSchema = string().email() export const emailSchema = string().email()
export const urlSchema = string().url() export const urlSchema = string().url()
export const uint32Schema = number().positive().lte(4294967295) export const uint32Schema = number().positive().lte(4294967295)

View File

@ -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)
})
})

View File

@ -0,0 +1,7 @@
import { object, date, array, string } from 'zod'
import { uuidv4Schema } from './base.schema'
export const communityAuthenticatedSchema = object({
communityUuid: uuidv4Schema,
authenticatedAt: date(),
})

View File

@ -1,2 +1,3 @@
export * from './user.schema' export * from './user.schema'
export * from './base.schema' export * from './base.schema'
export * from './community.schema'