From 9a9f0f3b9d2bdd01b6424c898de71d4d8a0d13f2 Mon Sep 17 00:00:00 2001 From: Benjamin Preiss Date: Mon, 9 Oct 2023 22:44:12 +0200 Subject: [PATCH] first batch of keychain program implementation --- packages/programs/program/src/index.ts | 1 + packages/programs/program/src/keychain.ts | 219 ++++++++++++++++++++++ packages/utils/crypto/src/ed25519.ts | 61 +++++- packages/utils/crypto/src/encryption.ts | 18 +- packages/utils/crypto/src/key.ts | 15 +- packages/utils/crypto/src/sepc256k1.ts | 61 +++++- packages/utils/crypto/src/x25519.ts | 70 ++++++- 7 files changed, 432 insertions(+), 13 deletions(-) create mode 100644 packages/programs/program/src/keychain.ts diff --git a/packages/programs/program/src/index.ts b/packages/programs/program/src/index.ts index 973a380f1..5d12d894e 100644 --- a/packages/programs/program/src/index.ts +++ b/packages/programs/program/src/index.ts @@ -5,3 +5,4 @@ export { export * from "./client.js"; export * from "./program.js"; export * from "./address.js"; +export * from "./keychain.js"; diff --git a/packages/programs/program/src/keychain.ts b/packages/programs/program/src/keychain.ts new file mode 100644 index 000000000..41ae6236a --- /dev/null +++ b/packages/programs/program/src/keychain.ts @@ -0,0 +1,219 @@ +import { AbstractType, field, option, variant, vec } from "@dao-xyz/borsh"; +import { Program } from "./program"; +import { + And, + BoolQuery, + ByteMatchQuery, + Documents, + MissingField, + Or, + PutOperation, + Query, + SearchRequest, + Sort, + SortDirection, + StateFieldQuery, + StringMatch +} from "../../data/document/src"; +import { + EncryptedThing, + EncryptedSecp256k1Keypair, + EncryptedX25519Keypair, + EncryptedEd25519Keypair, + EncryptedKeypair, + DecryptedThing, + createLocalEncryptProvider, + X25519Keypair, + X25519PublicKey, + createDecrypterFromKeyResolver, + KeyResolverReturnType, + Ed25519PublicKey, + Secp256k1PublicKey, + Identity, + EncryptProvide, + KeyExchangeOptions, + DecryptProvider +} from "@peerbit/crypto"; +import { compare } from "@peerbit/uint8arrays"; + +@variant(0) +class EncryptedExtendedKey { + // TODO: The owner of the key determined by wallet address or public sign key? + @field({ type: vec("u8") }) + owner: Uint8Array; + // Is this key revoked? HAS TO BE TRUE if recipient is set. + @field({ type: "bool" }) + revoked: boolean; + // Provide a single-use key for someone else + @field({ type: option(vec("u8")) }) + recipient?: Uint8Array; + // TODO: add the actual key here + @field({ type: EncryptedKeypair }) + keypair: + | EncryptedX25519Keypair + | EncryptedEd25519Keypair + | EncryptedSecp256k1Keypair; + + constructor(parameters: { + owner: Uint8Array; + keypair: + | EncryptedX25519Keypair + | EncryptedEd25519Keypair + | EncryptedSecp256k1Keypair; + revoked?: boolean; + }) { + this.keypair = parameters.keypair; + this.owner = parameters.owner; + this.revoked = parameters.revoked ?? false; + } +} + +export class KeychainProgram extends Program { + @field({ type: Documents }) + keys: Documents; + + @field({ type: Identity }) + identity: Identity; + + @field({ type: EncryptProvide }) + encryptProvider: EncryptProvide; + + @field({ type: DecryptProvider }) + decryptProvider: DecryptProvider; + + // TODO: Offer rotate keys functionality. revokes all unrevoked keys and adds a new key which it returns + // TODO: Write Key updater that either adds a given key or generates a new one. + // TODO: the key updater should always set the revoked flag for keys with a recipient + + // TODO: Write a key getter that get's my latest key. If no valid key is found, run rotate and return newly created + // TODO: Write a key getter that get's the latest key's of recipients. If no valid key is found for a recipient, add a new, temporary key for the recipient. + // TODO: Write a key getter that get's a specified key, no matter the revoked status + async getKey( + parameters: + | { owner?: Uint8Array; publicKey?: never } + | { + owner?: never; + publicKey?: X25519PublicKey | Ed25519PublicKey | Secp256k1PublicKey; + } = {} + ) { + const { owner = this.identity.publicKey.bytes, publicKey } = parameters; + // Create Filter options + const queries = + // Looking for specific public keys. We don't care about revocation status. + publicKey != undefined + ? [ + new ByteMatchQuery({ + key: ["keypair", "publicKey", "publicKey"], + value: publicKey.publicKey + }) + ] + : // Looking for latest key from a specific owner. + [ + new And([ + // Only non-revoked + new BoolQuery({ key: "revoked", value: false }), + // Recipient has to be undefined + // TODO: Is this correct? + new MissingField({ key: "recipient" }), + // And from the specified owner + new ByteMatchQuery({ + key: "owner", + value: owner + }) + ]) + ]; + // TODO: How can we limit this to only fetch the last entry - so a single one? + const keys = await this.keys.index.search( + new SearchRequest({ + query: queries, + sort: new Sort({ + key: "timestamp", + direction: SortDirection.DESC + }) + }) + ); + // Create a new key in case we can't find a match + // If owner is not me, create a new, temporary key with me as owner and recipient as the requested owner + // If owner is me, create a new permanent key with + if (keys.length === 0) { + //return compare(this.identity.publicKey.bytes, owner) ? + } + return keys[0].keypair.decrypt(this.decryptProvider); + } + + // TODO: Unsure about encryptProvider here honestly - should this be layer 1 keychain? + async open(parameters: { + identity: Identity; + encryptProvider: EncryptProvide; + decryptProvider: DecryptProvider; + }) { + this.identity = parameters.identity; + this.encryptProvider = parameters.encryptProvider; + this.decryptProvider = parameters.decryptProvider; + // TODO: This should be opened with the layer 1 encryption provider + await this.keys.open({ + type: EncryptedExtendedKey, + canPerform: async (operation, { entry }) => { + // Sender can put, identified by masterkey + const owner = ( + operation instanceof PutOperation + ? operation.value + : await this.keys.index.get(operation.key) + )?.owner; + for (const signer of entry.signatures) { + if (signer.publicKey.equals(owner)) return true; + } + return false; + }, + index: { + // TODO: not sure about this + key: ["keyPair", "publicKey"], + // -> I want to be able to search .pub and .owner properties + fields: async (doc, context) => { + { + return { + ...doc, + timestamp: context.created + }; + } + }, + canRead: () => true, + canSearch: () => true + }, + // TODO: Potentially unsafe, as untrusted nodes might hide entries + canReplicate: () => true + }); + } + + async encrypt(plainText: T, recipients: Uint8Array[]) { + if (recipients.length === 0) throw new Error("No recipients specified"); + // TODO: Get and Feed my own key into this. + const myEncryptionKeypair = await X25519Keypair.create(); + const encryptionProvider = createLocalEncryptProvider(myEncryptionKeypair); + // TODO: Get and Feed recipient keys into .encrypt + const recipientPublicKeys = await Promise.all([ + X25519PublicKey.create(), + X25519PublicKey.create() + ]); + return new DecryptedThing({ value: plainText }).encrypt( + encryptionProvider, + { type: "publicKey", receiverPublicKeys: recipientPublicKeys } + ); + } + + async decrypt(encryptedThing: EncryptedThing, clazz: AbstractType) { + const keyResolver = async ( + key: X25519PublicKey | Uint8Array + ) => { + // TODO: fetch keys here based on key parameter and return them + if (key instanceof X25519PublicKey) + return X25519Keypair.create() as Promise< + KeyResolverReturnType + >; + return undefined; + }; + const decryptionProvider = createDecrypterFromKeyResolver(keyResolver); + const decryptedThing = await encryptedThing.decrypt(decryptionProvider); + return decryptedThing.getValue(clazz); + } +} diff --git a/packages/utils/crypto/src/ed25519.ts b/packages/utils/crypto/src/ed25519.ts index 873a3530a..b186c24f4 100644 --- a/packages/utils/crypto/src/ed25519.ts +++ b/packages/utils/crypto/src/ed25519.ts @@ -1,5 +1,10 @@ import { field, fixedArray, variant } from "@dao-xyz/borsh"; -import { PrivateSignKey, PublicSignKey, Keypair } from "./key.js"; +import { + PrivateSignKey, + PublicSignKey, + Keypair, + EncryptedKeypair +} from "./key.js"; import { equals } from "@peerbit/uint8arrays"; import { Identity, Signer, SignWithKey } from "./signer.js"; import { SignatureWithKey } from "./signature.js"; @@ -12,6 +17,13 @@ import type { Ed25519PeerId, PeerId } from "@libp2p/interface/peer-id"; import { sign } from "./ed25519-sign.js"; import { PreHash } from "./prehash.js"; import { concat } from "uint8arrays"; +import { + DecryptProvider, + DecryptedThing, + EncryptProvide, + EncryptedThing, + KeyExchangeOptions +} from "./encryption.js"; @variant(0) export class Ed25519PublicKey extends PublicSignKey { @@ -191,4 +203,51 @@ export class Ed25519Keypair extends Keypair implements Identity { ).bytes ); } + + async encrypt( + provider: EncryptProvide, + parameters: Parameters + ) { + const decryptedSecret = new DecryptedThing({ value: this.privateKey }); + return new EncryptedEd25519Keypair({ + publicKey: this.publicKey, + encryptedPrivateKey: await decryptedSecret.encrypt(provider, parameters) + }); + } +} + +@variant(1) +export class EncryptedEd25519Keypair extends EncryptedKeypair { + @field({ type: Ed25519PublicKey }) + publicKey: Ed25519PublicKey; + + @field({ type: EncryptedThing }) + encryptedPrivateKey: EncryptedThing; + + constructor(properties: { + publicKey: Ed25519PublicKey; + encryptedPrivateKey: EncryptedThing; + }) { + super(); + this.encryptedPrivateKey = properties.encryptedPrivateKey; + this.publicKey = properties.publicKey; + } + + equals(other: EncryptedKeypair) { + if (other instanceof EncryptedEd25519Keypair) { + return ( + this.publicKey.equals(other.publicKey) && + this.encryptedPrivateKey.equals(other.encryptedPrivateKey) + ); + } + return false; + } + + async decrypt(provider: DecryptProvider) { + const decryptedThing = await this.encryptedPrivateKey.decrypt(provider); + return new Ed25519Keypair({ + publicKey: this.publicKey, + privateKey: decryptedThing.getValue(Ed25519PrivateKey) + }); + } } diff --git a/packages/utils/crypto/src/encryption.ts b/packages/utils/crypto/src/encryption.ts index 109502224..288e90647 100644 --- a/packages/utils/crypto/src/encryption.ts +++ b/packages/utils/crypto/src/encryption.ts @@ -67,7 +67,7 @@ type EnvelopeFromParameter = ? PublicKeyEnvelope : HashedKeyEnvelope; -type EncryptProvide = ( +export type EncryptProvide = ( bytes: Uint8Array, parameters: Parameters ) => Promise>>; @@ -143,18 +143,22 @@ export const createLocalEncryptProvider = < }; }; -type DecryptProvider = ( +export type DecryptProvider = ( encrypted: Uint8Array, nonce: Uint8Array, exchange: Envelope ) => Promise; -type KeyResolver = ( - key: PublicKey -) => +export type KeyResolverReturnType< + PublicKey extends X25519PublicKey | Uint8Array +> = | (PublicKey extends X25519PublicKey ? X25519Keypair : Uint8Array) | undefined; +export type KeyResolver = ( + key: PublicKey +) => Promise>; + export const createDecrypterFromKeyResolver = ( keyResolver: KeyResolver ): DecryptProvider => { @@ -174,7 +178,7 @@ export const createDecrypterFromKeyResolver = ( if (exported) { key = { index: i, - keypair: exported + keypair: await exported }; break; } @@ -203,7 +207,7 @@ export const createDecrypterFromKeyResolver = ( throw new AccessError("Failed to resolve decryption key"); } } else if (exchange instanceof HashedKeyEnvelope) { - epheremalKey = keyResolver(exchange.hash); + epheremalKey = await keyResolver(exchange.hash); } if (!epheremalKey) { diff --git a/packages/utils/crypto/src/key.ts b/packages/utils/crypto/src/key.ts index 8384ff5a2..b39f78e9f 100644 --- a/packages/utils/crypto/src/key.ts +++ b/packages/utils/crypto/src/key.ts @@ -1,8 +1,9 @@ -import { field, serialize } from "@dao-xyz/borsh"; +import { field, serialize, variant } from "@dao-xyz/borsh"; import { sha256Base64Sync } from "./hash.js"; import { PeerId } from "@libp2p/interface/peer-id"; import { compare } from "@peerbit/uint8arrays"; import { toHexString } from "./utils"; +import { DecryptProvider, EncryptedThing } from "./encryption.js"; interface Key { equals(other: Key): boolean; @@ -26,6 +27,18 @@ export abstract class Keypair { // TODO: Should we add not implemented errors for .create and and .from as well? } +export abstract class EncryptedKeypair { + equals(other: EncryptedKeypair): boolean { + throw new Error("Not implemented"); + } + + async decrypt(provider: DecryptProvider): Promise { + throw new Error("Not implemented"); + } + + // TODO: Should we add not implemented errors for .create and and .from as well? +} + // ---- SIGNATURE KEYS ----- export interface PublicSignKey extends Key {} export abstract class PublicSignKey implements Key { diff --git a/packages/utils/crypto/src/sepc256k1.ts b/packages/utils/crypto/src/sepc256k1.ts index c55458e41..b54652b74 100644 --- a/packages/utils/crypto/src/sepc256k1.ts +++ b/packages/utils/crypto/src/sepc256k1.ts @@ -1,5 +1,10 @@ import { field, fixedArray, variant, vec } from "@dao-xyz/borsh"; -import { Keypair, PrivateSignKey, PublicSignKey } from "./key.js"; +import { + EncryptedKeypair, + Keypair, + PrivateSignKey, + PublicSignKey +} from "./key.js"; import { Wallet } from "@ethersproject/wallet"; import { arrayify } from "@ethersproject/bytes"; @@ -19,6 +24,13 @@ import utf8 from "@protobufjs/utf8"; import { SignatureWithKey } from "./signature.js"; import { PreHash, prehashFn } from "./prehash.js"; import { peerIdFromKeys } from "@libp2p/peer-id"; +import { + DecryptProvider, + DecryptedThing, + EncryptProvide, + EncryptedThing, + KeyExchangeOptions +} from "./encryption.js"; @variant(1) export class Secp256k1PublicKey extends PublicSignKey { @@ -192,6 +204,53 @@ export class Secp256k1Keypair extends Keypair implements Identity { ).bytes ); } + + async encrypt( + provider: EncryptProvide, + parameters: Parameters + ) { + const decryptedSecret = new DecryptedThing({ value: this.privateKey }); + return new EncryptedSecp256k1Keypair({ + publicKey: this.publicKey, + encryptedPrivateKey: await decryptedSecret.encrypt(provider, parameters) + }); + } +} + +@variant(2) +export class EncryptedSecp256k1Keypair extends EncryptedKeypair { + @field({ type: Secp256k1PublicKey }) + publicKey: Secp256k1PublicKey; + + @field({ type: EncryptedThing }) + encryptedPrivateKey: EncryptedThing; + + constructor(properties: { + publicKey: Secp256k1PublicKey; + encryptedPrivateKey: EncryptedThing; + }) { + super(); + this.encryptedPrivateKey = properties.encryptedPrivateKey; + this.publicKey = properties.publicKey; + } + + equals(other: EncryptedKeypair) { + if (other instanceof EncryptedSecp256k1Keypair) { + return ( + this.publicKey.equals(other.publicKey) && + this.encryptedPrivateKey.equals(other.encryptedPrivateKey) + ); + } + return false; + } + + async decrypt(provider: DecryptProvider) { + const decryptedThing = await this.encryptedPrivateKey.decrypt(provider); + return new Secp256k1Keypair({ + publicKey: this.publicKey, + privateKey: decryptedThing.getValue(Secp256k1PrivateKey) + }); + } } const decoder = new TextDecoder(); diff --git a/packages/utils/crypto/src/x25519.ts b/packages/utils/crypto/src/x25519.ts index 96785cac1..450b76f55 100644 --- a/packages/utils/crypto/src/x25519.ts +++ b/packages/utils/crypto/src/x25519.ts @@ -3,6 +3,7 @@ import { field, fixedArray, variant } from "@dao-xyz/borsh"; import { compare } from "@peerbit/uint8arrays"; import sodium from "libsodium-wrappers"; import { + EncryptedKeypair, Keypair, PrivateEncryptionKey, PublicKeyEncryptionKey @@ -14,6 +15,14 @@ import { } from "./ed25519.js"; import { toHexString } from "./utils.js"; import { PeerId } from "@libp2p/interface/peer-id"; +import { + DecryptProvider, + DecryptedThing, + EncryptProvide, + EncryptedThing, + KeyExchangeOptions +} from "./encryption.js"; +import { Secp256k1Keypair } from "./sepc256k1.js"; @variant(0) export class X25519PublicKey extends PublicKeyEncryptionKey { @field({ type: fixedArray("u8", 32) }) @@ -141,10 +150,18 @@ export class X25519Keypair extends Keypair { return kp; } - static async from(ed25119Keypair: Ed25519Keypair): Promise { + static async from( + keypair: Ed25519Keypair | X25519Keypair + ): Promise { const kp = new X25519Keypair({ - publicKey: await X25519PublicKey.from(ed25119Keypair.publicKey), - secretKey: await X25519SecretKey.from(ed25119Keypair) + publicKey: + keypair instanceof X25519Keypair + ? keypair.publicKey + : await X25519PublicKey.from(keypair.publicKey), + secretKey: + keypair instanceof X25519Keypair + ? keypair.secretKey + : await X25519SecretKey.from(keypair) }); return kp; } @@ -164,4 +181,51 @@ export class X25519Keypair extends Keypair { } return false; } + + async encrypt( + provider: EncryptProvide, + parameters: Parameters + ) { + const decryptedSecret = new DecryptedThing({ value: this.secretKey }); + return new EncryptedX25519Keypair({ + publicKey: this.publicKey, + encryptedSecretKey: await decryptedSecret.encrypt(provider, parameters) + }); + } +} + +@variant(0) +export class EncryptedX25519Keypair extends EncryptedKeypair { + @field({ type: X25519PublicKey }) + publicKey: X25519PublicKey; + + @field({ type: EncryptedThing }) + encryptedSecretKey: EncryptedThing; + + constructor(properties: { + publicKey: X25519PublicKey; + encryptedSecretKey: EncryptedThing; + }) { + super(); + this.publicKey = properties.publicKey; + this.encryptedSecretKey = properties.encryptedSecretKey; + } + + equals(other: EncryptedKeypair) { + if (other instanceof EncryptedX25519Keypair) { + return ( + this.publicKey.equals(other.publicKey) && + this.encryptedSecretKey.equals(other.encryptedSecretKey) + ); + } + return false; + } + + async decrypt(provider: DecryptProvider) { + const decryptedThing = await this.encryptedSecretKey.decrypt(provider); + return new X25519Keypair({ + publicKey: this.publicKey, + secretKey: decryptedThing.getValue(X25519SecretKey) + }); + } }