getLocalUserPrivateKey
This commit is contained in:
parent
689a9ce5f9
commit
0127f89298
|
@ -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
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue