getLocalUserPrivateKey

This commit is contained in:
tamaina 2024-03-05 16:27:13 +00:00
parent 689a9ce5f9
commit 0127f89298
4 changed files with 74 additions and 14 deletions

View File

@ -5,19 +5,21 @@
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import * as Redis from 'ioredis'; import * as Redis from 'ioredis';
import { genEd25519KeyPair, PrivateKeyWithPem } from '@misskey-dev/node-http-message-signatures'; import { genEd25519KeyPair, importPrivateKey, PrivateKey, PrivateKeyWithPem } from '@misskey-dev/node-http-message-signatures';
import type { MiUser } from '@/models/User.js'; import type { MiUser } from '@/models/User.js';
import type { UserKeypairsRepository } from '@/models/_.js'; import type { UserKeypairsRepository } from '@/models/_.js';
import { RedisKVCache } from '@/misc/cache.js'; import { RedisKVCache, MemoryKVCache } from '@/misc/cache.js';
import type { MiUserKeypair } from '@/models/UserKeypair.js'; import type { MiUserKeypair } from '@/models/UserKeypair.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { GlobalEventService, GlobalEvents } from '@/core/GlobalEventService.js'; import { GlobalEventService, GlobalEvents } from '@/core/GlobalEventService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type { webcrypto } from 'node:crypto';
@Injectable() @Injectable()
export class UserKeypairService implements OnApplicationShutdown { export class UserKeypairService implements OnApplicationShutdown {
private cache: RedisKVCache<MiUserKeypair>; private keypairEntityCache: RedisKVCache<MiUserKeypair>;
private privateKeyObjectCache: MemoryKVCache<webcrypto.CryptoKey>;
constructor( constructor(
@Inject(DI.redis) @Inject(DI.redis)
@ -30,26 +32,29 @@ export class UserKeypairService implements OnApplicationShutdown {
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
) { ) {
this.cache = new RedisKVCache<MiUserKeypair>(this.redisClient, 'userKeypair', { this.keypairEntityCache = new RedisKVCache<MiUserKeypair>(this.redisClient, 'userKeypair', {
lifetime: 1000 * 60 * 60 * 24, // 24h lifetime: 1000 * 60 * 60 * 24, // 24h
memoryCacheLifetime: Infinity, memoryCacheLifetime: Infinity,
fetcher: (key) => this.userKeypairsRepository.findOneByOrFail({ userId: key }), fetcher: (key) => this.userKeypairsRepository.findOneByOrFail({ userId: key }),
toRedisConverter: (value) => JSON.stringify(value), toRedisConverter: (value) => JSON.stringify(value),
fromRedisConverter: (value) => JSON.parse(value), fromRedisConverter: (value) => JSON.parse(value),
}); });
this.privateKeyObjectCache = new MemoryKVCache<webcrypto.CryptoKey>(1000 * 60 * 60 * 1);
this.redisForSub.on('message', this.onMessage); this.redisForSub.on('message', this.onMessage);
} }
@bindThis @bindThis
public async getUserKeypair(userId: MiUser['id']): Promise<MiUserKeypair> { public async getUserKeypair(userId: MiUser['id']): Promise<MiUserKeypair> {
return await this.cache.fetch(userId); return await this.keypairEntityCache.fetch(userId);
} }
/** /**
* * Get private key [Only PrivateKeyWithPem for queue data etc.]
* @param userIdOrHint user id or MiUserKeypair * @param userIdOrHint user id or MiUserKeypair
* @param preferType If ed25519-like(`ed25519`, `01`, `11`) is specified, ed25519 keypair is returned if exists. Otherwise, main keypair is returned. * @param preferType
* If ed25519-like(`ed25519`, `01`, `11`) is specified, ed25519 keypair is returned if exists.
* Otherwise, main keypair is returned.
* @returns * @returns
*/ */
@bindThis @bindThis
@ -72,9 +77,62 @@ export class UserKeypairService implements OnApplicationShutdown {
}; };
} }
/**
* Get private key [Only PrivateKey for ap request]
* Using cache due to performance reasons of `crypto.subtle.importKey`
* @param userIdOrHint user id, MiUserKeypair, or PrivateKeyWithPem
* @param preferType
* If ed25519-like(`ed25519`, `01`, `11`) is specified, ed25519 keypair is returned if exists.
* Otherwise, main keypair is returned. (ignored if userIdOrHint is PrivateKeyWithPem)
* @returns
*/
@bindThis
public async getLocalUserPrivateKey(
userIdOrHint: MiUser['id'] | MiUserKeypair | PrivateKeyWithPem, preferType?: string,
): Promise<PrivateKey> {
if (typeof userIdOrHint === 'object' && 'privateKeyPem' in userIdOrHint) {
// userIdOrHint is PrivateKeyWithPem
return {
keyId: userIdOrHint.keyId,
privateKey: await this.privateKeyObjectCache.fetch(userIdOrHint.keyId, async () => {
return await importPrivateKey(userIdOrHint.privateKeyPem);
}),
};
}
const userId = typeof userIdOrHint === 'string' ? userIdOrHint : userIdOrHint.userId;
const getKeypair = () => typeof userIdOrHint === 'string' ? this.getUserKeypair(userId) : userIdOrHint;
if (preferType && ['01', '11', 'ed25519'].includes(preferType.toLowerCase())) {
const keyId = `${this.userEntityService.genLocalUserUri(userId)}#ed25519-key`;
const fetched = await this.privateKeyObjectCache.fetchMaybe(keyId, async () => {
const keypair = await getKeypair();
if (keypair.ed25519PublicKey != null && keypair.ed25519PrivateKey != null) {
return await importPrivateKey(keypair.ed25519PrivateKey);
}
return;
});
if (fetched) {
return {
keyId,
privateKey: fetched,
};
}
}
const keyId = `${this.userEntityService.genLocalUserUri(userId)}#main-key`;
return {
keyId,
privateKey: await this.privateKeyObjectCache.fetch(keyId, async () => {
const keypair = await getKeypair();
return await importPrivateKey(keypair.privateKey);
}),
};
}
@bindThis @bindThis
public async refresh(userId: MiUser['id']): Promise<void> { public async refresh(userId: MiUser['id']): Promise<void> {
return await this.cache.refresh(userId); return await this.keypairEntityCache.refresh(userId);
} }
/** /**
@ -85,7 +143,7 @@ export class UserKeypairService implements OnApplicationShutdown {
@bindThis @bindThis
public async refreshAndprepareEd25519KeyPair(userId: MiUser['id']): Promise<MiUserKeypair | void> { public async refreshAndprepareEd25519KeyPair(userId: MiUser['id']): Promise<MiUserKeypair | void> {
await this.refresh(userId); await this.refresh(userId);
const keypair = await this.cache.fetch(userId); const keypair = await this.keypairEntityCache.fetch(userId);
if (keypair.ed25519PublicKey != null) { if (keypair.ed25519PublicKey != null) {
return; return;
} }
@ -119,7 +177,7 @@ export class UserKeypairService implements OnApplicationShutdown {
} }
@bindThis @bindThis
public dispose(): void { public dispose(): void {
this.cache.dispose(); this.keypairEntityCache.dispose();
} }
@bindThis @bindThis

View File

@ -252,7 +252,7 @@ export class ApRendererService {
@bindThis @bindThis
public renderKey(user: MiLocalUser, publicKey: string, postfix?: string): IKey { public renderKey(user: MiLocalUser, publicKey: string, postfix?: string): IKey {
return { return {
id: `${this.config.url}/users/${user.id}${postfix ?? '/publickey'}`, id: `${this.userEntityService.genLocalUserUri(user.id)}${postfix ?? '/publickey'}`,
type: 'Key', type: 'Key',
owner: this.userEntityService.genLocalUserUri(user.id), owner: this.userEntityService.genLocalUserUri(user.id),
publicKeyPem: createPublicKey(publicKey).export({ publicKeyPem: createPublicKey(publicKey).export({

View File

@ -91,10 +91,9 @@ export class ApRequestService {
@bindThis @bindThis
public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown, level: string, digest?: string, key?: PrivateKeyWithPem): Promise<void> { public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown, level: string, digest?: string, key?: PrivateKeyWithPem): Promise<void> {
const body = typeof object === 'string' ? object : JSON.stringify(object); const body = typeof object === 'string' ? object : JSON.stringify(object);
key = key ?? await this.userKeypairService.getLocalUserPrivateKeyPem(user.id, level);
const req = await createSignedPost({ const req = await createSignedPost({
level, level,
key, key: await this.userKeypairService.getLocalUserPrivateKey(key ?? user.id, level),
url, url,
body, body,
additionalHeaders: { additionalHeaders: {
@ -124,7 +123,7 @@ export class ApRequestService {
*/ */
@bindThis @bindThis
public async signedGet(url: string, user: { id: MiUser['id'] }, level: string): Promise<unknown> { public async signedGet(url: string, user: { id: MiUser['id'] }, level: string): Promise<unknown> {
const key = await this.userKeypairService.getLocalUserPrivateKeyPem(user.id, level); const key = await this.userKeypairService.getLocalUserPrivateKey(user.id, level);
const req = await createSignedGet({ const req = await createSignedGet({
level, level,
key, key,

View File

@ -195,6 +195,9 @@ export class MemoryKVCache<T> {
private lifetime: number; private lifetime: number;
private gcIntervalHandle: NodeJS.Timeout; private gcIntervalHandle: NodeJS.Timeout;
/**
* @param lifetime (ms)
*/
constructor(lifetime: MemoryKVCache<never>['lifetime']) { constructor(lifetime: MemoryKVCache<never>['lifetime']) {
this.cache = new Map(); this.cache = new Map();
this.lifetime = lifetime; this.lifetime = lifetime;