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 * 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 { 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 { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { GlobalEventService, GlobalEvents } from '@/core/GlobalEventService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type { webcrypto } from 'node:crypto';
@Injectable()
export class UserKeypairService implements OnApplicationShutdown {
private cache: RedisKVCache<MiUserKeypair>;
private keypairEntityCache: RedisKVCache<MiUserKeypair>;
private privateKeyObjectCache: MemoryKVCache<webcrypto.CryptoKey>;
constructor(
@Inject(DI.redis)
@ -30,26 +32,29 @@ export class UserKeypairService implements OnApplicationShutdown {
private globalEventService: GlobalEventService,
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
memoryCacheLifetime: Infinity,
fetcher: (key) => this.userKeypairsRepository.findOneByOrFail({ userId: key }),
toRedisConverter: (value) => JSON.stringify(value),
fromRedisConverter: (value) => JSON.parse(value),
});
this.privateKeyObjectCache = new MemoryKVCache<webcrypto.CryptoKey>(1000 * 60 * 60 * 1);
this.redisForSub.on('message', this.onMessage);
}
@bindThis
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 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
*/
@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
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
public async refreshAndprepareEd25519KeyPair(userId: MiUser['id']): Promise<MiUserKeypair | void> {
await this.refresh(userId);
const keypair = await this.cache.fetch(userId);
const keypair = await this.keypairEntityCache.fetch(userId);
if (keypair.ed25519PublicKey != null) {
return;
}
@ -119,7 +177,7 @@ export class UserKeypairService implements OnApplicationShutdown {
}
@bindThis
public dispose(): void {
this.cache.dispose();
this.keypairEntityCache.dispose();
}
@bindThis

View file

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

View file

@ -91,10 +91,9 @@ export class ApRequestService {
@bindThis
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);
key = key ?? await this.userKeypairService.getLocalUserPrivateKeyPem(user.id, level);
const req = await createSignedPost({
level,
key,
key: await this.userKeypairService.getLocalUserPrivateKey(key ?? user.id, level),
url,
body,
additionalHeaders: {
@ -124,7 +123,7 @@ export class ApRequestService {
*/
@bindThis
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({
level,
key,

View file

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