Skip to content

Commit

Permalink
SImplify bip39 usage
Browse files Browse the repository at this point in the history
Bip39 imported ad-hoc was micro-optimization.
  • Loading branch information
steida committed May 25, 2024
1 parent 32f4807 commit 8a3a02a
Show file tree
Hide file tree
Showing 9 changed files with 46 additions and 129 deletions.
3 changes: 1 addition & 2 deletions packages/evolu-common-web/src/Db.worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import { NanoIdGeneratorLive } from "./NanoIdGeneratorLive.js";
import { Bip39Live, SyncLockLive } from "./PlatformLive.js";
import { SyncLockLive } from "./PlatformLive.js";
import { expose, wrap } from "./ProxyWorker.js";
import { SqliteFactoryLive } from "./SqliteFactoryLive.js";

Expand All @@ -25,7 +25,6 @@ const SyncFactoryLive = Layer.succeed(SyncFactory, {
createDb.pipe(
Effect.provide(
Layer.mergeAll(
Bip39Live,
SqliteFactory.Common.pipe(
Layer.provide(SqliteFactoryLive),
Layer.provide(NanoIdGeneratorLive),
Expand Down
35 changes: 0 additions & 35 deletions packages/evolu-common-web/src/PlatformLive.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import {
AppState,
Bip39,
Mnemonic,
SyncLock,
SyncLockAlreadySyncingError,
SyncLockRelease,
getLockName,
validateMnemonicToEffect,
} from "@evolu/common";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
Expand Down Expand Up @@ -85,35 +82,3 @@ export const SyncLockLive = Layer.succeed(SyncLock, {
return yield* Effect.acquireRelease(acquire, release);
}),
});

const importBip39WithEnglish = Effect.all(
[
Effect.promise(() => import("@scure/bip39")),
Effect.promise(() => import("@scure/bip39/wordlists/english")),
],
{ concurrency: "unbounded" },
);

export const Bip39Live = Layer.succeed(
Bip39,
Bip39.of({
make: importBip39WithEnglish.pipe(
Effect.map(
([{ generateMnemonic }, { wordlist }]) =>
generateMnemonic(wordlist, 128) as Mnemonic,
),
),

toSeed: (mnemonic) =>
Effect.promise(() => import("@scure/bip39")).pipe(
Effect.flatMap((a) => Effect.promise(() => a.mnemonicToSeed(mnemonic))),
),

parse: (mnemonic) =>
importBip39WithEnglish.pipe(
Effect.flatMap(([{ validateMnemonic }, { wordlist }]) =>
validateMnemonicToEffect(validateMnemonic)(mnemonic, wordlist),
),
),
}),
);
17 changes: 1 addition & 16 deletions packages/evolu-common-web/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import {
Bip39,
Db,
DbFactory,
EvoluFactory,
InvalidMnemonicError,
Mnemonic,
notSupportedPlatformWorker,
} from "@evolu/common";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import { NanoIdGeneratorLive } from "./NanoIdGeneratorLive.js";
import { AppStateLive, Bip39Live } from "./PlatformLive.js";
import { AppStateLive } from "./PlatformLive.js";
import { wrap } from "./ProxyWorker.js";

const DbFactoryLive = Layer.succeed(DbFactory, {
Expand All @@ -31,18 +28,6 @@ export const EvoluFactoryWeb = Layer.provide(
Layer.mergeAll(DbFactoryLive, NanoIdGeneratorLive, AppStateLive),
);

/**
* Parse a string to {@link Mnemonic}.
*
* This function is async because Bip39 is imported dynamically.
*/
export const parseMnemonic: (
mnemonic: string,
) => Effect.Effect<Mnemonic, InvalidMnemonicError> = Bip39.pipe(
Effect.provide(Bip39Live),
Effect.runSync,
).parse;

// JSDoc doesn't support destructured parameters, so we must copy-paste
// createEvolu docs from `evolu-common/src/Evolu.ts`.
// https://github.com/microsoft/TypeScript/issues/11859
Expand Down
63 changes: 31 additions & 32 deletions packages/evolu-common/src/Crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,44 +4,14 @@ import { concatBytes } from "@noble/ciphers/utils";
import { hmac } from "@noble/hashes/hmac";
import { sha512 } from "@noble/hashes/sha512";
import { randomBytes } from "@noble/hashes/utils";
import * as bip39 from "@scure/bip39";
import { wordlist } from "@scure/bip39/wordlists/english";
import * as Brand from "effect/Brand";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";

import { Id } from "./Model.js";

export class Bip39 extends Context.Tag("Bip39")<
Bip39,
{
readonly make: Effect.Effect<Mnemonic>;

readonly toSeed: (mnemonic: Mnemonic) => Effect.Effect<Uint8Array>;

readonly parse: (
mnemonic: string,
) => Effect.Effect<Mnemonic, InvalidMnemonicError>;
}
>() {}

export interface InvalidMnemonicError {
readonly _tag: "InvalidMnemonicError";
}

export const validateMnemonicToEffect =
(validateMnemonic: (mnemonic: string, wordlist: string[]) => boolean) =>
(
mnemonic: string,
wordlist: string[],
): Effect.Effect<Mnemonic, InvalidMnemonicError, never> => {
const mnemonicTrimmed = mnemonic.trim();
return validateMnemonic(mnemonicTrimmed, wordlist)
? Effect.succeed(mnemonicTrimmed as Mnemonic)
: Effect.fail<InvalidMnemonicError>({
_tag: "InvalidMnemonicError",
});
};

/**
* Mnemonic is a password generated by Evolu in BIP39 format.
*
Expand All @@ -51,6 +21,35 @@ export const validateMnemonicToEffect =
*/
export type Mnemonic = string & Brand.Brand<"Mnemonic">;

/**
* Mnemonic is a password generated by Evolu in BIP39 format.
*
* A mnemonic, also known as a "seed phrase," is a set of 12 words in a specific
* order chosen from a predefined list. The purpose of the BIP39 mnemonic is to
* provide a human-readable way of storing a private key.
*/
export const createMnemonic = (): Mnemonic =>
bip39.generateMnemonic(wordlist, 128) as Mnemonic;

/** Parse a string to {@link Mnemonic}. */
export const parseMnemonic = (
mnemonic: string,
): Effect.Effect<Mnemonic, InvalidMnemonicError> => {
const mnemonicTrimmed = mnemonic.trim();
return bip39.validateMnemonic(mnemonicTrimmed, wordlist)
? Effect.succeed(mnemonicTrimmed as Mnemonic)
: Effect.fail<InvalidMnemonicError>({
_tag: "InvalidMnemonicError",
});
};

export interface InvalidMnemonicError {
readonly _tag: "InvalidMnemonicError";
}

export const mnemonicToSeed = (mnemonic: Mnemonic): Uint8Array =>
bip39.mnemonicToSeedSync(mnemonic);

export class NanoIdGenerator extends Context.Tag("NanoIdGenerator")<
NanoIdGenerator,
{
Expand Down
16 changes: 9 additions & 7 deletions packages/evolu-common/src/Owner.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import * as Brand from "effect/Brand";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import { Bip39, Mnemonic, slip21Derive } from "./Crypto.js";
import {
Mnemonic,
createMnemonic,
mnemonicToSeed,
slip21Derive,
} from "./Crypto.js";
import { Id } from "./Model.js";

/**
Expand Down Expand Up @@ -35,13 +40,10 @@ export const Owner = Context.GenericTag<Owner>("Owner");
*/
export type OwnerId = Id & Brand.Brand<"Owner">;

export const makeOwner = (
mnemonic?: Mnemonic,
): Effect.Effect<Owner, never, Bip39> =>
export const makeOwner = (mnemonic?: Mnemonic): Effect.Effect<Owner> =>
Effect.gen(function* (_) {
const bip39 = yield* Bip39;
if (mnemonic == null) mnemonic = yield* bip39.make;
const seed = yield* bip39.toSeed(mnemonic);
if (mnemonic == null) mnemonic = createMnemonic();
const seed = mnemonicToSeed(mnemonic);
const id = yield* Effect.map(
slip21Derive(seed, ["Evolu", "Owner Id"]),
(key) => {
Expand Down
1 change: 1 addition & 0 deletions packages/evolu-common/src/Public.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export { sql } from "kysely";
export type { NotNull } from "kysely";
export { jsonArrayFrom, jsonObjectFrom } from "kysely/helpers/sqlite";
export type { Timestamp, TimestampError } from "./Crdt.js";
export { createMnemonic, parseMnemonic } from "./Crypto.js";
export type { InvalidMnemonicError, Mnemonic } from "./Crypto.js";
export type { ExtractRow, QueryResult } from "./Db.js";
export * from "./Error.js";
Expand Down
21 changes: 0 additions & 21 deletions packages/evolu-react-native/src/PlatformLive.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,10 @@
import {
AppState,
Bip39,
Mnemonic,
SyncLock,
SyncLockAlreadySyncingError,
SyncLockRelease,
validateMnemonicToEffect,
} from "@evolu/common";
import NetInfo, { NetInfoState } from "@react-native-community/netinfo";
import {
generateMnemonic,
mnemonicToSeed,
validateMnemonic,
} from "@scure/bip39";
import { wordlist } from "@scure/bip39/wordlists/english";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import { reloadAsync } from "expo-updates";
Expand Down Expand Up @@ -84,15 +75,3 @@ export const SyncLockLive = Layer.effect(
});
}),
);

export const Bip39Live = Layer.succeed(
Bip39,
Bip39.of({
make: Effect.sync(() => generateMnemonic(wordlist, 128) as Mnemonic),

toSeed: (mnemonic) => Effect.promise(() => mnemonicToSeed(mnemonic)),

parse: (mnemonic) =>
validateMnemonicToEffect(validateMnemonic)(mnemonic, wordlist),
}),
);
18 changes: 3 additions & 15 deletions packages/evolu-react-native/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import {
Bip39,
DbFactory,
EvoluFactory,
InvalidMnemonicError,
Mnemonic,
SecretBox,
Sqlite,
SqliteFactory,
Expand All @@ -17,21 +14,13 @@ import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
// @ts-expect-error https://github.com/ai/nanoid/issues/468
import { customAlphabet, nanoid } from "nanoid/index.browser.js";
import { AppStateLive, Bip39Live, SyncLockLive } from "./PlatformLive.js";
import { AppStateLive, SyncLockLive } from "./PlatformLive.js";
import { SqliteLive } from "./SqliteLive.js";

// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
const NanoIdGeneratorLive = createNanoIdGeneratorLive(customAlphabet, nanoid);

export * from "@evolu/common/public";

/** Parse a string to {@link Mnemonic}. */
export const parseMnemonic: (
mnemonic: string,
) => Effect.Effect<Mnemonic, InvalidMnemonicError> = Bip39.pipe(
Effect.provide(Bip39Live),
Effect.runSync,
).parse;
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
const NanoIdGeneratorLive = createNanoIdGeneratorLive(customAlphabet, nanoid);

const SyncFactoryLive = Layer.succeed(SyncFactory, {
createSync: Effect.provide(createSync, SecretBox.Live),
Expand All @@ -48,7 +37,6 @@ export const EvoluFactoryReactNative = Layer.provide(
createDb: createDb.pipe(
Effect.provide(
Layer.mergeAll(
Bip39Live,
SqliteFactory.Common.pipe(
Layer.provide(SqliteFactoryLive),
Layer.provide(NanoIdGeneratorLive),
Expand Down
1 change: 0 additions & 1 deletion packages/evolu-react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import { flushSync } from "react-dom";

export { parseMnemonic } from "@evolu/common-web";
export * from "@evolu/common/public";

const EvoluFactoryWebReact = EvoluFactoryWeb.pipe(
Expand Down

0 comments on commit 8a3a02a

Please sign in to comment.