diff --git a/packages/backend/src/core/AccountUpdateService.ts b/packages/backend/src/core/AccountUpdateService.ts index fd790913b4..208131ed6e 100644 --- a/packages/backend/src/core/AccountUpdateService.ts +++ b/packages/backend/src/core/AccountUpdateService.ts @@ -13,6 +13,7 @@ import { RelayService } from '@/core/RelayService.js'; import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; +import type { PrivateKey } from './activitypub/type.js'; @Injectable() export class AccountUpdateService implements OnModuleInit { @@ -39,7 +40,7 @@ export class AccountUpdateService implements OnModuleInit { * @param userId ユーザーID * @param isKeyUpdation Ed25519キーの作成など公開鍵のアップデートによる呼び出しか? trueにするとメインキーを使うようになる */ - public async publishToFollowers(userId: MiUser['id'], isKeyUpdation: boolean = false) { + public async publishToFollowers(userId: MiUser['id'], deliverKey?: PrivateKey) { const user = await this.usersRepository.findOneBy({ id: userId }); if (user == null) throw new Error('user not found'); @@ -47,8 +48,8 @@ export class AccountUpdateService implements OnModuleInit { if (this.userEntityService.isLocalUser(user)) { const content = this.apRendererService.addContext(this.apRendererService.renderUpdate(await this.apRendererService.renderPerson(user), user)); await Promise.allSettled([ - this.apDeliverManagerService.deliverToFollowers(user, content, isKeyUpdation), - this.relayService.deliverToRelays(user, content, isKeyUpdation), + this.apDeliverManagerService.deliverToFollowers(user, content, deliverKey), + this.relayService.deliverToRelays(user, content, deliverKey), ]); } } diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index cb5dca2d39..b73d0de4d0 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -5,7 +5,7 @@ import { randomUUID } from 'node:crypto'; import { Inject, Injectable } from '@nestjs/common'; -import type { IActivity } from '@/core/activitypub/type.js'; +import type { IActivity, PrivateKey } from '@/core/activitypub/type.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiWebhook, webhookEventTypes } from '@/models/Webhook.js'; import type { Config } from '@/config.js'; @@ -70,7 +70,7 @@ export class QueueService { } @bindThis - public async deliver(user: ThinUser, content: IActivity | null, to: string | null, isSharedInbox: boolean, forceMainKey?: boolean) { + public async deliver(user: ThinUser, content: IActivity | null, to: string | null, isSharedInbox: boolean, privateKey?: PrivateKey) { if (content == null) return null; if (to == null) return null; @@ -84,7 +84,7 @@ export class QueueService { digest: await genRFC3230DigestHeader(contentBody, 'SHA-256'), to, isSharedInbox, - forceMainKey, + privateKey, }; return this.deliverQueue.add(to, data, { @@ -106,7 +106,7 @@ export class QueueService { * @returns void */ @bindThis - public async deliverMany(user: ThinUser, content: IActivity | null, inboxes: Map, forceMainKey?: boolean) { + public async deliverMany(user: ThinUser, content: IActivity | null, inboxes: Map, privateKey?: PrivateKey) { if (content == null) return null; const contentBody = JSON.stringify(content); @@ -126,7 +126,7 @@ export class QueueService { content: contentBody, to: d[0], isSharedInbox: d[1], - forceMainKey, + privateKey, } as DeliverJobData, opts, }))); diff --git a/packages/backend/src/core/RelayService.ts b/packages/backend/src/core/RelayService.ts index 52bf5d8e6a..c5099a5425 100644 --- a/packages/backend/src/core/RelayService.ts +++ b/packages/backend/src/core/RelayService.ts @@ -16,6 +16,7 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { DI } from '@/di-symbols.js'; import { deepClone } from '@/misc/clone.js'; import { bindThis } from '@/decorators.js'; +import { PrivateKey } from './activitypub/type.js'; const ACTOR_USERNAME = 'relay.actor' as const; @@ -111,7 +112,7 @@ export class RelayService { } @bindThis - public async deliverToRelays(user: { id: MiUser['id']; host: null; }, activity: any, forceMainKey?: boolean): Promise { + public async deliverToRelays(user: { id: MiUser['id']; host: null; }, activity: any, privateKey?: PrivateKey): Promise { if (activity == null) return; const relays = await this.relaysCache.fetch(() => this.relaysRepository.findBy({ @@ -125,7 +126,7 @@ export class RelayService { const signed = await this.apRendererService.attachLdSignature(copy, user); for (const relay of relays) { - this.queueService.deliver(user, signed, relay.inbox, false, forceMainKey); + this.queueService.deliver(user, signed, relay.inbox, false, privateKey); } } } diff --git a/packages/backend/src/core/UserKeypairService.ts b/packages/backend/src/core/UserKeypairService.ts index 2341976068..26190024cd 100644 --- a/packages/backend/src/core/UserKeypairService.ts +++ b/packages/backend/src/core/UserKeypairService.ts @@ -13,6 +13,7 @@ import type { MiUserKeypair } from '@/models/UserKeypair.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import { GlobalEventService, GlobalEvents } from '@/core/GlobalEventService.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; @Injectable() export class UserKeypairService implements OnApplicationShutdown { @@ -27,6 +28,7 @@ export class UserKeypairService implements OnApplicationShutdown { private userKeypairsRepository: UserKeypairsRepository, private globalEventService: GlobalEventService, + private userEntityService: UserEntityService, ) { this.cache = new RedisKVCache(this.redisClient, 'userKeypair', { lifetime: 1000 * 60 * 60 * 24, // 24h @@ -44,6 +46,35 @@ export class UserKeypairService implements OnApplicationShutdown { return await this.cache.fetch(userId); } + /** + * + * @param userIdOrHint user id or MiUserKeypair + * @param preferType 'main' or 'ed25519'; If 'ed25519' is specified and ed25519 keypair is not exists, it will return main keypair + * @returns + */ + @bindThis + public async getLocalUserKeypairWithKeyId( + userIdOrHint: MiUser['id'] | MiUserKeypair, preferType: 'main' | 'ed25519' + ): Promise<{ keyId: string; publicKey: string; privateKey: string; }> { + const keypair = typeof userIdOrHint === 'string' ? await this.getUserKeypair(userIdOrHint) : userIdOrHint; + if (preferType === 'ed25519' && keypair.ed25519PublicKey != null && keypair.ed25519PrivateKey != null) { + return { + keyId: `${this.userEntityService.genLocalUserUri(keypair.userId)}#ed25519-key`, + publicKey: keypair.ed25519PublicKey, + privateKey: keypair.ed25519PrivateKey, + }; + } + if (preferType === 'main') { + return { + keyId: `${this.userEntityService.genLocalUserUri(keypair.userId)}#main-key`, + publicKey: keypair.publicKey, + privateKey: keypair.privateKey, + }; + } + + throw new Error('invalid type'); + } + @bindThis public async refresh(userId: MiUser['id']): Promise { return await this.cache.refresh(userId); @@ -52,14 +83,14 @@ export class UserKeypairService implements OnApplicationShutdown { /** * * @param userId user id - * @returns Promise true if keypair is created, false if keypair is already exists + * @returns MiUserKeypair if keypair is created, void if keypair is already exists */ @bindThis - public async refreshAndprepareEd25519KeyPair(userId: MiUser['id']): Promise { + public async refreshAndprepareEd25519KeyPair(userId: MiUser['id']): Promise { await this.refresh(userId); const keypair = await this.cache.fetch(userId); if (keypair.ed25519PublicKey != null) { - return false; + return; } const ed25519 = await genEd25519KeyPair(); @@ -68,7 +99,11 @@ export class UserKeypairService implements OnApplicationShutdown { ed25519PrivateKey: ed25519.privateKey, }); this.globalEventService.publishInternalEvent('userKeypairUpdated', { userId }); - return true; + return { + ...keypair, + ed25519PublicKey: ed25519.publicKey, + ed25519PrivateKey: ed25519.privateKey, + }; } @bindThis diff --git a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts index 449524c5ff..7f22f167af 100644 --- a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts +++ b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts @@ -10,7 +10,7 @@ import type { FollowingsRepository } from '@/models/_.js'; import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js'; import { QueueService } from '@/core/QueueService.js'; import { bindThis } from '@/decorators.js'; -import type { IActivity } from '@/core/activitypub/type.js'; +import type { IActivity, PrivateKey } from '@/core/activitypub/type.js'; import { ThinUser } from '@/queue/types.js'; import { AccountUpdateService } from '@/core/AccountUpdateService.js'; import type Logger from '@/logger.js'; @@ -109,17 +109,19 @@ class DeliverManager { * Execute delivers */ @bindThis - public async execute(opts?: { forceMainKey?: boolean }): Promise { + public async execute(opts?: { privateKey?: PrivateKey }): Promise { //#region MIGRATION - if (opts?.forceMainKey !== true) { + if (!opts?.privateKey) { /** * ed25519の署名がなければ追加する */ const created = await this.userKeypairService.refreshAndprepareEd25519KeyPair(this.actor.id); if (created) { + // createdが存在するということは新規作成されたということなので、フォロワーに配信する this.logger.info(`ed25519 key pair created for user ${this.actor.id} and publishing to followers`); // リモートに配信 - await this.accountUpdateService.publishToFollowers(this.actor.id, true); + const keyPair = await this.userKeypairService.getLocalUserKeypairWithKeyId(created, 'ed25519'); + await this.accountUpdateService.publishToFollowers(this.actor.id, { keyId: keyPair.keyId, privateKeyPem: keyPair.privateKey }); } } //#endregion @@ -163,7 +165,7 @@ class DeliverManager { } // deliver - await this.queueService.deliverMany(this.actor, this.activity, inboxes); + await this.queueService.deliverMany(this.actor, this.activity, inboxes, opts?.privateKey); this.logger.info(`Deliver queues dispatched: inboxes=${inboxes.size} actorId=${this.actor.id} activityId=${this.activity?.id}`); } } @@ -191,7 +193,7 @@ export class ApDeliverManagerService { * @param forceMainKey Force to use main (rsa) key */ @bindThis - public async deliverToFollowers(actor: { id: MiLocalUser['id']; host: null; }, activity: IActivity, forceMainKey?: boolean): Promise { + public async deliverToFollowers(actor: { id: MiLocalUser['id']; host: null; }, activity: IActivity, privateKey?: PrivateKey): Promise { const manager = new DeliverManager( this.userKeypairService, this.followingsRepository, @@ -202,7 +204,7 @@ export class ApDeliverManagerService { activity, ); manager.addFollowersRecipe(); - await manager.execute({ forceMainKey }); + await manager.execute({ privateKey }); } /** diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index 132593679e..a6b9053706 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -15,18 +15,7 @@ import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; import type Logger from '@/logger.js'; import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js'; - -type Signed = { - request: Request; - signingString: string; - signature: string; - signatureHeader: string; -}; - -type PrivateKey = { - privateKeyPem: string; - keyId: string; -}; +import type { PrivateKey } from './type.js'; export async function createSignedPost(args: { level: string; key: PrivateKey; url: string; body: string; digest?: string, additionalHeaders: Record }) { const u = new URL(args.url); @@ -98,21 +87,19 @@ export class ApRequestService { */ @bindThis private async getPrivateKey(userId: MiUser['id'], level: string): Promise { - const keypair = await this.userKeypairService.getUserKeypair(userId); + const type = level === '00' || level === '10' ? 'ed25519' : 'main'; + const keypair = await this.userKeypairService.getLocalUserKeypairWithKeyId(userId, type); - return (level !== '00' && level !== '10' && keypair.ed25519PrivateKey) ? { - privateKeyPem: keypair.ed25519PrivateKey, - keyId: `${this.config.url}/users/${userId}#ed25519-key`, - } : { + return { + keyId: keypair.keyId, privateKeyPem: keypair.privateKey, - keyId: `${this.config.url}/users/${userId}#main-key`, }; } @bindThis - public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown, level: string, digest?: string): Promise { + public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown, level: string, digest?: string, key?: PrivateKey): Promise { const body = typeof object === 'string' ? object : JSON.stringify(object); - const key = await this.getPrivateKey(user.id, level); + key = key ?? await this.getPrivateKey(user.id, level); const req = await createSignedPost({ level, key, diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 0b34fd7664..54d1c52dc0 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -326,3 +326,8 @@ export const isAnnounce = (object: IObject): object is IAnnounce => getApType(ob export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block'; export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag'; export const isMove = (object: IObject): object is IMove => getApType(object) === 'Move'; + +export type PrivateKey = { + privateKeyPem: string; + keyId: string; +}; diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts index 2a4dd5f997..47a69e7622 100644 --- a/packages/backend/src/queue/processors/DeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts @@ -76,16 +76,14 @@ export class DeliverProcessorService { await this.fetchInstanceMetadataService.fetchInstanceMetadata(_server).then(() => {}); const server = await this.federatedInstanceService.fetch(host); - /** - * RSAキーを強制するかでレベルを変える - */ - const level = job.data.forceMainKey ? - server.httpMessageSignaturesImplementationLevel === '11' ? - '10' : - '00' - : server.httpMessageSignaturesImplementationLevel; - - await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content, level, job.data.digest); + await this.apRequestService.signedPost( + job.data.user, + job.data.to, + job.data.content, + server.httpMessageSignaturesImplementationLevel, + job.data.digest, + job.data.privateKey, + ); // Update stats if (server.isNotResponding) { diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts index 74e0c0a66e..12e3136270 100644 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -8,7 +8,7 @@ import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiNote } from '@/models/Note.js'; import type { MiUser } from '@/models/User.js'; import type { MiWebhook } from '@/models/Webhook.js'; -import type { IActivity } from '@/core/activitypub/type.js'; +import type { IActivity, PrivateKey } from '@/core/activitypub/type.js'; import type { ParsedSignature } from '@misskey-dev/node-http-message-signatures'; /** @@ -39,7 +39,7 @@ export type DeliverJobData = { /** whether it is sharedInbox */ isSharedInbox: boolean; /** force to use main (rsa) key */ - forceMainKey?: boolean; + privateKey?: PrivateKey; }; export type InboxJobData = {