@kokuin/browser 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # @kokuin/browser
2
+
3
+ ## Installation
4
+
5
+ ```sh
6
+ npm install @kokuin/browser
7
+ ```
package/lib/entry.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ import type { KeyEntry } from '@kokuin/token';
2
+ export type GetStore = (mode?: IDBTransactionMode) => IDBObjectStore;
3
+ export declare class BrowserKeyEntry implements KeyEntry<CryptoKeyPair> {
4
+ #private;
5
+ constructor(keyID: string, getStore: GetStore);
6
+ get keyID(): string;
7
+ getAsync(): Promise<CryptoKeyPair | null>;
8
+ setAsync(keyPair: CryptoKeyPair): Promise<void>;
9
+ provideAsync(): Promise<CryptoKeyPair>;
10
+ removeAsync(): Promise<void>;
11
+ }
package/lib/entry.js ADDED
@@ -0,0 +1,42 @@
1
+ import { randomKeyPair } from './utils.js';
2
+ export class BrowserKeyEntry {
3
+ #getStore;
4
+ #keyID;
5
+ constructor(keyID, getStore){
6
+ this.#getStore = getStore;
7
+ this.#keyID = keyID;
8
+ }
9
+ get keyID() {
10
+ return this.#keyID;
11
+ }
12
+ getAsync() {
13
+ return new Promise((resolve, reject)=>{
14
+ const request = this.#getStore().get(this.#keyID);
15
+ request.onerror = ()=>reject(request.error);
16
+ request.onsuccess = ()=>resolve(request.result ?? null);
17
+ });
18
+ }
19
+ setAsync(keyPair) {
20
+ return new Promise((resolve, reject)=>{
21
+ const request = this.#getStore('readwrite').put(keyPair, this.#keyID);
22
+ request.onerror = ()=>reject(request.error);
23
+ request.onsuccess = ()=>resolve();
24
+ });
25
+ }
26
+ async provideAsync() {
27
+ const existing = await this.getAsync();
28
+ if (existing != null) {
29
+ return existing;
30
+ }
31
+ const keyPair = await randomKeyPair();
32
+ await this.setAsync(keyPair);
33
+ return keyPair;
34
+ }
35
+ removeAsync() {
36
+ return new Promise((resolve, reject)=>{
37
+ const request = this.#getStore('readwrite').delete(this.#keyID);
38
+ request.onerror = ()=>reject(request.error);
39
+ request.onsuccess = ()=>resolve();
40
+ });
41
+ }
42
+ }
@@ -0,0 +1,3 @@
1
+ import { type SigningIdentity } from '@kokuin/token';
2
+ import { BrowserKeyStore } from './store.js';
3
+ export declare function provideSigningIdentity(keyID: string, useStore?: BrowserKeyStore | Promise<BrowserKeyStore> | string): Promise<SigningIdentity>;
@@ -0,0 +1,72 @@
1
+ import { CODECS, getDID } from '@kokuin/token';
2
+ import { b64uFromJSON, fromUTF, toB64U } from '@sozai/codec';
3
+ import { getLogger } from '@sozai/log';
4
+ import { AttributeKeys, createTracer, SpanNames, withSpan } from '@sozai/otel';
5
+ import { BrowserKeyStore } from './store.js';
6
+ import { getPublicKey } from './utils.js';
7
+ const tracer = createTracer('keystore.browser');
8
+ const logger = getLogger([
9
+ 'kokuin',
10
+ 'browser'
11
+ ]);
12
+ async function createBrowserSigningIdentity(keyPair) {
13
+ const publicKey = await getPublicKey(keyPair);
14
+ const id = getDID(CODECS.ES256, publicKey);
15
+ async function signToken(payload, options = {}) {
16
+ if (payload.iss != null && payload.iss !== id) {
17
+ throw new Error('Invalid payload: issuer does not match signer');
18
+ }
19
+ const fullHeader = {
20
+ ...options.header ?? {},
21
+ typ: 'JWT',
22
+ alg: 'ES256'
23
+ };
24
+ const fullPayload = {
25
+ ...payload,
26
+ iss: id
27
+ };
28
+ const data = `${b64uFromJSON(fullHeader)}.${b64uFromJSON(fullPayload)}`;
29
+ const messageBytes = fromUTF(data);
30
+ const signatureBuffer = await globalThis.crypto.subtle.sign({
31
+ name: 'ECDSA',
32
+ hash: 'SHA-256'
33
+ }, keyPair.privateKey, messageBytes.buffer);
34
+ return {
35
+ header: fullHeader,
36
+ payload: fullPayload,
37
+ signature: toB64U(new Uint8Array(signatureBuffer)),
38
+ data
39
+ };
40
+ }
41
+ return {
42
+ id,
43
+ publicKey,
44
+ signToken
45
+ };
46
+ }
47
+ export async function provideSigningIdentity(keyID, useStore) {
48
+ return withSpan(tracer, SpanNames.KEYSTORE_GET_OR_CREATE, {
49
+ attributes: {
50
+ [AttributeKeys.KEYSTORE_STORE_TYPE]: 'browser'
51
+ }
52
+ }, async (span)=>{
53
+ const storePromise = useStore == null || typeof useStore === 'string' ? BrowserKeyStore.open(useStore) : Promise.resolve(useStore);
54
+ const store = await storePromise;
55
+ const entry = store.entry(keyID);
56
+ const existing = await entry.getAsync();
57
+ if (existing != null) {
58
+ const identity = await createBrowserSigningIdentity(existing);
59
+ span.setAttribute(AttributeKeys.AUTH_DID, identity.id);
60
+ span.setAttribute(AttributeKeys.KEYSTORE_KEY_CREATED, false);
61
+ return identity;
62
+ }
63
+ const keyPair = await entry.provideAsync();
64
+ const identity = await createBrowserSigningIdentity(keyPair);
65
+ span.setAttribute(AttributeKeys.AUTH_DID, identity.id);
66
+ span.setAttribute(AttributeKeys.KEYSTORE_KEY_CREATED, true);
67
+ logger.info('New identity generated: {did}', {
68
+ did: identity.id
69
+ });
70
+ return identity;
71
+ });
72
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Key store for browser.
3
+ *
4
+ * ## Installation
5
+ *
6
+ * ```sh
7
+ * npm install @kokuin/browser
8
+ * ```
9
+ *
10
+ * @module browser-keystore
11
+ */
12
+ export { BrowserKeyEntry } from './entry.js';
13
+ export { provideSigningIdentity } from './identity.js';
14
+ export { BrowserKeyStore } from './store.js';
15
+ export { getPublicKey, randomKeyPair } from './utils.js';
package/lib/index.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Key store for browser.
3
+ *
4
+ * ## Installation
5
+ *
6
+ * ```sh
7
+ * npm install @kokuin/browser
8
+ * ```
9
+ *
10
+ * @module browser-keystore
11
+ */ export { BrowserKeyEntry } from './entry.js';
12
+ export { provideSigningIdentity } from './identity.js';
13
+ export { BrowserKeyStore } from './store.js';
14
+ export { getPublicKey, randomKeyPair } from './utils.js';
package/lib/store.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ import type { KeyStore } from '@kokuin/token';
2
+ import { BrowserKeyEntry } from './entry.js';
3
+ export declare class BrowserKeyStore implements KeyStore<CryptoKeyPair, BrowserKeyEntry> {
4
+ #private;
5
+ static open(name?: string): Promise<BrowserKeyStore>;
6
+ constructor(db: IDBDatabase);
7
+ entry(keyID: string): BrowserKeyEntry;
8
+ }
package/lib/store.js ADDED
@@ -0,0 +1,45 @@
1
+ import { defer } from '@sozai/async';
2
+ import { BrowserKeyEntry } from './entry.js';
3
+ const DEFAULT_DB_NAME = 'kokuin:key-store';
4
+ const STORE_NAME = 'keys';
5
+ function createGetStore(db) {
6
+ return function getStore(mode = 'readonly') {
7
+ return db.transaction(STORE_NAME, mode).objectStore(STORE_NAME);
8
+ };
9
+ }
10
+ export class BrowserKeyStore {
11
+ static #byName = {};
12
+ static open(name = DEFAULT_DB_NAME) {
13
+ const existing = BrowserKeyStore.#byName[name];
14
+ if (existing != null) {
15
+ return existing;
16
+ }
17
+ const { promise, reject, resolve } = defer();
18
+ BrowserKeyStore.#byName[name] = promise;
19
+ if (typeof globalThis.crypto.subtle === 'undefined') {
20
+ reject(new Error('Unable to open KeyStore: SubtleCrypto is not available'));
21
+ return promise;
22
+ }
23
+ if (typeof globalThis.indexedDB === 'undefined') {
24
+ reject(new Error('Unable to open KeyStore: IndexedDB is not available'));
25
+ return promise;
26
+ }
27
+ const request = indexedDB.open(name, 1);
28
+ request.onerror = ()=>reject(request.error);
29
+ request.onsuccess = ()=>resolve(new BrowserKeyStore(request.result));
30
+ request.onupgradeneeded = (event)=>{
31
+ ;
32
+ event.target.result.createObjectStore(STORE_NAME);
33
+ };
34
+ return promise;
35
+ }
36
+ #entries = {};
37
+ #getStore;
38
+ constructor(db){
39
+ this.#getStore = createGetStore(db);
40
+ }
41
+ entry(keyID) {
42
+ this.#entries[keyID] ??= new BrowserKeyEntry(keyID, this.#getStore);
43
+ return this.#entries[keyID];
44
+ }
45
+ }
package/lib/utils.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export declare function getPublicKey(keyPair: CryptoKeyPair): Promise<Uint8Array>;
2
+ export declare function randomKeyPair(): Promise<CryptoKeyPair>;
package/lib/utils.js ADDED
@@ -0,0 +1,18 @@
1
+ function ecPointCompress(x, y) {
2
+ const out = new Uint8Array(x.length + 1);
3
+ out[0] = 2 + (y[y.length - 1] & 1);
4
+ out.set(x, 1);
5
+ return out;
6
+ }
7
+ export async function getPublicKey(keyPair) {
8
+ const rawKey = await globalThis.crypto.subtle.exportKey('raw', keyPair.publicKey);
9
+ return ecPointCompress(new Uint8Array(rawKey.slice(1, 33)), new Uint8Array(rawKey.slice(33, 65)));
10
+ }
11
+ export async function randomKeyPair() {
12
+ return await globalThis.crypto.subtle.generateKey({
13
+ name: 'ECDSA',
14
+ namedCurve: 'P-256'
15
+ }, false, [
16
+ 'sign'
17
+ ]);
18
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@kokuin/browser",
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "description": "Key store for browser",
6
+ "keywords": [
7
+ "cryptography",
8
+ "key-management",
9
+ "browser",
10
+ "storage"
11
+ ],
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/TairuFramework/kokuin",
15
+ "directory": "packages/browser"
16
+ },
17
+ "type": "module",
18
+ "main": "lib/index.js",
19
+ "types": "lib/index.d.ts",
20
+ "exports": {
21
+ ".": "./lib/index.js"
22
+ },
23
+ "files": [
24
+ "lib/*"
25
+ ],
26
+ "sideEffects": false,
27
+ "dependencies": {
28
+ "@sozai/async": "^0.1.0",
29
+ "@sozai/codec": "^0.1.0",
30
+ "@sozai/log": "^0.1.0",
31
+ "@sozai/otel": "^0.1.0",
32
+ "@kokuin/token": "^0.1.0"
33
+ },
34
+ "scripts": {
35
+ "build:clean": "del lib",
36
+ "build:js": "swc src -d ./lib --config-file ../../node_modules/@kigu/dev/swc.json --strip-leading-paths",
37
+ "build:types": "tsc --emitDeclarationOnly --skipLibCheck",
38
+ "build": "pnpm run build:clean && pnpm run build:js && pnpm run build:types",
39
+ "test:types": "tsc --noEmit --skipLibCheck -p tsconfig.test.json",
40
+ "test:unit": "vitest run",
41
+ "test": "pnpm run test:types && pnpm run test:unit"
42
+ }
43
+ }