mirror of
https://github.com/Blah-IM/typescript-core.git
synced 2025-04-30 16:21:10 +00:00
refactor: extract BlahActKey to streamline BlahIdentity
This commit is contained in:
parent
f7afcaa1f2
commit
79fbd17ff8
4 changed files with 203 additions and 131 deletions
|
@ -42,6 +42,10 @@ export class BlahKeyPair {
|
|||
this.internalPrivateKey = privateKey;
|
||||
}
|
||||
|
||||
async verifyPayload<P>(signedPayload: BlahSignedPayload<P>): Promise<P> {
|
||||
return await this.internalPublicKey.verifyPayload(signedPayload);
|
||||
}
|
||||
|
||||
static async generate(extractable: boolean = true): Promise<BlahKeyPair> {
|
||||
const { publicKey, privateKey } = await crypto.subtle.generateKey(
|
||||
"Ed25519",
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
import z from "zod";
|
||||
import { BlahPublicKey } from "../crypto/publicKey.ts";
|
||||
import { BlahKeyPair } from "../crypto/keypair.ts";
|
||||
import type { BlahSignedPayload } from "../crypto/signedPayload.ts";
|
||||
|
||||
export const blahActKeyRecordSchema = z.object({
|
||||
typ: z.literal("user_act_key"),
|
||||
|
@ -13,3 +16,156 @@ export type BlahActKeyRecord = {
|
|||
expire_time: number;
|
||||
comment: string;
|
||||
};
|
||||
|
||||
export type ActKeyUpdate = {
|
||||
expiresAt?: Date;
|
||||
comment?: string;
|
||||
};
|
||||
|
||||
export class BlahActKey {
|
||||
private internalKey: BlahPublicKey | BlahKeyPair;
|
||||
private internalExpiresAt: Date;
|
||||
private internalComment: string;
|
||||
private internalIdKeyPublic: BlahPublicKey;
|
||||
private internalSigValid: boolean;
|
||||
private internalSignedRecord: BlahSignedPayload<BlahActKeyRecord>;
|
||||
|
||||
private constructor(
|
||||
key: BlahPublicKey | BlahKeyPair,
|
||||
idKeyPublic: BlahPublicKey,
|
||||
fullConfig: Required<ActKeyUpdate>,
|
||||
sigValid: boolean,
|
||||
signedRecord: BlahSignedPayload<BlahActKeyRecord>,
|
||||
) {
|
||||
this.internalKey = key;
|
||||
this.internalIdKeyPublic = idKeyPublic;
|
||||
this.internalExpiresAt = fullConfig.expiresAt;
|
||||
this.internalComment = fullConfig.comment;
|
||||
this.internalSigValid = sigValid;
|
||||
this.internalSignedRecord = signedRecord;
|
||||
}
|
||||
|
||||
static async fromSignedRecord(
|
||||
raw: BlahSignedPayload<BlahActKeyRecord>,
|
||||
idKeyPublic: BlahPublicKey,
|
||||
keypair?: BlahKeyPair,
|
||||
): Promise<BlahActKey> {
|
||||
let record: BlahActKeyRecord;
|
||||
let sigValid = false;
|
||||
try {
|
||||
record = await idKeyPublic.verifyPayload(raw);
|
||||
sigValid = true;
|
||||
} catch {
|
||||
record = raw.signee.payload;
|
||||
sigValid = false;
|
||||
}
|
||||
|
||||
const key: BlahPublicKey | BlahKeyPair = keypair ??
|
||||
await BlahPublicKey.fromID(record.act_key);
|
||||
const fullConfig: Required<ActKeyUpdate> = {
|
||||
expiresAt: new Date(record.expire_time * 1000),
|
||||
comment: record.comment,
|
||||
};
|
||||
|
||||
return new BlahActKey(key, idKeyPublic, fullConfig, sigValid, raw);
|
||||
}
|
||||
|
||||
static async create(
|
||||
key: BlahPublicKey | BlahKeyPair,
|
||||
idKeyPair: BlahKeyPair,
|
||||
config?: ActKeyUpdate,
|
||||
): Promise<BlahActKey> {
|
||||
const fullConfig: Required<ActKeyUpdate> = {
|
||||
expiresAt: new Date(Date.now() + 365 * 24 * 3600 * 1000),
|
||||
comment: "",
|
||||
...config,
|
||||
};
|
||||
const record: BlahActKeyRecord = {
|
||||
typ: "user_act_key",
|
||||
act_key: key.id,
|
||||
expire_time: Math.floor(fullConfig.expiresAt.getTime() / 1000),
|
||||
comment: fullConfig.comment,
|
||||
};
|
||||
|
||||
const signedRecord = await idKeyPair.signPayload(record);
|
||||
|
||||
return new BlahActKey(
|
||||
key,
|
||||
idKeyPair.publicKey,
|
||||
fullConfig,
|
||||
true,
|
||||
signedRecord,
|
||||
);
|
||||
}
|
||||
|
||||
get isExpired(): boolean {
|
||||
return new Date() > this.internalExpiresAt;
|
||||
}
|
||||
|
||||
get isSigValid(): boolean {
|
||||
return this.internalSigValid;
|
||||
}
|
||||
|
||||
get comment(): string {
|
||||
return this.internalComment;
|
||||
}
|
||||
|
||||
get canSign(): boolean {
|
||||
return this.internalKey instanceof BlahKeyPair;
|
||||
}
|
||||
|
||||
get publicKey(): BlahPublicKey {
|
||||
if (this.internalKey instanceof BlahKeyPair) {
|
||||
return this.internalKey.publicKey;
|
||||
} else {
|
||||
return this.internalKey;
|
||||
}
|
||||
}
|
||||
|
||||
async signPayload<P>(
|
||||
payload: P,
|
||||
date: Date = new Date(),
|
||||
): Promise<BlahSignedPayload<P>> {
|
||||
if (!this.canSign) throw new Error("Cannot sign without a private key");
|
||||
|
||||
return await (this.internalKey as BlahKeyPair).signPayload(
|
||||
payload,
|
||||
date,
|
||||
this.internalIdKeyPublic.id,
|
||||
);
|
||||
}
|
||||
|
||||
async verifyPayload<P>(
|
||||
payload: BlahSignedPayload<P>,
|
||||
): Promise<P> {
|
||||
if (payload.signee.id_key !== this.internalIdKeyPublic.id) {
|
||||
throw new Error("Payload signed with a different ID key");
|
||||
}
|
||||
if (new Date(payload.signee.timestamp * 1000) > this.internalExpiresAt) {
|
||||
throw new Error("Key was expired at the time of signing");
|
||||
}
|
||||
return await this.internalKey.verifyPayload(payload);
|
||||
}
|
||||
|
||||
async update(update: ActKeyUpdate, idKeyPair: BlahKeyPair): Promise<void> {
|
||||
if (update.expiresAt) this.internalExpiresAt = update.expiresAt;
|
||||
if (update.comment) this.internalComment = update.comment;
|
||||
|
||||
this.internalSignedRecord = await idKeyPair.signPayload({
|
||||
typ: "user_act_key",
|
||||
act_key: this.internalKey.id,
|
||||
expire_time: Math.floor(this.internalExpiresAt.getTime() / 1000),
|
||||
comment: this.internalComment,
|
||||
});
|
||||
this.internalSigValid = true;
|
||||
}
|
||||
|
||||
toSignedRecord(): BlahSignedPayload<BlahActKeyRecord> {
|
||||
return this.internalSignedRecord;
|
||||
}
|
||||
|
||||
setKeyPair(keypair: BlahKeyPair) {
|
||||
if (this.internalKey.id !== keypair.id) throw new Error("Key ID mismatch");
|
||||
this.internalKey = keypair;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -76,7 +76,7 @@ Deno.test("identity file act key sigs are properly verfied", async () => {
|
|||
.fromIdentityFile(
|
||||
identityFileWithActKeyInvalidActKeySig,
|
||||
);
|
||||
expect(identityWithActKeyInvalidActKeySig.actKeys[0].sigValid).toBe(false);
|
||||
expect(identityWithActKeyInvalidActKeySig.actKeys[0].isSigValid).toBe(false);
|
||||
});
|
||||
|
||||
Deno.test("add a second act key", async () => {
|
||||
|
@ -105,6 +105,12 @@ Deno.test("update first act key", async () => {
|
|||
expect(record.comment).toBe("test2");
|
||||
});
|
||||
|
||||
Deno.test("act key properly expires", async () => {
|
||||
expect(identity.actKeys[0].isExpired).toBe(false);
|
||||
await identity.updateActKey(actKeyPair.id, { expiresAt: new Date(10000) });
|
||||
expect(identity.actKeys[0].isExpired).toBe(true);
|
||||
});
|
||||
|
||||
Deno.test("update profile", async () => {
|
||||
const newProfile: BlahProfile = {
|
||||
typ: "profile",
|
||||
|
|
|
@ -3,87 +3,20 @@ import {
|
|||
BlahPublicKey,
|
||||
type BlahSignedPayload,
|
||||
} from "../crypto/mod.ts";
|
||||
import { type BlahActKeyRecord, blahActKeyRecordSchema } from "./actKey.ts";
|
||||
import { type ActKeyUpdate, BlahActKey } from "./actKey.ts";
|
||||
import { blahIdentityFileSchema } from "./identityFile.ts";
|
||||
import type { BlahIdentityFile } from "./mod.ts";
|
||||
import type { BlahProfile } from "./profile.ts";
|
||||
|
||||
type InternalActKey = {
|
||||
raw: BlahSignedPayload<BlahActKeyRecord>;
|
||||
key: BlahPublicKey | BlahKeyPair;
|
||||
expiresAt: Date;
|
||||
sigValid: boolean;
|
||||
};
|
||||
|
||||
type ActKey = {
|
||||
publicKey: BlahPublicKey;
|
||||
expiresAt: Date;
|
||||
sigValid: boolean;
|
||||
comment: string;
|
||||
};
|
||||
|
||||
type ActKeyConfig = Partial<Omit<ActKey, "publicKey" | "sigValid">>;
|
||||
|
||||
async function constructActKeyFromRaw(
|
||||
raw: BlahSignedPayload<BlahActKeyRecord>,
|
||||
idKey: BlahPublicKey | BlahKeyPair,
|
||||
): Promise<InternalActKey> {
|
||||
const publicKey = idKey instanceof BlahKeyPair ? idKey.publicKey : idKey;
|
||||
let sigValid = false;
|
||||
try {
|
||||
await publicKey.verifyPayload(raw);
|
||||
sigValid = true;
|
||||
} catch {
|
||||
sigValid = false;
|
||||
}
|
||||
|
||||
const key = await BlahPublicKey.fromID(raw.signee.payload.act_key);
|
||||
const expiresAt = new Date(raw.signee.payload.expire_time * 1000);
|
||||
return { raw, key, expiresAt, sigValid };
|
||||
}
|
||||
|
||||
async function constructInternalActKey(
|
||||
idKeyPair: BlahKeyPair,
|
||||
key: BlahPublicKey | BlahKeyPair,
|
||||
config?: ActKeyConfig,
|
||||
): Promise<InternalActKey> {
|
||||
const actKey: ActKey = {
|
||||
publicKey: key instanceof BlahKeyPair ? key.publicKey : key,
|
||||
expiresAt: new Date(Date.now() + 365 * 24 * 3600 * 1000),
|
||||
sigValid: true,
|
||||
comment: "",
|
||||
...config,
|
||||
};
|
||||
|
||||
const rawRecord = await idKeyPair
|
||||
.signPayload(blahActKeyRecordSchema.parse(
|
||||
{
|
||||
typ: "user_act_key",
|
||||
expire_time: Math.floor(actKey.expiresAt.getTime() / 1000),
|
||||
comment: actKey.comment,
|
||||
act_key: actKey.publicKey.id,
|
||||
} satisfies BlahActKeyRecord,
|
||||
));
|
||||
|
||||
const internalActKey: InternalActKey = {
|
||||
raw: rawRecord,
|
||||
key,
|
||||
expiresAt: actKey.expiresAt,
|
||||
sigValid: true,
|
||||
};
|
||||
|
||||
return internalActKey;
|
||||
}
|
||||
|
||||
export class BlahIdentity {
|
||||
private internalIdKey: BlahPublicKey | BlahKeyPair;
|
||||
private internalActKeys: InternalActKey[];
|
||||
private internalActKeys: BlahActKey[];
|
||||
private rawProfile: BlahSignedPayload<BlahProfile>;
|
||||
private internalProfileSigValid: boolean;
|
||||
|
||||
private constructor(
|
||||
internalIdKey: BlahPublicKey | BlahKeyPair,
|
||||
internalActKeys: InternalActKey[],
|
||||
internalActKeys: BlahActKey[],
|
||||
rawProfile: BlahSignedPayload<BlahProfile>,
|
||||
internalProfileSigValid: boolean,
|
||||
) {
|
||||
|
@ -107,13 +40,8 @@ export class BlahIdentity {
|
|||
: this.internalIdKey;
|
||||
}
|
||||
|
||||
get actKeys(): ActKey[] {
|
||||
return this.internalActKeys.map(({ key, expiresAt, sigValid, raw }) => ({
|
||||
publicKey: key instanceof BlahKeyPair ? key.publicKey : key,
|
||||
expiresAt,
|
||||
sigValid,
|
||||
comment: raw.signee.payload.comment,
|
||||
}));
|
||||
get actKeys(): BlahActKey[] {
|
||||
return this.internalActKeys;
|
||||
}
|
||||
|
||||
static async fromIdentityFile(
|
||||
|
@ -133,28 +61,31 @@ export class BlahIdentity {
|
|||
if (idKey.id !== id_key) {
|
||||
throw new Error("ID key pair does not match ID key in identity file.");
|
||||
}
|
||||
const idKeyPublic = idKey instanceof BlahKeyPair ? idKey.publicKey : idKey;
|
||||
|
||||
const actKeys: InternalActKey[] = await Promise.all(
|
||||
const actKeys: BlahActKey[] = await Promise.all(
|
||||
act_keys.map(async (raw) => {
|
||||
const actKey = await constructActKeyFromRaw(raw, idKey);
|
||||
if (actingKeyPair?.id === actKey.key.id) actKey.key = actingKeyPair;
|
||||
const actKey = await BlahActKey.fromSignedRecord(raw, idKeyPublic);
|
||||
if (actingKeyPair?.id === actKey.publicKey.id) {
|
||||
actKey.setKeyPair(actingKeyPair);
|
||||
}
|
||||
return actKey;
|
||||
}),
|
||||
);
|
||||
|
||||
const rawProfile = profile;
|
||||
const profileSigningKey = await BlahPublicKey.fromID(
|
||||
rawProfile.signee.act_key,
|
||||
const profileSigningKey = actKeys.find((k) =>
|
||||
k.publicKey.id === profile.signee.act_key
|
||||
);
|
||||
if (actKeys.findIndex((k) => k.key.id === profileSigningKey.id) === -1) {
|
||||
throw new Error("Profile is not signed by any of the act keys.");
|
||||
}
|
||||
|
||||
let profileSigValid = false;
|
||||
try {
|
||||
await profileSigningKey.verifyPayload(rawProfile);
|
||||
profileSigValid = true;
|
||||
} catch {
|
||||
profileSigValid = false;
|
||||
if (profileSigningKey) {
|
||||
try {
|
||||
await profileSigningKey.verifyPayload(rawProfile);
|
||||
profileSigValid = true;
|
||||
} catch {
|
||||
profileSigValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
return new BlahIdentity(idKey, actKeys, rawProfile, profileSigValid);
|
||||
|
@ -164,82 +95,57 @@ export class BlahIdentity {
|
|||
idKeyPair: BlahKeyPair,
|
||||
firstActKey: BlahKeyPair,
|
||||
profile: BlahProfile,
|
||||
firstActKeyConfig?: ActKeyConfig,
|
||||
firstActKeyConfig?: ActKeyUpdate,
|
||||
): Promise<BlahIdentity> {
|
||||
const internalActKey = await constructInternalActKey(
|
||||
idKeyPair,
|
||||
const actKey = await BlahActKey.create(
|
||||
firstActKey,
|
||||
idKeyPair,
|
||||
firstActKeyConfig,
|
||||
);
|
||||
|
||||
const profileRecord: BlahSignedPayload<BlahProfile> = await firstActKey
|
||||
.signPayload(profile);
|
||||
const profileRecord = await firstActKey.signPayload(profile);
|
||||
|
||||
return new BlahIdentity(
|
||||
idKeyPair,
|
||||
[internalActKey],
|
||||
profileRecord,
|
||||
true,
|
||||
);
|
||||
return new BlahIdentity(idKeyPair, [actKey], profileRecord, true);
|
||||
}
|
||||
|
||||
generateIdentityFile(): BlahIdentityFile {
|
||||
return blahIdentityFileSchema.parse(
|
||||
{
|
||||
id_key: this.idPublicKey.id,
|
||||
act_keys: this.internalActKeys.map((k) => (k.raw)),
|
||||
act_keys: this.internalActKeys.map((k) => k.toSignedRecord()),
|
||||
profile: this.rawProfile,
|
||||
} satisfies BlahIdentityFile,
|
||||
);
|
||||
}
|
||||
|
||||
async addActKey(actKey: BlahKeyPair | BlahPublicKey, config?: ActKeyConfig) {
|
||||
async addActKey(actKey: BlahKeyPair | BlahPublicKey, config?: ActKeyUpdate) {
|
||||
if (this.internalIdKey instanceof BlahPublicKey) {
|
||||
throw new Error("Cannot add act key to identity without ID key pair.");
|
||||
}
|
||||
|
||||
const internalActKey = await constructInternalActKey(
|
||||
this.internalIdKey,
|
||||
actKey,
|
||||
config,
|
||||
);
|
||||
|
||||
this.internalActKeys.push(internalActKey);
|
||||
const key = await BlahActKey.create(actKey, this.internalIdKey, config);
|
||||
this.internalActKeys.push(key);
|
||||
}
|
||||
|
||||
async updateActKey(
|
||||
keyId: string,
|
||||
config: ActKeyConfig,
|
||||
) {
|
||||
async updateActKey(id: string, update: ActKeyUpdate) {
|
||||
if (this.internalIdKey instanceof BlahPublicKey) {
|
||||
throw new Error("Cannot update act key in identity without ID key pair.");
|
||||
}
|
||||
|
||||
const actKeyIndex = this.internalActKeys.findIndex(
|
||||
(k) => k.key.id === keyId,
|
||||
);
|
||||
if (actKeyIndex === -1) {
|
||||
const key = this.internalActKeys.find((k) => k.publicKey.id === id);
|
||||
if (!key) {
|
||||
throw new Error("Act key not found in identity.");
|
||||
}
|
||||
|
||||
this.internalActKeys[actKeyIndex] = await constructInternalActKey(
|
||||
this.internalIdKey,
|
||||
this.internalActKeys[actKeyIndex].key,
|
||||
config,
|
||||
);
|
||||
await key.update(update, this.internalIdKey);
|
||||
}
|
||||
|
||||
async updateProfile(profile: BlahProfile) {
|
||||
const signingActKey = this.internalActKeys.find((k) =>
|
||||
k.key instanceof BlahKeyPair
|
||||
);
|
||||
const signingActKey = this.internalActKeys.find((k) => k.canSign);
|
||||
if (!signingActKey) {
|
||||
throw new Error("No act key to sign profile with.");
|
||||
}
|
||||
|
||||
this.rawProfile = await (signingActKey.key as BlahKeyPair).signPayload(
|
||||
profile,
|
||||
);
|
||||
this.rawProfile = await signingActKey.signPayload(profile);
|
||||
this.internalProfileSigValid = true;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue