Skip to content

Commit 0ff1a62

Browse files
authored
chore: clean up common-identity, using Identity for all ed25519 keypairs (#427)
chore: clean up common-identity, using Identity for all ed25519 keypairs, and deriving keys as needed.
1 parent f2d2144 commit 0ff1a62

File tree

9 files changed

+103
-156
lines changed

9 files changed

+103
-156
lines changed

typescript/packages/common-identity/examples/index.html

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,9 @@ <h3>Signature</h3>
7979
</nav>
8080
</div>
8181
<script type="module">
82-
import { PassKey, RootKey } from "../dist/identity.js";
82+
import { PassKey, Identity, KeyStore } from "../dist/identity.js";
8383
const $ = s => document.querySelector(s);
84+
const ROOT_KEY = "$ROOT_KEY";
8485
const encoder = new TextEncoder();
8586
let passKeyDeferred = defer();
8687
let rootKey = null;
@@ -90,19 +91,23 @@ <h3>Signature</h3>
9091
// from a successful registration
9192
maybeDisplayRegisterSuccess();
9293

94+
// Open the key store at the default location.
95+
let store = await KeyStore.open();
96+
9397
// First check if we've already stored a root key.
94-
let rootKey = await RootKey.fromStorage();
98+
let rootKey = await store.get(ROOT_KEY);
9599
if (rootKey) {
96100
setRootKey(rootKey);
97101
return;
98102
}
103+
99104
// If no root key set, wait for user to authenticate a PassKey,
100105
// either by creating a new one, or reauthenticating.
101106
let passKey = await passKeyDeferred.promise;
102-
// Create and set a root key from the passkey
103-
rootKey = await RootKey.fromPassKey(passKey);
107+
// Create a root key derive from the pass key.
108+
rootKey = await passKey.createRootKey();
104109
// Store root key to storage for subsequent page loads
105-
await rootKey.saveToStorage();
110+
await store.set(ROOT_KEY, rootKey);
106111
setRootKey(rootKey);
107112
})();
108113

typescript/packages/common-identity/src/derived-key.ts

Lines changed: 0 additions & 37 deletions
This file was deleted.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { Ed25519KeyPair } from "./ed25519/index.js";
2+
import { KeyPair, KeyPairRaw } from "./keys.js";
3+
import { hash } from "./utils.js";
4+
5+
const textEncoder = new TextEncoder();
6+
7+
// An `Identity` represents a public/private key pair.
8+
//
9+
// Additional keys can be deterministically derived from an identity.
10+
export class Identity implements KeyPair {
11+
private keypair: Ed25519KeyPair;
12+
constructor(keypair: Ed25519KeyPair) {
13+
this.keypair = keypair;
14+
}
15+
16+
// Sign `data` with this identity.
17+
async sign(data: Uint8Array): Promise<Uint8Array> {
18+
return await this.keypair.sign(data);
19+
}
20+
21+
// Verify `signature` and `data` with this identity.
22+
async verify(signature: Uint8Array, data: Uint8Array): Promise<boolean> {
23+
return await this.keypair.verify(signature, data);
24+
}
25+
26+
// Serialize this identity for storage.
27+
serialize(): KeyPairRaw {
28+
return this.keypair.serialize();
29+
}
30+
31+
// Derive a new `Identity` given a seed string.
32+
async derive(name: string): Promise<Identity> {
33+
const seed = textEncoder.encode(name);
34+
const hashed = await hash(seed);
35+
const signed = await this.sign(hashed);
36+
const signedHash = await hash(signed);
37+
return await Identity.generateFromRaw(signedHash);
38+
}
39+
40+
// Generate a new identity from raw ed25519 key material.
41+
static async generateFromRaw(rawPrivateKey: Uint8Array): Promise<Identity> {
42+
return new Identity(await Ed25519KeyPair.generateFromRaw(rawPrivateKey));
43+
}
44+
45+
// Generate a new identity.
46+
static async generate(): Promise<Identity> {
47+
return new Identity(await Ed25519KeyPair.generate());
48+
}
49+
50+
// Deserialize `input` from storage into an `Identity`.
51+
static deserialize(input: any): Identity {
52+
return new Identity(Ed25519KeyPair.deserialize(input));
53+
}
54+
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export { PassKey } from "./pass-key.js";
2-
export { RootKey } from "./root-key.js";
2+
export { Identity } from "./identity.js";
3+
export { KeyStore } from "./key-store.js";

typescript/packages/common-identity/src/key-store.ts

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,51 @@
1-
import { Ed25519KeyPair } from "./ed25519/index.js";
1+
import { Identity } from "./identity.js";
22
import { once } from "./utils.js";
33

4-
const DB_NAME = "key-store";
5-
const STORE_NAME = "key-store";
6-
const ROOT_KEY = "root-key";
4+
const DB_NAME = "common-key-store";
5+
const DEFAULT_STORE_NAME = "key-store";
76
const DB_VERSION = 1;
87

98
// An abstraction around storing key materials in IndexedDb.
10-
// For now, we use a hardcoded, well-known database, store, and key-name,
11-
// such that there's a single location to determine if a root key is
12-
// available.
139
export class KeyStore {
10+
static DEFAULT_STORE_NAME = DEFAULT_STORE_NAME;
1411
private db: DB;
15-
constructor(db: DB) {
12+
private storeName: string;
13+
14+
constructor(db: DB, storeName: string) {
1615
this.db = db;
16+
this.storeName = storeName;
1717
}
1818

19-
// Get the `RootKey` keypair at the globally-known key space.
20-
async get(): Promise<Ed25519KeyPair | undefined> {
21-
let result = await this.db.get(STORE_NAME, ROOT_KEY);
19+
// Get the `name` keypair.
20+
async get(name: string): Promise<Identity | undefined> {
21+
let result = await this.db.get(this.storeName, name);
2222
if (result) {
23-
return Ed25519KeyPair.deserialize(result);
23+
return Identity.deserialize(result);
2424
}
2525
return result;
2626
}
2727

28-
// Set the global `RootKey` keypair at the globally-known key space.
29-
async set(value: Ed25519KeyPair): Promise<undefined> {
30-
await this.db.set(STORE_NAME, ROOT_KEY, value.serialize());
28+
// Set the `name` keypair with `value`.
29+
async set(name: string, value: Identity): Promise<undefined> {
30+
await this.db.set(this.storeName, name, value.serialize());
3131
}
3232

3333
// Clear the key store's table.
3434
async clear(): Promise<any> {
35-
return await this.db.clear(STORE_NAME);
35+
return await this.db.clear(this.storeName);
3636
}
3737

38-
// Create a new instance of `KeyStore`.
39-
static async open(): Promise<KeyStore> {
38+
// Opens a new instance of `KeyStore`.
39+
// If no `name` provided, `KeyStore.DEFAULT_STORE_NAME` is used.
40+
static async open(name = KeyStore.DEFAULT_STORE_NAME): Promise<KeyStore> {
4041
let db = await DB.open(DB_NAME, DB_VERSION, (e: IDBVersionChangeEvent) => {
4142
const { newVersion, oldVersion: _ } = e;
4243
if (newVersion !== DB_VERSION) { throw new Error("common-identity: Invalid DB version."); }
4344
if (!e.target) { throw new Error("common-identity: No target on change event."); }
4445
let db: IDBDatabase = (e.target as IDBRequest).result;
45-
db.createObjectStore(STORE_NAME);
46+
db.createObjectStore(name);
4647
});
47-
return new KeyStore(db);
48+
return new KeyStore(db, name);
4849
}
4950

5051
}

typescript/packages/common-identity/src/pass-key.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Identity } from "./identity.js";
12
import { bufferSourceToArrayBuffer, random } from "./utils.js";
23

34
const RP = "Common Tools";
@@ -23,15 +24,27 @@ export interface PassKeyGetOptions {
2324

2425
// A `PassKey` represents an authentication via a WebAuthn authenticator.
2526
// A key must first be created for an origin, and then retrieved
26-
// as a `PassKey` instance. From there, a `RootKey` can be derived/stored.
27+
// as a `PassKey` instance. From there, a root key `Identity` can be derived/stored.
2728
export class PassKey {
2829
private credentials: PublicKeyCredential;
2930
private constructor(credentials: PublicKeyCredential) {
3031
this.credentials = credentials;
3132
}
3233

34+
// Generate a root key from a `PassKey`.
35+
// A root key identity is deterministically derived from a `PassKey`'s
36+
// PRF output, a 32-byte hash, which is used as ed25519 key material.
37+
async createRootKey(): Promise<Identity> {
38+
let seed = this.prf();
39+
if (!seed) {
40+
throw new Error("common-identity: No prf found from PassKey");
41+
}
42+
43+
return await Identity.generateFromRaw(seed);
44+
}
45+
3346
// Return the secret 32-bytes derived from the passkey's PRF data.
34-
prf(): Uint8Array | null {
47+
private prf(): Uint8Array | null {
3548
// PRF results are only available when calling `get()`,
3649
// not during key creation.
3750
let extResults = this.getCredentials().getClientExtensionResults();

typescript/packages/common-identity/src/root-key.ts

Lines changed: 0 additions & 63 deletions
This file was deleted.

typescript/packages/common-identity/src/space-key.ts

Lines changed: 0 additions & 28 deletions
This file was deleted.

typescript/packages/common-identity/src/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export function once(target: EventTarget, eventName: string, callback: (e: any)
1818
}
1919

2020
const HASH_ALG = "SHA-512";
21+
// Hash input via SHA-512.
2122
export async function hash(input: Uint8Array): Promise<Uint8Array> {
2223
return new Uint8Array(await window.crypto.subtle.digest(HASH_ALG, input));
2324
}

0 commit comments

Comments
 (0)