diff --git a/README.md b/README.md
index d00340b71..5171b72ce 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,3 @@
-
@@ -26,38 +25,39 @@ Your database schema can remain very simple but still utilize P2P networks, auto
## Optimized for performance
-Peerbit is performant, so performant in fact you can use it for [streaming video](https://stream.dao.xyz) by having peers subscribing to database updates. In a low latency setting, you can achieve around 1000 replications a second and have a thoughput of 100 MB/s.
+Peerbit is performant, so performant in fact you can use it for [streaming video](https://stream.dao.xyz) by having peers subscribing to database updates. In a low latency setting, you can achieve around 1000 replications a second and have a thoughput of 100 MB/s.
![Dogestream](/docs/videostream.gif)
-
## Other examples
### [Chat room](https://github.com/dao-xyz/peerbit-examples/tree/master/packages/one-chat-room/)
+
[](https://github.com/dao-xyz/peerbit-examples/tree/master/packages/one-chat-room/)
### [Lobby + chat rooms](https://github.com/dao-xyz/peerbit-examples/tree/master/packages/many-chat-rooms/)
+
[](https://github.com/dao-xyz/peerbit-examples/tree/master/packages/many-chat-rooms/)
### [Sync files](https://github.com/dao-xyz/peerbit-examples/tree/master/packages/file-share/)
+
#### [React app](https://github.com/dao-xyz/peerbit-examples/tree/master/packages/file-share/)
+
[](https://github.com/dao-xyz/peerbit-examples/tree/master/packages/file-share/)
#### [CLI](https://github.com/dao-xyz/peerbit-examples/tree/master/packages/file-share/)
-[](https://github.com/dao-xyz/peerbit-examples/tree/master/packages/file-share/)
-
+[](https://github.com/dao-xyz/peerbit-examples/tree/master/packages/file-share/)
### [Collaborative machine learning](https://github.com/dao-xyz/peerbit-examples/tree/master/packages/collaborative-learning/)
-[](https://github.com/dao-xyz/peerbit-examples/tree/master/packages/collaborative-learning/)
-
+[](https://github.com/dao-xyz/peerbit-examples/tree/master/packages/collaborative-learning/)
## Get Started
1. Install Peerbit by following the simple setup instructions in our [Installation Guide](https://peerbit.org/#/getting-started).
-2. Dive into our comprehensive [Documentation](https://peerbit.org/#/modules/client/) or checkout the [Example repository](https://github.com/dao-xyz/peerbit-examples) to explore the powerful features and learn how to leverage Peerbit to its fullest potential.
+2. Dive into our comprehensive [Documentation](https://peerbit.org/#/modules/client/) or checkout the [Example repository](https://github.com/dao-xyz/peerbit-examples) to explore the powerful features and learn how to leverage Peerbit to its fullest potential.
3. Join us on [Matrix](https://matrix.to/#/#peerbit:matrix.org) to connect, share ideas, and collaborate with like-minded individuals.
@@ -65,9 +65,27 @@ Peerbit is performant, so performant in fact you can use it for [streaming video
Peerbit is an open-source project, and we welcome contributions from developers like you! Feel free to contribute code, report issues, and submit feature requests. Together, let's shape the future of Peerbit.
+IMPORTANT: Peerbit uses yarn.
+
+1. Check yarn version: `yarn -v` should print 1.something
+2. Install: `yarn`
+3. Build: `yarn build`
+4. Run tests: `yarn test`
+
+You might possibly need to CMD + Shift + P and then enter to restart the typescript server after the build step.
+
+To create a new package, follow the following steps:
+
+1. Clone the time folder within /packages/utils/time to the desired destination and rename it
+2. Update the package.json `name`, `description`, `version` fields
+3. Possibly add other depencencies to the package.json `dependencies` field (like `@peerbit/crypto`)
+4. Delete contents in CHANGELOG.md
+5. Update the root package.json `workspaces.packages` field
+6. Update root lerna.json `workspaces.packages` field
+7. run yarn once in root
+
+We recommend running tests with the VS Code integration though: https://marketplace.visualstudio.com/items?itemName=firsttris.vscode-jest-runner
## Let's Get Coding!
[peerbit.org](https://peerbit.org)
-
-
diff --git a/lerna.json b/lerna.json
index 8516622f0..7f070f676 100644
--- a/lerna.json
+++ b/lerna.json
@@ -34,7 +34,8 @@
"packages/utils/uint8arrays",
"packages/utils/cache",
"packages/utils/time",
- "packages/utils/logger"
+ "packages/utils/logger",
+ "packages/utils/keychain"
],
"version": "independent",
"npmClient": "yarn"
diff --git a/package.json b/package.json
index 1c501481e..10fcd3017 100644
--- a/package.json
+++ b/package.json
@@ -46,7 +46,8 @@
"packages/utils/uint8arrays",
"packages/utils/time",
"packages/utils/cache",
- "packages/utils/logger"
+ "packages/utils/logger",
+ "packages/utils/keychain"
]
},
"engines": {
diff --git a/packages/transport/stream/src/__tests__/stream.test.ts b/packages/transport/stream/src/__tests__/stream.test.ts
index 844405988..cabf3aefb 100644
--- a/packages/transport/stream/src/__tests__/stream.test.ts
+++ b/packages/transport/stream/src/__tests__/stream.test.ts
@@ -814,6 +814,73 @@ describe("streams", function () {
});
it("through relay if fails", async () => {
+ const dialFn =
+ streams[0].stream.components.connectionManager.openConnection.bind(
+ streams[0].stream.components.connectionManager
+ );
+
+ let directlyDialded = false;
+ const filteredDial = (address: PeerId | Multiaddr | Multiaddr[]) => {
+ if (
+ isPeerId(address) &&
+ address.toString() === streams[3].stream.peerIdStr
+ ) {
+ throw new Error("Mock fail"); // don't allow connect directly
+ }
+
+ let addresses: Multiaddr[] = Array.isArray(address)
+ ? address
+ : [address as Multiaddr];
+ for (const a of addresses) {
+ if (
+ !a.protoNames().includes("p2p-circuit") &&
+ a.toString().includes(streams[3].stream.peerIdStr)
+ ) {
+ throw new Error("Mock fail"); // don't allow connect directly
+ }
+ }
+ addresses = addresses.map((x) =>
+ x.protoNames().includes("p2p-circuit")
+ ? multiaddr(x.toString().replace("/webrtc/", "/"))
+ : x
+ ); // TODO use webrtc in node
+
+ directlyDialded = true;
+ return dialFn(addresses);
+ };
+
+ streams[0].stream.components.connectionManager.openConnection =
+ filteredDial;
+ expect(streams[0].stream.peers.size).toEqual(1);
+ await streams[0].stream.publish(data, {
+ to: [streams[3].stream.components.peerId]
+ });
+ await waitFor(() => streams[3].received.length === 1);
+ await waitForResolved(() => expect(directlyDialded).toBeTrue());
+ });
+
+ it("tries multiple relays", async () => {
+ await session.connect([[session.peers[1], session.peers[3]]]);
+ await waitForPeerStreams(streams[1].stream, streams[3].stream);
+
+ /*
+ ┌───┐
+ │ 0 │
+ └┬─┬┘
+ │┌▽┐
+ ││1│
+ │└┬┘
+ ┌▽┐│
+ │2││
+ └┬┘│
+ ┌▽─▽─┐
+ │ 3 │
+ └────┘
+
+ */
+
+ const dialedCircuitRelayAddresses: Set = new Set();
+
const dialFn =
streams[0].stream.components.connectionManager.openConnection.bind(
streams[0].stream.components.connectionManager
@@ -837,22 +904,32 @@ describe("streams", function () {
throw new Error("Mock fail"); // don't allow connect directly
}
}
- const q = 123;
+ addresses
+ .filter((x) => x.protoNames().includes("p2p-circuit"))
+ .forEach((x) => {
+ dialedCircuitRelayAddresses.add(x.toString());
+ });
addresses = addresses.map((x) =>
- x.protoCodes().includes(281)
+ x.protoNames().includes("p2p-circuit")
? multiaddr(x.toString().replace("/webrtc/", "/"))
: x
); // TODO use webrtc in node
+
+ if (dialedCircuitRelayAddresses.size === 1) {
+ throw new Error("Mock fail"); // only succeed with the dial once we have tried two unique addresses (both neighbors)
+ }
return dialFn(addresses);
};
streams[0].stream.components.connectionManager.openConnection =
filteredDial;
+
expect(streams[0].stream.peers.size).toEqual(1);
await streams[0].stream.publish(data, {
to: [streams[3].stream.components.peerId]
});
await waitFor(() => streams[3].received.length === 1);
+ expect(dialedCircuitRelayAddresses.size).toEqual(2);
});
});
diff --git a/packages/transport/stream/src/index.ts b/packages/transport/stream/src/index.ts
index 9ea7bedb0..1c66889a7 100644
--- a/packages/transport/stream/src/index.ts
+++ b/packages/transport/stream/src/index.ts
@@ -1412,7 +1412,7 @@ export abstract class DirectStream<
) {
// Dont await this even if it is async since this method can fail
// and might take some time to run
- this.maybeConnectDirectly(path).catch((e) => {
+ this.maybeConnectDirectly(to).catch((e) => {
logger.error(
"Failed to request direct connection: " + e.message
);
@@ -1499,13 +1499,7 @@ export abstract class DirectStream<
}
}
- async maybeConnectDirectly(path: string[]) {
- if (path.length < 3) {
- return;
- }
-
- const toHash = path[path.length - 1];
-
+ async maybeConnectDirectly(toHash: string) {
if (this.peers.has(toHash)) {
return; // TODO, is this expected, or are we to dial more addresses?
}
@@ -1527,49 +1521,51 @@ export abstract class DirectStream<
}
// Connect through a closer relay that maybe does holepunch for us
- const nextToHash = path[path.length - 2];
- const routeKey = nextToHash + toHash;
- if (!this.recentDials.has(routeKey)) {
- this.recentDials.add(routeKey);
- const to = this.peerKeyHashToPublicKey.get(toHash)! as Ed25519PublicKey;
- const toPeerId = await to.toPeerId();
- const addrs = this.multiaddrsMap.get(path[path.length - 2]);
- if (addrs && addrs.length > 0) {
- const addressesToDial = addrs.sort((a, b) => {
- if (a.includes("/wss/")) {
- if (b.includes("/wss/")) {
- return 0;
- }
- return -1;
- }
- if (a.includes("/ws/")) {
- if (b.includes("/ws/")) {
- return 0;
+ const neighbours = this.routes.graph.neighbors(toHash);
+ outer: for (const neighbour of neighbours) {
+ const routeKey = neighbour + toHash;
+ if (!this.recentDials.has(routeKey)) {
+ this.recentDials.add(routeKey);
+ const to = this.peerKeyHashToPublicKey.get(toHash)! as Ed25519PublicKey;
+ const toPeerId = await to.toPeerId();
+ const addrs = this.multiaddrsMap.get(neighbour);
+ if (addrs && addrs.length > 0) {
+ const addressesToDial = addrs.sort((a, b) => {
+ if (a.includes("/wss/")) {
+ if (b.includes("/wss/")) {
+ return 0;
+ }
+ return -1;
}
- if (b.includes("/wss/")) {
- return 1;
+ if (a.includes("/ws/")) {
+ if (b.includes("/ws/")) {
+ return 0;
+ }
+ if (b.includes("/wss/")) {
+ return 1;
+ }
+ return -1;
}
- return -1;
- }
- return 0;
- });
+ return 0;
+ });
- for (const addr of addressesToDial) {
- const circuitAddress = multiaddr(
- addr + "/p2p-circuit/webrtc/p2p/" + toPeerId.toString()
- );
- try {
- await this.components.connectionManager.openConnection(
- circuitAddress
- );
- return;
- } catch (error: any) {
- logger.error(
- "Failed to connect directly to: " +
- circuitAddress.toString() +
- ". " +
- error?.message
+ for (const addr of addressesToDial) {
+ const circuitAddress = multiaddr(
+ addr + "/p2p-circuit/webrtc/p2p/" + toPeerId.toString()
);
+ try {
+ await this.components.connectionManager.openConnection(
+ circuitAddress
+ );
+ break outer; // We succeeded! that means we dont have to try anymore
+ } catch (error: any) {
+ logger.warn(
+ "Failed to connect directly to: " +
+ circuitAddress.toString() +
+ ". " +
+ error?.message
+ );
+ }
}
}
}
diff --git a/packages/utils/any-store/src/interface.ts b/packages/utils/any-store/src/interface.ts
index f14efbe2f..0bc9754cd 100644
--- a/packages/utils/any-store/src/interface.ts
+++ b/packages/utils/any-store/src/interface.ts
@@ -5,7 +5,7 @@ export interface AnyStore {
close(): MaybePromise;
open(): MaybePromise;
get(key: string): MaybePromise;
- put(key: string, value: Uint8Array);
+ put(key: string, value: Uint8Array): MaybePromise;
del(key): MaybePromise;
sublevel(name: string): MaybePromise;
iterator: () => {
diff --git a/packages/utils/any-store/src/memory.ts b/packages/utils/any-store/src/memory.ts
index ca96c1c08..a00f13f06 100644
--- a/packages/utils/any-store/src/memory.ts
+++ b/packages/utils/any-store/src/memory.ts
@@ -42,7 +42,7 @@ export class MemoryStore implements AnyStore {
}
put(key: string, value: Uint8Array) {
- return this.store.set(key, value);
+ this.store.set(key, value);
}
// Remove a value and key from the cache
diff --git a/packages/utils/crypto/src/__tests__/encryption.test.ts b/packages/utils/crypto/src/__tests__/encryption.test.ts
index d510529e0..5cf574c75 100644
--- a/packages/utils/crypto/src/__tests__/encryption.test.ts
+++ b/packages/utils/crypto/src/__tests__/encryption.test.ts
@@ -1,21 +1,11 @@
import {
DecryptedThing,
X25519Keypair,
- Keychain,
- PublicSignKey
+ createDecrypterFromKeyResolver,
+ createLocalEncryptProvider
} from "../index.js";
describe("encryption", function () {
- const keychain = (keypair: X25519Keypair): Keychain => {
- return {
- exportById: async (id: Uint8Array) => undefined,
- exportByKey: async (publicKey: T) =>
- publicKey.equals(keypair.publicKey) ? (keypair as Q) : undefined,
- import: (keypair: any, id: Uint8Array) => {
- throw new Error("No implemented+");
- }
- };
- };
it("encrypt", async () => {
const senderKey = await X25519Keypair.create();
const receiverKey1 = await X25519Keypair.create();
@@ -26,14 +16,27 @@ describe("encryption", function () {
data
});
- const receiver1Config = keychain(receiverKey1);
- const receiver2Config = keychain(receiverKey2);
+ const receiver1Config = createDecrypterFromKeyResolver(
+ () => receiverKey1 as any
+ );
+ const receiver2Config = createDecrypterFromKeyResolver(
+ () => receiverKey2 as any
+ );
const encrypted = await decrypted.encrypt(
- senderKey,
- receiverKey1.publicKey,
- receiverKey2.publicKey
+ createLocalEncryptProvider(new Uint8Array([1, 2, 3])),
+ {
+ receiverPublicKeys: [receiverKey1.publicKey, receiverKey2.publicKey]
+ }
);
+
+ /* const encrypted = await decrypted.encrypt(
+ createLocalEncryptProvider(new Uint8Array(32)),
+ {
+ type: 'symmetric'
+ },
+ ); */
+
encrypted._decrypted = undefined;
const decryptedFromEncrypted1 = await encrypted.decrypt(receiver1Config);
diff --git a/packages/utils/crypto/src/encryption.ts b/packages/utils/crypto/src/encryption.ts
index e57507593..109502224 100644
--- a/packages/utils/crypto/src/encryption.ts
+++ b/packages/utils/crypto/src/encryption.ts
@@ -1,19 +1,218 @@
export * from "./errors.js";
+
import {
AbstractType,
deserialize,
field,
serialize,
variant,
- vec
+ vec,
+ fixedArray
} from "@dao-xyz/borsh";
import { equals } from "@peerbit/uint8arrays";
import { AccessError } from "./errors.js";
import sodium from "libsodium-wrappers";
import { X25519Keypair, X25519PublicKey, X25519SecretKey } from "./x25519.js";
-import { Ed25519Keypair, Ed25519PublicKey } from "./ed25519.js";
+import { Ed25519PublicKey } from "./ed25519.js";
import { randomBytes } from "./random.js";
-import { Keychain } from "./keychain.js";
+import { sha256 } from "./hash.js";
+
+export type PublicKeyEncryptionParameters = {
+ type?: "publicKey";
+ receiverPublicKeys: (X25519PublicKey | Ed25519PublicKey)[];
+};
+
+export type SymmetricKeyEncryptionParameters = {
+ type?: "hash";
+};
+/*
+export type NoExchange = {
+ type: 'none'
+};
+ */
+
+export type KeyExchangeOptions =
+ | PublicKeyEncryptionParameters
+ | SymmetricKeyEncryptionParameters;
+
+type EncryptReturnValue<
+ T,
+ Parameters extends KeyExchangeOptions
+> = EncryptedThing>;
+
+type CipherWithEnvelope = {
+ cipher: Uint8Array;
+ nonce: Uint8Array;
+ envelope: E;
+};
+
+type SymmetricKeys = Uint8Array;
+type PublicKeyEncryptionKeys = X25519Keypair;
+
+function isAsymmetriEncryptionParameters(
+ parameters: KeyExchangeOptions
+): parameters is PublicKeyEncryptionParameters {
+ return (
+ (parameters as PublicKeyEncryptionParameters).receiverPublicKeys != null
+ );
+}
+function isAsymmetricEncryptionKeys(
+ parameters: PublicKeyEncryptionKeys | SymmetricKeys
+): parameters is PublicKeyEncryptionKeys {
+ return (parameters as PublicKeyEncryptionKeys) instanceof X25519Keypair;
+}
+
+type EnvelopeFromParameter =
+ Parameters extends PublicKeyEncryptionParameters
+ ? PublicKeyEnvelope
+ : HashedKeyEnvelope;
+
+type EncryptProvide = (
+ bytes: Uint8Array,
+ parameters: Parameters
+) => Promise>>;
+
+export const createLocalEncryptProvider = <
+ K extends PublicKeyEncryptionKeys | SymmetricKeys,
+ Parameters extends KeyExchangeOptions = K extends PublicKeyEncryptionKeys
+ ? PublicKeyEncryptionParameters
+ : SymmetricKeyEncryptionParameters
+>(
+ keys: K
+) => {
+ return async (
+ bytes: Uint8Array,
+ parameters: Parameters
+ ): Promise>> => {
+ const nonce = randomBytes(NONCE_LENGTH); // crypto random is faster than sodim random
+ if (
+ isAsymmetriEncryptionParameters(parameters) &&
+ isAsymmetricEncryptionKeys(keys)
+ ) {
+ const epheremalKey = sodium.crypto_secretbox_keygen();
+ const cipher = sodium.crypto_secretbox_easy(bytes, nonce, epheremalKey);
+ const { receiverPublicKeys } = parameters;
+ const receiverX25519PublicKeys = await Promise.all(
+ receiverPublicKeys.map((key) => {
+ if (key instanceof Ed25519PublicKey) {
+ return X25519PublicKey.from(key);
+ }
+ return key;
+ })
+ );
+
+ const ks = receiverX25519PublicKeys.map((receiverPublicKey) => {
+ const kNonce = randomBytes(NONCE_LENGTH); // crypto random is faster than sodium random
+ return new K({
+ encryptedKey: new CipherWithNonce({
+ cipher: sodium.crypto_box_easy(
+ epheremalKey,
+ kNonce,
+ receiverPublicKey.publicKey,
+ keys.secretKey.secretKey
+ ),
+ nonce: kNonce
+ }),
+ receiverPublicKey
+ });
+ });
+
+ return {
+ cipher: new Uint8Array(cipher), // TODO do we need this clone?
+ nonce,
+ envelope: new PublicKeyEnvelope({
+ senderPublicKey: keys.publicKey,
+ ks
+ }) as EnvelopeFromParameter
+ };
+ } else if (
+ !isAsymmetriEncryptionParameters(parameters) &&
+ !isAsymmetricEncryptionKeys(keys)
+ ) {
+ const cipher = sodium.crypto_secretbox_easy(bytes, nonce, keys);
+ return {
+ cipher: new Uint8Array(cipher), // TODO do we need this clone?
+ nonce,
+ envelope: new HashedKeyEnvelope({
+ hash: await sha256(keys)
+ }) as EnvelopeFromParameter
+ };
+ }
+
+ throw new Error("Unexpected encryption parameters");
+ };
+};
+
+type DecryptProvider = (
+ encrypted: Uint8Array,
+ nonce: Uint8Array,
+ exchange: Envelope
+) => Promise;
+
+type KeyResolver = (
+ key: PublicKey
+) =>
+ | (PublicKey extends X25519PublicKey ? X25519Keypair : Uint8Array)
+ | undefined;
+
+export const createDecrypterFromKeyResolver = (
+ keyResolver: KeyResolver
+): DecryptProvider => {
+ return async (
+ encrypted: Uint8Array,
+ nonce: Uint8Array,
+ exchange: Envelope
+ ): Promise => {
+ // We only need to open with one of the keys
+
+ let epheremalKey: Uint8Array | undefined;
+
+ if (exchange instanceof PublicKeyEnvelope) {
+ let key: { index: number; keypair: X25519Keypair } | undefined;
+ for (const [i, k] of exchange._ks.entries()) {
+ const exported = keyResolver(k._receiverPublicKey);
+ if (exported) {
+ key = {
+ index: i,
+ keypair: exported
+ };
+ break;
+ }
+ }
+
+ if (key) {
+ const k = exchange[key.index];
+ let secretKey: X25519SecretKey = undefined as any;
+ if (key.keypair instanceof X25519Keypair) {
+ secretKey = key.keypair.secretKey;
+ } else {
+ secretKey = await X25519SecretKey.from(key.keypair);
+ }
+ let epheremalKey: Uint8Array;
+ try {
+ epheremalKey = sodium.crypto_box_open_easy(
+ k._encryptedKey.cipher,
+ k._encryptedKey.nonce,
+ exchange._senderPublicKey.publicKey,
+ secretKey.secretKey
+ );
+ } catch (error) {
+ throw new AccessError("Failed to decrypt");
+ }
+ } else {
+ throw new AccessError("Failed to resolve decryption key");
+ }
+ } else if (exchange instanceof HashedKeyEnvelope) {
+ epheremalKey = keyResolver(exchange.hash);
+ }
+
+ if (!epheremalKey) {
+ throw new Error("Failed to resolve ephemeral key");
+ }
+
+ return sodium.crypto_secretbox_open_easy(encrypted, nonce, epheremalKey);
+ };
+};
const NONCE_LENGTH = 24;
@@ -27,7 +226,7 @@ export abstract class MaybeEncrypted {
}
decrypt(
- keyOrKeychain?: Keychain | X25519Keypair
+ provider?: DecryptProvider
): Promise> | DecryptedThing {
throw new Error("Not implemented");
}
@@ -69,47 +268,16 @@ export class DecryptedThing extends MaybeEncrypted {
return deserialize(this._data, clazz);
}
- async encrypt(
- x25519Keypair: X25519Keypair,
- ...receiverPublicKeys: (X25519PublicKey | Ed25519PublicKey)[]
- ): Promise> {
+ async encrypt(
+ provider: EncryptProvide,
+ parameters: Parameters
+ ): Promise> {
const bytes = serialize(this);
- const epheremalKey = sodium.crypto_secretbox_keygen();
- const nonce = randomBytes(NONCE_LENGTH); // crypto random is faster than sodim random
- const cipher = sodium.crypto_secretbox_easy(bytes, nonce, epheremalKey);
-
- const receiverX25519PublicKeys = await Promise.all(
- receiverPublicKeys.map((key) => {
- if (key instanceof Ed25519PublicKey) {
- return X25519PublicKey.from(key);
- }
- return key;
- })
- );
-
- const ks = receiverX25519PublicKeys.map((receiverPublicKey) => {
- const kNonce = randomBytes(NONCE_LENGTH); // crypto random is faster than sodium random
- return new K({
- encryptedKey: new CipherWithNonce({
- cipher: sodium.crypto_box_easy(
- epheremalKey,
- kNonce,
- receiverPublicKey.publicKey,
- x25519Keypair.secretKey.secretKey
- ),
- nonce: kNonce
- }),
- receiverPublicKey
- });
- });
-
- const enc = new EncryptedThing({
- encrypted: new Uint8Array(cipher),
- nonce,
- envelope: new Envelope({
- senderPublicKey: x25519Keypair.publicKey,
- ks
- })
+ const { cipher, envelope, nonce } = await provider(bytes, parameters);
+ const enc = new EncryptedThing>({
+ encrypted: cipher,
+ envelope,
+ nonce
});
enc._decrypted = this;
return enc;
@@ -196,8 +364,12 @@ export class K {
}
}
+abstract class Envelope {
+ abstract equals(other: Envelope): boolean;
+}
+
@variant(0)
-export class Envelope {
+class PublicKeyEnvelope extends Envelope {
@field({ type: X25519PublicKey })
_senderPublicKey: X25519PublicKey;
@@ -205,14 +377,16 @@ export class Envelope {
_ks: K[];
constructor(props?: { senderPublicKey: X25519PublicKey; ks: K[] }) {
+ super();
if (props) {
this._senderPublicKey = props.senderPublicKey;
this._ks = props.ks;
}
}
- equals(other: Envelope): boolean {
- if (other instanceof Envelope) {
+ // TODO: should this be comparable to AbstractEnvelope?
+ equals(other: PublicKeyEnvelope): boolean {
+ if (other instanceof PublicKeyEnvelope) {
if (!this._senderPublicKey.equals(other._senderPublicKey)) {
return false;
}
@@ -233,7 +407,35 @@ export class Envelope {
}
@variant(1)
-export class EncryptedThing extends MaybeEncrypted {
+class HashedKeyEnvelope extends Envelope {
+ @field({ type: fixedArray("u8", 32) })
+ hash: Uint8Array;
+ // TODO: Do we need a salt here?
+ constructor(props?: { hash: Uint8Array }) {
+ super();
+ if (props) {
+ this.hash = props.hash;
+ }
+ }
+
+ // TODO: should this be comparable to AbstractEnvelope?
+ equals(other: HashedKeyEnvelope): boolean {
+ if (other instanceof HashedKeyEnvelope) {
+ if (!equals(this.hash, other.hash)) {
+ return false;
+ }
+ return true;
+ } else {
+ return false;
+ }
+ }
+}
+
+@variant(1)
+export class EncryptedThing<
+ T,
+ E extends Envelope = PublicKeyEnvelope | HashedKeyEnvelope
+> extends MaybeEncrypted {
@field({ type: Uint8Array })
_encrypted: Uint8Array;
@@ -241,18 +443,18 @@ export class EncryptedThing extends MaybeEncrypted {
_nonce: Uint8Array;
@field({ type: Envelope })
- _envelope: Envelope;
+ _keyexchange: E;
constructor(props?: {
encrypted: Uint8Array;
nonce: Uint8Array;
- envelope: Envelope;
+ envelope: E;
}) {
super();
if (props) {
this._encrypted = props.encrypted;
this._nonce = props.nonce;
- this._envelope = props.envelope;
+ this._keyexchange = props.envelope;
}
}
@@ -266,86 +468,28 @@ export class EncryptedThing extends MaybeEncrypted {
return this._decrypted;
}
- async decrypt(
- keyResolver?: Keychain | X25519Keypair
- ): Promise> {
+ async decrypt(provider?: DecryptProvider): Promise> {
if (this._decrypted) {
return this._decrypted;
}
- if (!keyResolver) {
- throw new AccessError("Expecting key resolver");
- }
-
- // We only need to open with one of the keys
- let key: { index: number; keypair: X25519Keypair } | undefined;
- if (keyResolver instanceof X25519Keypair) {
- for (const [i, k] of this._envelope._ks.entries()) {
- if (k._receiverPublicKey.equals(keyResolver.publicKey)) {
- key = {
- index: i,
- keypair: keyResolver
- };
- }
- }
- } else {
- for (const [i, k] of this._envelope._ks.entries()) {
- const exported = await keyResolver.exportByKey(k._receiverPublicKey);
- if (exported) {
- key = {
- index: i,
- keypair: exported
- };
- break;
- }
- }
+ if (!provider) {
+ throw new AccessError("Expecting decryption provider");
}
- if (key) {
- const k = this._envelope._ks[key.index];
- let secretKey: X25519SecretKey = undefined as any;
- if (key.keypair instanceof X25519Keypair) {
- secretKey = key.keypair.secretKey;
- } else {
- secretKey = await X25519SecretKey.from(key.keypair);
- }
- let epheremalKey: Uint8Array;
- try {
- epheremalKey = sodium.crypto_box_open_easy(
- k._encryptedKey.cipher,
- k._encryptedKey.nonce,
- this._envelope._senderPublicKey.publicKey,
- secretKey.secretKey
- );
- } catch (error) {
- throw new AccessError("Failed to decrypt");
- }
+ const decrypted = await provider(
+ this._encrypted,
+ this._nonce,
+ this._keyexchange
+ );
+ if (decrypted) {
+ const der = deserialize(decrypted, DecryptedThing);
- // TODO: is nested decryption necessary?
- /* let der: any = this;
- let counter = 0;
- while (der instanceof EncryptedThing) {
- const decrypted = await sodium.crypto_secretbox_open_easy(this._encrypted, this._nonce, epheremalKey);
- der = deserialize(decrypted, DecryptedThing)
- counter += 1;
- if (counter >= 10) {
- throw new Error("Unexpected decryption behaviour, data seems to always be in encrypted state")
- }
- } */
-
- const der = deserialize(
- sodium.crypto_secretbox_open_easy(
- this._encrypted,
- this._nonce,
- epheremalKey
- ),
- DecryptedThing
- );
this._decrypted = der as DecryptedThing;
- } else {
- throw new AccessError("Failed to resolve decryption key");
+ return this._decrypted;
}
- return this._decrypted;
+
+ throw new AccessError("Failed to resolve decryption key");
}
equals(other: MaybeEncrypted): boolean {
@@ -357,7 +501,7 @@ export class EncryptedThing extends MaybeEncrypted {
return false;
}
- if (!this._envelope.equals(other._envelope)) {
+ if (!this._keyexchange.equals(other._keyexchange)) {
return false;
}
return true;
diff --git a/packages/utils/crypto/src/index.ts b/packages/utils/crypto/src/index.ts
index d81825a16..22389dee4 100644
--- a/packages/utils/crypto/src/index.ts
+++ b/packages/utils/crypto/src/index.ts
@@ -1,7 +1,6 @@
export * from "./key.js";
export * from "./ed25519.js";
export * from "./signature.js";
-export * from "./key.js";
export * from "./sepc256k1.js";
export * from "./x25519.js";
export * from "./encryption.js";
@@ -11,7 +10,7 @@ export * from "./hash.js";
export * from "./random.js";
export * from "./prehash.js";
export * from "./signer.js";
-export * from "./keychain.js";
+export * from "./xsalsa20poly1305.js";
import libsodium from "libsodium-wrappers";
const ready = libsodium.ready; // TODO can we export ready directly ?
export { ready };
diff --git a/packages/utils/crypto/src/key.ts b/packages/utils/crypto/src/key.ts
index 121f658e9..8384ff5a2 100644
--- a/packages/utils/crypto/src/key.ts
+++ b/packages/utils/crypto/src/key.ts
@@ -1,6 +1,8 @@
-import { serialize } from "@dao-xyz/borsh";
+import { field, serialize } 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";
interface Key {
equals(other: Key): boolean;
@@ -20,6 +22,8 @@ export abstract class Keypair {
toPeerId(): Promise {
throw new Error("Not implemented");
}
+
+ // TODO: Should we add not implemented errors for .create and and .from as well?
}
// ---- SIGNATURE KEYS -----
@@ -78,3 +82,22 @@ export abstract class PlainKey implements Key {
return this._hashcode || (this._hashcode = sha256Base64Sync(this.bytes));
}
}
+
+export class ByteKey extends PlainKey {
+ @field({ type: Uint8Array })
+ key: Uint8Array;
+
+ constructor(properties: { key: Uint8Array }) {
+ super();
+ this.key = properties.key;
+ }
+
+ equals(other: ByteKey) {
+ return compare(this.key, other.key) === 0;
+ }
+
+ // TODO: What should be preprended to this string here?
+ toString(): string {
+ return "bytekey/" + toHexString(this.key);
+ }
+}
diff --git a/packages/utils/crypto/src/keychain.ts b/packages/utils/crypto/src/keychain.ts
deleted file mode 100644
index c681a0840..000000000
--- a/packages/utils/crypto/src/keychain.ts
+++ /dev/null
@@ -1,199 +0,0 @@
-import { KeyChain as InternalKeychain } from "@libp2p/interface/keychain";
-import { keysPBM } from "@libp2p/crypto/keys";
-import { identity } from "multiformats/hashes/identity";
-import { base58btc } from "multiformats/bases/base58";
-import { Cache } from "@peerbit/cache";
-import { Ed25519Keypair, Ed25519PublicKey } from "./ed25519.js";
-import { Keypair, PublicSignKey } from "./key.js";
-
-import { KeyInfo } from "@libp2p/interface/keychain";
-import { AccessError, X25519Keypair, X25519PublicKey } from "./x25519.js";
-
-export type KeypairFromPublicKey = T extends X25519PublicKey
- ? X25519PublicKey extends T
- ? X25519Keypair
- : Ed25519Keypair
- : Ed25519Keypair;
-
-export interface Keychain {
- import(keypair: Ed25519Keypair, id: Uint8Array): Promise;
-
- exportByKey<
- T extends Ed25519PublicKey | X25519PublicKey,
- Q = KeypairFromPublicKey
- >(
- publicKey: T
- ): Promise;
-
- exportById<
- T = "ed25519" | "x25519",
- Q = T extends "ed25519" ? Ed25519Keypair : X25519Keypair
- >(
- id: Uint8Array,
- type: T
- ): Promise;
-}
-
-export class Libp2pKeychain implements Keychain {
- constructor(
- readonly keychain: InternalKeychain,
- readonly options?: { cache?: Cache }
- ) {}
-
- keychainKeyIdFromPublicKey(publicKey: X25519PublicKey) {
- const bytes = keysPBM.PublicKey.encode({
- Type: keysPBM.KeyType.Ed25519,
- Data: publicKey.publicKey
- }).subarray();
-
- const encoding = identity.digest(bytes);
- return base58btc.encode(encoding.bytes).substring(1);
- }
-
- private cacheKey(key: Ed25519Keypair | X25519Keypair, id?: Uint8Array) {
- this.options?.cache?.add(base58btc.encode(key.publicKey.bytes), key);
- id && this.options?.cache?.add(base58btc.encode(id), key);
- }
-
- private getCachedById(id: Uint8Array): Ed25519Keypair | null | undefined {
- const key = base58btc.encode(id instanceof PublicSignKey ? id.bytes : id);
- const cached = this.options?.cache?.get(key);
- if (cached === null) {
- return null;
- } else if (!cached) {
- return undefined;
- } else if (cached instanceof Ed25519Keypair) {
- return cached;
- }
- throw new Error("Unexpected cached keypair type: " + key?.constructor.name);
- }
-
- private getCachedByKey<
- T extends X25519PublicKey | Ed25519PublicKey,
- Q = KeypairFromPublicKey
- >(publicKey: T): Q | null | undefined {
- const key = base58btc.encode(publicKey.bytes);
- const cached = this.options?.cache?.get(key);
- if (cached === null) {
- return null;
- } else if (!cached) {
- return undefined;
- } else if (cached instanceof Keypair) {
- return cached as Q;
- }
- throw new Error("Unexpected cached keypair type: " + key?.constructor.name);
- }
-
- exportByKey = async <
- T extends X25519PublicKey | Ed25519PublicKey,
- Q = KeypairFromPublicKey
- >(
- publicKey: T
- ): Promise => {
- const cached = this.getCachedByKey(publicKey);
- if (cached !== undefined) {
- // if null, means key is deleted
- return cached ? cached : undefined;
- }
-
- let keyInfo: KeyInfo | undefined = undefined;
- if (publicKey instanceof Ed25519PublicKey) {
- try {
- keyInfo = await this.keychain.findKeyById(
- (await publicKey.toPeerId()).toString()
- );
- } catch (e: any) {
- if (e.code !== "ERR_KEY_NOT_FOUND") {
- throw e;
- }
- }
- }
-
- if (!keyInfo) {
- try {
- keyInfo = await this.keychain.findKeyByName(
- base58btc.encode(publicKey.bytes)
- );
- } catch (e: any) {
- if (e.code !== "ERR_KEY_NOT_FOUND") {
- throw e;
- }
- }
- }
-
- if (!keyInfo) {
- return undefined;
- }
-
- const peerId = await this.keychain.exportPeerId(keyInfo.name);
-
- return (
- publicKey instanceof X25519PublicKey
- ? X25519Keypair.fromPeerId(peerId)
- : Ed25519Keypair.fromPeerId(peerId)
- ) as Q;
- };
-
- async exportById<
- T = "ed25519" | "x25519",
- Q = T extends "ed25519" ? Ed25519Keypair : X25519Keypair
- >(id: Uint8Array, type: T): Promise {
- const cached = this.getCachedById(id) as Ed25519Keypair | undefined | null;
- if (cached !== undefined) {
- // if null, means key is deleted
- if (type === "x25519" && cached instanceof Ed25519Keypair) {
- return X25519Keypair.from(cached) as Q; // TODO perf, don't do this all the time
- }
- return cached ? (cached as Q) : undefined;
- }
- try {
- const keyInfo = await this.keychain.findKeyByName(base58btc.encode(id));
- const peerId = await this.keychain.exportPeerId(keyInfo.name);
- if (type === "x25519") {
- return X25519Keypair.fromPeerId(peerId) as Q;
- }
- return Ed25519Keypair.fromPeerId(peerId) as Q;
- } catch (e: any) {
- if (e.code !== "ERR_KEY_NOT_FOUND") {
- throw e;
- }
- }
- }
-
- import = async (keypair: Ed25519Keypair, id: Uint8Array) => {
- const receiverKeyPeerId = await keypair.toPeerId();
- this.cacheKey(keypair, id);
-
- // import as ed
- await this.keychain.importPeer(base58btc.encode(id), receiverKeyPeerId);
-
- // import as x so we can decrypt messages with this public key (if received any)
- const xKeypair = await X25519Keypair.from(keypair);
- this.cacheKey(xKeypair);
- await this.keychain.importPeer(
- base58btc.encode(xKeypair.publicKey.bytes),
- receiverKeyPeerId
- );
- };
-
- // Arrow function is used so we can reference this function and use 'this' without .bind(self)
- getAnyKeypair = async (publicKeys) => {
- for (let i = 0; i < publicKeys.length; i++) {
- try {
- const key = await this.exportByKey(publicKeys[i]);
- if (key && key instanceof X25519Keypair) {
- return {
- index: i,
- keypair: key as X25519Keypair
- };
- }
- } catch (error: any) {
- // Key missing
- if (error.code !== "ERR_NOT_FOUND") {
- throw error;
- }
- }
- }
- throw new AccessError("Failed to access key");
- };
-}
diff --git a/packages/utils/crypto/src/xsalsa20poly1305.ts b/packages/utils/crypto/src/xsalsa20poly1305.ts
new file mode 100644
index 000000000..c5d061482
--- /dev/null
+++ b/packages/utils/crypto/src/xsalsa20poly1305.ts
@@ -0,0 +1,40 @@
+import { field, fixedArray, variant } from "@dao-xyz/borsh";
+import { PlainKey } from "./key";
+import { compare } from "@peerbit/uint8arrays";
+import { toHexString } from "./utils";
+
+import sodium from "libsodium-wrappers";
+
+@variant(0)
+export class XSalsa20Poly1305 extends PlainKey {
+ @field({ type: fixedArray("u8", 32) })
+ key: Uint8Array;
+
+ constructor(properties: { key: Uint8Array }) {
+ super();
+ if (properties.key.length !== 32) {
+ throw new Error("Expecting key to have length 32");
+ }
+ this.key = properties.key;
+ }
+
+ static async create(): Promise {
+ await sodium.ready;
+ const generated = sodium.crypto_secretbox_keygen();
+ const kp = new XSalsa20Poly1305({
+ key: generated
+ });
+
+ return kp;
+ }
+
+ equals(other: PlainKey): boolean {
+ if (other instanceof XSalsa20Poly1305) {
+ return compare(this.key, other.key) === 0;
+ }
+ return false;
+ }
+ toString(): string {
+ return "xsalsa20poly1305/" + toHexString(this.key);
+ }
+}
diff --git a/packages/utils/keychain/.gitignore b/packages/utils/keychain/.gitignore
new file mode 100644
index 000000000..9b26ed04f
--- /dev/null
+++ b/packages/utils/keychain/.gitignore
@@ -0,0 +1,2 @@
+node_modules
+lib
\ No newline at end of file
diff --git a/packages/utils/keychain/CHANGELOG.md b/packages/utils/keychain/CHANGELOG.md
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/utils/keychain/LICENSE b/packages/utils/keychain/LICENSE
new file mode 100644
index 000000000..cdf9c0bfc
--- /dev/null
+++ b/packages/utils/keychain/LICENSE
@@ -0,0 +1,24 @@
+The MIT License (MIT)
+
+Copyright (c) 2015-2018 shamb0t
+Copyright (c) 2018 Haja Networks Oy
+Copyright (c) 2020 dao.xyz
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
diff --git a/packages/utils/keychain/package.json b/packages/utils/keychain/package.json
new file mode 100644
index 000000000..da3adb6d4
--- /dev/null
+++ b/packages/utils/keychain/package.json
@@ -0,0 +1,36 @@
+{
+ "name": "@peerbit/keychain",
+ "version": "1.0.0",
+ "description": "Utility functions for keychain",
+ "type": "module",
+ "sideEffects": false,
+ "module": "lib/esm/index.js",
+ "types": "lib/esm/index.d.ts",
+ "exports": {
+ "import": "./lib/esm/index.js",
+ "require": "./lib/cjs/index.js"
+ },
+ "files": [
+ "lib",
+ "src",
+ "!src/**/__tests__",
+ "!lib/**/__tests__",
+ "LICENSE"
+ ],
+ "publishConfig": {
+ "access": "public"
+ },
+ "scripts": {
+ "clean": "shx rm -rf lib/*",
+ "build": "yarn clean && tsc -p tsconfig.json",
+ "test": "node ../../../node_modules/.bin/jest test -c ../../../jest.config.ts --runInBand --forceExit",
+ "test:unit": "node ../../../node_modules/.bin/jest test -c ../../../jest.config.unit.ts --runInBand --forceExit",
+ "test:integration": "node ../node_modules/.bin/jest test -c ../../../jest.config.integration.ts --runInBand --forceExit"
+ },
+ "author": "dao.xyz",
+ "license": "MIT",
+ "dependencies": {
+ "@peerbit/crypto": "^1.0.10",
+ "@peerbit/any-store": "^0.0.1"
+ }
+}
diff --git a/packages/utils/keychain/src/__tests__/index.test.ts b/packages/utils/keychain/src/__tests__/index.test.ts
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/utils/crypto/src/__tests__/keychain.test.ts b/packages/utils/keychain/src/__tests__/keychain.test.ts
similarity index 82%
rename from packages/utils/crypto/src/__tests__/keychain.test.ts
rename to packages/utils/keychain/src/__tests__/keychain.test.ts
index 406b4b796..63557b977 100644
--- a/packages/utils/crypto/src/__tests__/keychain.test.ts
+++ b/packages/utils/keychain/src/__tests__/keychain.test.ts
@@ -1,9 +1,10 @@
-import { Keychain, Libp2pKeychain } from "../keychain";
import { MemoryDatastore } from "datastore-core";
import { DefaultKeyChain } from "@libp2p/keychain";
-import { Ed25519Keypair } from "../ed25519";
-import { X25519Keypair } from "../x25519";
+import { Ed25519Keypair, X25519Keypair, ByteKey } from "@peerbit/crypto";
import { Cache } from "@peerbit/cache";
+import { Keychain } from "../";
+
+// TODO update tests
describe("keychain", () => {
let keychains: Keychain[];
@@ -31,7 +32,7 @@ describe("keychain", () => {
)?.equals(kp)
).toBeTrue();
expect(
- (await keychain.exportByKey(kp.publicKey))?.equals(kp)
+ (await keychain.exportByPublicKey(kp.publicKey))?.equals(kp)
).toBeTrue();
}
});
@@ -49,7 +50,7 @@ describe("keychain", () => {
)?.equals(xkp)
).toBeTrue();
expect(
- (await keychain.exportByKey(xkp.publicKey))?.equals(xkp)
+ (await keychain.exportByPublicKey(xkp.publicKey))?.equals(xkp)
).toBeTrue();
}
});
diff --git a/packages/utils/keychain/src/index.ts b/packages/utils/keychain/src/index.ts
new file mode 100644
index 000000000..96d65789f
--- /dev/null
+++ b/packages/utils/keychain/src/index.ts
@@ -0,0 +1 @@
+export * from "./interface.js";
diff --git a/packages/utils/keychain/src/interface.ts b/packages/utils/keychain/src/interface.ts
new file mode 100644
index 000000000..5c8467fed
--- /dev/null
+++ b/packages/utils/keychain/src/interface.ts
@@ -0,0 +1,88 @@
+import {
+ Ed25519Keypair,
+ X25519Keypair,
+ Keypair,
+ XSalsa20Poly1305,
+ Ed25519PublicKey,
+ X25519PublicKey,
+ ByteKey,
+ Secp256k1Keypair,
+ Secp256k1PublicKey,
+ PublicKeyEncryptionKey,
+ PublicSignKey
+} from "@peerbit/crypto";
+
+export type KeypairFromPublicKey = T extends X25519PublicKey
+ ? X25519Keypair
+ : T extends Ed25519PublicKey
+ ? Ed25519Keypair
+ : T extends Secp256k1PublicKey
+ ? Secp256k1Keypair
+ : T extends PublicSignKey | PublicKeyEncryptionKey
+ ? Keypair
+ : never;
+
+// Should perhaps be un crypto package
+export type Keypairs =
+ | Ed25519Keypair
+ | Keypair
+ | X25519Keypair
+ | Secp256k1Keypair;
+
+// Should perhaps be un crypto package
+export type PublicKeys =
+ | Ed25519PublicKey
+ | X25519PublicKey
+ | Secp256k1PublicKey
+ | PublicSignKey
+ | PublicKeyEncryptionKey;
+
+export interface Keychain {
+ // Add a key to the keychain.
+ import(
+ parameters: (
+ | { keypair: Keypairs }
+ | { key: XSalsa20Poly1305 | ByteKey }
+ ) & { id: Uint8Array }
+ ): Promise;
+
+ // This is only really relevant for asymmetric keys? -> No changes
+ exportByPublicKey>(
+ publicKey: T
+ ): Promise;
+
+ // Export any key by their hashcode.
+ // If Key is PublicKey/PrivateKey keypair. The hashcode should be of the publickey
+ /*
+ key = new ByteKey({key: new Uint8Array(32)})
+ keychain.exportByHash(key.hashcode()) // returns key
+ */
+ exportByHash>(
+ hash: string
+ ): Promise;
+
+ // ID's are the sha256base are user defined ids. Anyone can store any key with a specific id
+ exportById<
+ T =
+ | "ed25519"
+ | "x25519"
+ | "secp256k1"
+ | "xsalsa20poly1305"
+ | "bytekey"
+ | "keypair",
+ Q = T extends "ed25519"
+ ? Ed25519Keypair
+ : T extends "x25519"
+ ? X25519Keypair
+ : T extends "secp256k1"
+ ? Secp256k1Keypair
+ : T extends "keypair"
+ ? Keypair
+ : T extends "xsalsa20poly1305"
+ ? XSalsa20Poly1305
+ : ByteKey
+ >(
+ id: string,
+ type: T
+ ): Promise;
+}
diff --git a/packages/utils/keychain/src/keychain.ts b/packages/utils/keychain/src/keychain.ts
new file mode 100644
index 000000000..aa17a6434
--- /dev/null
+++ b/packages/utils/keychain/src/keychain.ts
@@ -0,0 +1,90 @@
+/**
+ * L0 Keychain implementation using AnyStore
+ */
+
+import { createStore, AnyStore } from "@peerbit/any-store";
+import {
+ Keychain as IKeychain,
+ KeypairFromPublicKey,
+ Keypairs,
+ PublicKeys
+} from "./interface.js";
+import {
+ Ed25519Keypair,
+ Keypair,
+ X25519Keypair,
+ Secp256k1Keypair,
+ XSalsa20Poly1305,
+ ByteKey,
+ sha256Base64Sync
+} from "@peerbit/crypto";
+import { serialize } from "@dao-xyz/borsh";
+
+class Keychain implements IKeychain {
+ store: AnyStore;
+ constructor(directory?: string | undefined) {
+ this.store = createStore(directory);
+ }
+ import(
+ parameters: (
+ | { keypair: Keypairs }
+ | { key: XSalsa20Poly1305 | ByteKey }
+ ) & { id?: Uint8Array }
+ ): Promise {
+ let hashcode: string;
+ let bytes: Uint8Array;
+ if ((parameters as { keypair: Keypairs }).keypair) {
+ const kp = (parameters as { keypair: Keypairs }).keypair;
+ hashcode = kp.publicKey.hashcode();
+ bytes = serialize(kp);
+ } else {
+ const key = (parameters as { key: XSalsa20Poly1305 | ByteKey }).key;
+ hashcode = key.hashcode();
+ bytes = serialize(key);
+ }
+ this.store.put(hashcode, bytes);
+
+ if ((parameters as { id: Uint8Array }).id) {
+ this.store.put(
+ sha256Base64Sync((parameters as { id: Uint8Array }).id),
+ bytes
+ );
+ }
+ }
+
+ exportByPublicKey>(
+ publicKey: T
+ ): Promise {
+ // anystore.get by publicKey.hashcode() -> deserialize(bytes, T) -> return
+ return this.exportByHash(publicKey.hashcode());
+ }
+ exportByHash>(
+ hash: string
+ ): Promise {
+ // anystore.get by hash -> deserialize(bytes, T) -> return
+ throw new Error("Method not implemented.");
+ }
+ exportById<
+ T =
+ | "ed25519"
+ | "x25519"
+ | "secp256k1"
+ | "xsalsa20poly1305"
+ | "bytekey"
+ | "keypair",
+ Q = T extends "ed25519"
+ ? Ed25519Keypair
+ : T extends "x25519"
+ ? X25519Keypair
+ : T extends "secp256k1"
+ ? Secp256k1Keypair
+ : T extends "keypair"
+ ? Keypair
+ : T extends "xsalsa20poly1305"
+ ? XSalsa20Poly1305
+ : ByteKey
+ >(id: string, type: T): Promise {
+ // anystore.get by sha256Base64Sync(id) -> deserialize(bytes, T) -> return
+ throw new Error("Method not implemented.");
+ }
+}
diff --git a/packages/utils/keychain/tsconfig.json b/packages/utils/keychain/tsconfig.json
new file mode 100644
index 000000000..9c2db1ebd
--- /dev/null
+++ b/packages/utils/keychain/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../../../tsconfig.json",
+ "include": ["src"],
+ "compilerOptions": {
+ "noEmit": false,
+ "outDir": "lib/esm"
+ }
+}