Skip to content

Commit

Permalink
first batch of keychain program implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
benjaminpreiss committed Oct 9, 2023
1 parent 12c5981 commit 9a9f0f3
Show file tree
Hide file tree
Showing 7 changed files with 432 additions and 13 deletions.
1 change: 1 addition & 0 deletions packages/programs/program/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export {
export * from "./client.js";
export * from "./program.js";
export * from "./address.js";
export * from "./keychain.js";
219 changes: 219 additions & 0 deletions packages/programs/program/src/keychain.ts
Original file line number Diff line number Diff line change
@@ -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<EncryptedExtendedKey> })
keys: Documents<EncryptedExtendedKey>;

@field({ type: Identity })
identity: Identity;

@field({ type: EncryptProvide })
encryptProvider: EncryptProvide<KeyExchangeOptions>;

@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<KeyExchangeOptions>;
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<T>(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<T>(encryptedThing: EncryptedThing<T>, clazz: AbstractType<T>) {
const keyResolver = async <PublicKey extends X25519PublicKey | Uint8Array>(
key: X25519PublicKey | Uint8Array
) => {
// TODO: fetch keys here based on key parameter and return them
if (key instanceof X25519PublicKey)
return X25519Keypair.create() as Promise<
KeyResolverReturnType<PublicKey>
>;
return undefined;
};
const decryptionProvider = createDecrypterFromKeyResolver(keyResolver);
const decryptedThing = await encryptedThing.decrypt(decryptionProvider);
return decryptedThing.getValue(clazz);
}
}
61 changes: 60 additions & 1 deletion packages/utils/crypto/src/ed25519.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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 {
Expand Down Expand Up @@ -191,4 +203,51 @@ export class Ed25519Keypair extends Keypair implements Identity {
).bytes
);
}

async encrypt<Parameters extends KeyExchangeOptions>(
provider: EncryptProvide<Parameters>,
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<Ed25519PrivateKey> })
encryptedPrivateKey: EncryptedThing<Ed25519PrivateKey>;

constructor(properties: {
publicKey: Ed25519PublicKey;
encryptedPrivateKey: EncryptedThing<Ed25519PrivateKey>;
}) {
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)
});
}
}
18 changes: 11 additions & 7 deletions packages/utils/crypto/src/encryption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ type EnvelopeFromParameter<Parameters extends KeyExchangeOptions> =
? PublicKeyEnvelope
: HashedKeyEnvelope;

type EncryptProvide<Parameters extends KeyExchangeOptions> = (
export type EncryptProvide<Parameters extends KeyExchangeOptions> = (
bytes: Uint8Array,
parameters: Parameters
) => Promise<CipherWithEnvelope<EnvelopeFromParameter<Parameters>>>;
Expand Down Expand Up @@ -143,18 +143,22 @@ export const createLocalEncryptProvider = <
};
};

type DecryptProvider = (
export type DecryptProvider = (
encrypted: Uint8Array,
nonce: Uint8Array,
exchange: Envelope
) => Promise<Uint8Array>;

type KeyResolver = <PublicKey extends X25519PublicKey | Uint8Array>(
key: PublicKey
) =>
export type KeyResolverReturnType<
PublicKey extends X25519PublicKey | Uint8Array
> =
| (PublicKey extends X25519PublicKey ? X25519Keypair : Uint8Array)
| undefined;

export type KeyResolver = <PublicKey extends X25519PublicKey | Uint8Array>(
key: PublicKey
) => Promise<KeyResolverReturnType<PublicKey>>;

export const createDecrypterFromKeyResolver = (
keyResolver: KeyResolver
): DecryptProvider => {
Expand All @@ -174,7 +178,7 @@ export const createDecrypterFromKeyResolver = (
if (exported) {
key = {
index: i,
keypair: exported
keypair: await exported
};
break;
}
Expand Down Expand Up @@ -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) {
Expand Down
15 changes: 14 additions & 1 deletion packages/utils/crypto/src/key.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<Keypair> {
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 {
Expand Down
Loading

0 comments on commit 9a9f0f3

Please sign in to comment.