@kokuin/electron 0.3.0 → 0.4.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 +17 -0
- package/lib/entry.d.ts +21 -10
- package/lib/entry.js +50 -31
- package/lib/index.d.ts +3 -3
- package/lib/index.js +0 -1
- package/lib/store.d.ts +26 -6
- package/lib/store.js +77 -11
- package/lib/types.d.ts +8 -0
- package/lib/types.js +8 -1
- package/package.json +8 -6
- package/lib/identity.d.ts +0 -4
- package/lib/identity.js +0 -61
package/README.md
CHANGED
|
@@ -5,3 +5,20 @@
|
|
|
5
5
|
```sh
|
|
6
6
|
npm install @kokuin/electron
|
|
7
7
|
```
|
|
8
|
+
|
|
9
|
+
## Cross-process locking
|
|
10
|
+
|
|
11
|
+
`ElectronKeyStore.open({ name, lockPath })` is opt-in. Without it, two processes racing to
|
|
12
|
+
generate a key for a fresh keyID both create one, and only the in-process lock applies — the
|
|
13
|
+
result is unsafe: silent key loss on backends that upsert unconditionally (e.g. Linux/
|
|
14
|
+
libsecret), or a thrown duplicate-item error on macOS Keychain. With `lockPath` set,
|
|
15
|
+
`provideAsync()` serializes the create through a file lock (`@sozai/lock`) so both processes
|
|
16
|
+
converge on the same key.
|
|
17
|
+
|
|
18
|
+
`lockPath` must point to a **local** filesystem — `link()` is not atomic on NFS — and names a
|
|
19
|
+
file, not a directory: one coarse lock per store, not one per keyID. Acquisition is bounded and
|
|
20
|
+
throws on timeout rather than proceeding unlocked.
|
|
21
|
+
|
|
22
|
+
The synchronous `entry.provide()` cannot hold a cross-process lock (a file lock can't be
|
|
23
|
+
acquired synchronously), so it throws when the store was opened with a `lockPath`; use
|
|
24
|
+
`provideAsync()` instead.
|
package/lib/entry.d.ts
CHANGED
|
@@ -1,15 +1,26 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { MutableKeyEntry } from '@kokuin/token';
|
|
2
2
|
import type { KeyStorage } from './types.js';
|
|
3
|
-
export
|
|
3
|
+
export type ElectronKeyEntryParams = {
|
|
4
|
+
storage: KeyStorage;
|
|
5
|
+
keyID: string;
|
|
6
|
+
key?: Uint8Array;
|
|
7
|
+
allowInsecureStorage?: boolean;
|
|
8
|
+
lockPath?: string;
|
|
9
|
+
};
|
|
10
|
+
export declare class ElectronKeyEntry implements MutableKeyEntry<Uint8Array> {
|
|
4
11
|
#private;
|
|
5
|
-
constructor(
|
|
12
|
+
constructor(params: ElectronKeyEntryParams);
|
|
6
13
|
get keyID(): string;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
get(): Uint8Array | null;
|
|
15
|
+
getAsync(): Promise<Uint8Array | null>;
|
|
16
|
+
set(privateKey: Uint8Array): void;
|
|
17
|
+
setAsync(privateKey: Uint8Array): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* The stored key, generating one if absent. Synchronous, and therefore **not** cross-process
|
|
20
|
+
* safe. Throws when a `lockPath` is set rather than silently dropping the guarantee.
|
|
21
|
+
*/
|
|
22
|
+
provide(): Uint8Array;
|
|
23
|
+
provideAsync(): Promise<Uint8Array>;
|
|
14
24
|
remove(): void;
|
|
25
|
+
removeAsync(): Promise<void>;
|
|
15
26
|
}
|
package/lib/entry.js
CHANGED
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
import { randomPrivateKey } from '@kokuin/token';
|
|
2
2
|
import { fromB64, toB64 } from '@sozai/codec';
|
|
3
|
+
import { withFileLock } from '@sozai/lock';
|
|
3
4
|
import { safeStorage } from 'electron';
|
|
4
|
-
function encryptKey(
|
|
5
|
-
return toB64(safeStorage.encryptString(
|
|
5
|
+
/** Encrypt raw key bytes for storage: base64(safeStorage(base64(key))). */ function encryptKey(privateKey) {
|
|
6
|
+
return toB64(safeStorage.encryptString(toB64(privateKey)));
|
|
6
7
|
}
|
|
7
8
|
function decryptKey(encrypted) {
|
|
8
|
-
return safeStorage.decryptString(Buffer.from(fromB64(encrypted)));
|
|
9
|
+
return fromB64(safeStorage.decryptString(Buffer.from(fromB64(encrypted))));
|
|
9
10
|
}
|
|
10
|
-
// Stored as base64
|
|
11
11
|
export class ElectronKeyEntry {
|
|
12
12
|
#keyID;
|
|
13
13
|
#key;
|
|
14
14
|
#storage;
|
|
15
15
|
#allowInsecureStorage;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
this.#
|
|
20
|
-
this.#
|
|
16
|
+
#lockPath;
|
|
17
|
+
#provideLock = Promise.resolve();
|
|
18
|
+
constructor(params){
|
|
19
|
+
this.#keyID = params.keyID;
|
|
20
|
+
this.#key = params.key;
|
|
21
|
+
this.#storage = params.storage;
|
|
22
|
+
this.#allowInsecureStorage = params.allowInsecureStorage ?? false;
|
|
23
|
+
this.#lockPath = params.lockPath;
|
|
21
24
|
}
|
|
22
25
|
#assertEncryptionAvailable() {
|
|
23
26
|
if (!this.#allowInsecureStorage && !safeStorage.isEncryptionAvailable()) {
|
|
@@ -27,9 +30,6 @@ export class ElectronKeyEntry {
|
|
|
27
30
|
get keyID() {
|
|
28
31
|
return this.#keyID;
|
|
29
32
|
}
|
|
30
|
-
getAsync() {
|
|
31
|
-
return Promise.resolve(this.get());
|
|
32
|
-
}
|
|
33
33
|
get() {
|
|
34
34
|
if (this.#key != null) {
|
|
35
35
|
return this.#key;
|
|
@@ -38,42 +38,61 @@ export class ElectronKeyEntry {
|
|
|
38
38
|
if (encrypted == null) {
|
|
39
39
|
return null;
|
|
40
40
|
}
|
|
41
|
-
|
|
42
|
-
if (key == null) {
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
this.#key = key;
|
|
41
|
+
this.#key = decryptKey(encrypted);
|
|
46
42
|
return this.#key;
|
|
47
43
|
}
|
|
48
|
-
async
|
|
49
|
-
return this.
|
|
44
|
+
async getAsync() {
|
|
45
|
+
return this.get();
|
|
50
46
|
}
|
|
51
|
-
set(
|
|
47
|
+
set(privateKey) {
|
|
52
48
|
this.#assertEncryptionAvailable();
|
|
53
|
-
const encrypted = encryptKey(key);
|
|
54
49
|
const keys = this.#storage.getKeys();
|
|
55
|
-
keys[this.#keyID] =
|
|
50
|
+
keys[this.#keyID] = encryptKey(privateKey);
|
|
56
51
|
this.#storage.setKeys(keys);
|
|
57
|
-
this.#key =
|
|
52
|
+
this.#key = privateKey;
|
|
58
53
|
}
|
|
59
|
-
async
|
|
60
|
-
|
|
54
|
+
async setAsync(privateKey) {
|
|
55
|
+
this.set(privateKey);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The stored key, generating one if absent. Synchronous, and therefore **not** cross-process
|
|
59
|
+
* safe. Throws when a `lockPath` is set rather than silently dropping the guarantee.
|
|
60
|
+
*/ provide() {
|
|
61
|
+
if (this.#lockPath != null) {
|
|
62
|
+
throw new Error('ElectronKeyEntry.provide() cannot hold a cross-process lock: a file lock cannot be ' + 'acquired synchronously. This store was opened with a lockPath — use provideAsync().');
|
|
63
|
+
}
|
|
64
|
+
return this.#provideUnlocked();
|
|
61
65
|
}
|
|
62
|
-
|
|
66
|
+
provideAsync() {
|
|
67
|
+
const run = this.#provideLock.then(async ()=>{
|
|
68
|
+
const lockPath = this.#lockPath;
|
|
69
|
+
if (lockPath == null) {
|
|
70
|
+
return this.#provideUnlocked();
|
|
71
|
+
}
|
|
72
|
+
return await withFileLock(lockPath, async ()=>this.#provideUnlocked());
|
|
73
|
+
});
|
|
74
|
+
this.#provideLock = run.catch(()=>undefined);
|
|
75
|
+
return run;
|
|
76
|
+
}
|
|
77
|
+
/** Read-if-absent, generate, write. The caller owns exclusion. */ #provideUnlocked() {
|
|
78
|
+
// Drop the in-memory cache: inside the lock we must see a peer's write, not our own
|
|
79
|
+
// stale read. Without this the winner clobbers the peer's key.
|
|
80
|
+
this.#key = undefined;
|
|
63
81
|
const existing = this.get();
|
|
64
82
|
if (existing != null) {
|
|
65
83
|
return existing;
|
|
66
84
|
}
|
|
67
|
-
const privateKey =
|
|
85
|
+
const privateKey = randomPrivateKey();
|
|
68
86
|
this.set(privateKey);
|
|
69
87
|
return privateKey;
|
|
70
88
|
}
|
|
71
|
-
removeAsync() {
|
|
72
|
-
return Promise.resolve(this.remove());
|
|
73
|
-
}
|
|
74
89
|
remove() {
|
|
75
|
-
const
|
|
90
|
+
const keys = this.#storage.getKeys();
|
|
91
|
+
delete keys[this.#keyID];
|
|
76
92
|
this.#storage.setKeys(keys);
|
|
77
93
|
this.#key = undefined;
|
|
78
94
|
}
|
|
95
|
+
async removeAsync() {
|
|
96
|
+
this.remove();
|
|
97
|
+
}
|
|
79
98
|
}
|
package/lib/index.d.ts
CHANGED
|
@@ -9,6 +9,6 @@
|
|
|
9
9
|
*
|
|
10
10
|
* @module electron-keystore
|
|
11
11
|
*/
|
|
12
|
-
export { ElectronKeyEntry } from './entry.js';
|
|
13
|
-
export {
|
|
14
|
-
export {
|
|
12
|
+
export { ElectronKeyEntry, type ElectronKeyEntryParams } from './entry.js';
|
|
13
|
+
export { ElectronKeyStore, type ElectronKeyStoreParams } from './store.js';
|
|
14
|
+
export type { KeyStorage } from './types.js';
|
package/lib/index.js
CHANGED
package/lib/store.d.ts
CHANGED
|
@@ -1,10 +1,30 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type FullIdentity, type IdentityProvider, type KeyStore } from '@kokuin/token';
|
|
2
2
|
import { ElectronKeyEntry } from './entry.js';
|
|
3
|
-
export
|
|
3
|
+
export type ElectronKeyStoreParams = {
|
|
4
|
+
/** The electron-store file name. Defaults to `'keystore'`. */
|
|
5
|
+
name?: string;
|
|
6
|
+
allowInsecureStorage?: boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Path to a lockfile enabling **cross-process** exclusion on `provideAsync`.
|
|
9
|
+
*
|
|
10
|
+
* Absent, nothing touches the filesystem and only the in-process lock applies — two
|
|
11
|
+
* processes can still both generate a key for a fresh keyID, and the loser's key is lost.
|
|
12
|
+
*
|
|
13
|
+
* A **file**, not a directory: one coarse lock per store, not one per keyID. A per-keyID
|
|
14
|
+
* lockfile would derive its name from an attacker-influenced keyID (`entry('../../etc/x')`),
|
|
15
|
+
* and `provideAsync` runs once per identity, so serializing across keyIDs costs nothing real.
|
|
16
|
+
*
|
|
17
|
+
* Must be on a local filesystem (`link()` is not atomic on NFS). Acquisition is bounded and
|
|
18
|
+
* throws `TimeoutInterruption` on expiry — it never proceeds unlocked.
|
|
19
|
+
*/
|
|
20
|
+
lockPath?: string;
|
|
21
|
+
};
|
|
22
|
+
export declare class ElectronKeyStore implements KeyStore<Uint8Array, ElectronKeyEntry>, IdentityProvider<FullIdentity> {
|
|
4
23
|
#private;
|
|
5
|
-
static open(
|
|
6
|
-
|
|
7
|
-
}): ElectronKeyStore;
|
|
8
|
-
constructor(name: string, allowInsecureStorage?: boolean);
|
|
24
|
+
static open(params?: ElectronKeyStoreParams): ElectronKeyStore;
|
|
25
|
+
constructor(params?: ElectronKeyStoreParams);
|
|
9
26
|
entry(keyID: string): ElectronKeyEntry;
|
|
27
|
+
provideIdentity(keyID: string): Promise<FullIdentity>;
|
|
28
|
+
/** {@link provideIdentity}, synchronously. Beyond the `IdentityProvider` contract. */
|
|
29
|
+
provideIdentitySync(keyID: string): FullIdentity;
|
|
10
30
|
}
|
package/lib/store.js
CHANGED
|
@@ -1,23 +1,40 @@
|
|
|
1
|
+
import { createTracer, KokuinAttributeKeys, KokuinSpanNames } from '@kokuin/otel';
|
|
2
|
+
import { createFullIdentity } from '@kokuin/token';
|
|
3
|
+
import { getLogger } from '@sozai/log';
|
|
4
|
+
import { withSpan, withSyncSpan } from '@sozai/otel';
|
|
1
5
|
import Store from 'electron-store';
|
|
2
6
|
import { ElectronKeyEntry } from './entry.js';
|
|
7
|
+
const tracer = createTracer('keystore.electron');
|
|
8
|
+
const logger = getLogger([
|
|
9
|
+
'kokuin',
|
|
10
|
+
'electron'
|
|
11
|
+
]);
|
|
3
12
|
export class ElectronKeyStore {
|
|
4
13
|
static #byName = Object.create(null);
|
|
5
|
-
static open(
|
|
14
|
+
static open(params) {
|
|
15
|
+
const name = params?.name ?? 'keystore';
|
|
6
16
|
const cached = ElectronKeyStore.#byName[name];
|
|
7
17
|
if (cached == null) {
|
|
8
|
-
ElectronKeyStore.#byName[name] = new ElectronKeyStore(
|
|
9
|
-
|
|
10
|
-
throw new Error(`ElectronKeyStore.open('${name}') was already opened with allowInsecureStorage: ` + `${cached.#allowInsecureStorage}; cannot reopen with conflicting allowInsecureStorage: ` + `${options.allowInsecureStorage}.`);
|
|
18
|
+
ElectronKeyStore.#byName[name] = new ElectronKeyStore(params);
|
|
19
|
+
return ElectronKeyStore.#byName[name];
|
|
11
20
|
}
|
|
12
|
-
|
|
21
|
+
if (params?.allowInsecureStorage != null && params.allowInsecureStorage !== cached.#allowInsecureStorage) {
|
|
22
|
+
throw new Error(`ElectronKeyStore.open('${name}') was already opened with allowInsecureStorage: ` + `${cached.#allowInsecureStorage}; cannot reopen with conflicting allowInsecureStorage: ` + `${params.allowInsecureStorage}.`);
|
|
23
|
+
}
|
|
24
|
+
if (params?.lockPath != null && params.lockPath !== cached.#lockPath) {
|
|
25
|
+
throw new Error(`ElectronKeyStore.open('${name}') was already opened with lockPath: ` + `${String(cached.#lockPath)}; cannot reopen with conflicting lockPath: ${params.lockPath}.`);
|
|
26
|
+
}
|
|
27
|
+
return cached;
|
|
13
28
|
}
|
|
14
29
|
#entries = Object.create(null);
|
|
15
30
|
#storage;
|
|
16
31
|
#allowInsecureStorage;
|
|
17
|
-
|
|
18
|
-
|
|
32
|
+
#lockPath;
|
|
33
|
+
constructor(params){
|
|
34
|
+
this.#allowInsecureStorage = params?.allowInsecureStorage ?? false;
|
|
35
|
+
this.#lockPath = params?.lockPath;
|
|
19
36
|
const store = new Store({
|
|
20
|
-
name,
|
|
37
|
+
name: params?.name ?? 'keystore',
|
|
21
38
|
schema: {
|
|
22
39
|
keys: {
|
|
23
40
|
type: 'object',
|
|
@@ -33,12 +50,61 @@ export class ElectronKeyStore {
|
|
|
33
50
|
}
|
|
34
51
|
});
|
|
35
52
|
this.#storage = {
|
|
36
|
-
|
|
37
|
-
|
|
53
|
+
// Copy onto a null prototype: the object electron-store hands back is JSON-derived and
|
|
54
|
+
// inherits Object.prototype, which makes '__proto__' and 'constructor' hazardous keyIDs.
|
|
55
|
+
getKeys: ()=>Object.assign(Object.create(null), store.get('keys', {})),
|
|
56
|
+
setKeys: (keys)=>store.set('keys', {
|
|
57
|
+
...keys
|
|
58
|
+
})
|
|
38
59
|
};
|
|
39
60
|
}
|
|
40
61
|
entry(keyID) {
|
|
41
|
-
this.#entries[keyID] ??= new ElectronKeyEntry(
|
|
62
|
+
this.#entries[keyID] ??= new ElectronKeyEntry({
|
|
63
|
+
storage: this.#storage,
|
|
64
|
+
keyID,
|
|
65
|
+
allowInsecureStorage: this.#allowInsecureStorage,
|
|
66
|
+
lockPath: this.#lockPath
|
|
67
|
+
});
|
|
42
68
|
return this.#entries[keyID];
|
|
43
69
|
}
|
|
70
|
+
async provideIdentity(keyID) {
|
|
71
|
+
return withSpan(tracer, KokuinSpanNames.KEYSTORE_GET_OR_CREATE, {
|
|
72
|
+
attributes: {
|
|
73
|
+
[KokuinAttributeKeys.KEYSTORE_STORE_TYPE]: 'electron'
|
|
74
|
+
}
|
|
75
|
+
}, async (span)=>{
|
|
76
|
+
const entry = this.entry(keyID);
|
|
77
|
+
const existing = await entry.getAsync();
|
|
78
|
+
const privateKey = existing ?? await entry.provideAsync();
|
|
79
|
+
const identity = createFullIdentity(privateKey);
|
|
80
|
+
span.setAttribute(KokuinAttributeKeys.AUTH_DID, identity.id);
|
|
81
|
+
span.setAttribute(KokuinAttributeKeys.KEYSTORE_KEY_CREATED, existing == null);
|
|
82
|
+
if (existing == null) {
|
|
83
|
+
logger.info('New identity generated: {did}', {
|
|
84
|
+
did: identity.id
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return identity;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/** {@link provideIdentity}, synchronously. Beyond the `IdentityProvider` contract. */ provideIdentitySync(keyID) {
|
|
91
|
+
return withSyncSpan(tracer, KokuinSpanNames.KEYSTORE_GET_OR_CREATE, {
|
|
92
|
+
attributes: {
|
|
93
|
+
[KokuinAttributeKeys.KEYSTORE_STORE_TYPE]: 'electron'
|
|
94
|
+
}
|
|
95
|
+
}, (span)=>{
|
|
96
|
+
const entry = this.entry(keyID);
|
|
97
|
+
const existing = entry.get();
|
|
98
|
+
const privateKey = existing ?? entry.provide();
|
|
99
|
+
const identity = createFullIdentity(privateKey);
|
|
100
|
+
span.setAttribute(KokuinAttributeKeys.AUTH_DID, identity.id);
|
|
101
|
+
span.setAttribute(KokuinAttributeKeys.KEYSTORE_KEY_CREATED, existing == null);
|
|
102
|
+
if (existing == null) {
|
|
103
|
+
logger.info('New identity generated: {did}', {
|
|
104
|
+
did: identity.id
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return identity;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
44
110
|
}
|
package/lib/types.d.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The persisted keyID → encrypted-key map.
|
|
3
|
+
*
|
|
4
|
+
* `getKeys` MUST return a **null-prototype** object. A plain `{}` inherits from
|
|
5
|
+
* `Object.prototype`, so `keys['constructor']` reads back a function rather than `undefined`,
|
|
6
|
+
* and `keys['__proto__'] = value` sets the prototype instead of creating an own property —
|
|
7
|
+
* silently discarding the key. With a null prototype both behave as ordinary string keys.
|
|
8
|
+
*/
|
|
1
9
|
export type KeyStorage = {
|
|
2
10
|
getKeys: () => Record<string, string>;
|
|
3
11
|
setKeys: (keys: Record<string, string>) => void;
|
package/lib/types.js
CHANGED
|
@@ -1 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The persisted keyID → encrypted-key map.
|
|
3
|
+
*
|
|
4
|
+
* `getKeys` MUST return a **null-prototype** object. A plain `{}` inherits from
|
|
5
|
+
* `Object.prototype`, so `keys['constructor']` reads back a function rather than `undefined`,
|
|
6
|
+
* and `keys['__proto__'] = value` sets the prototype instead of creating an own property —
|
|
7
|
+
* silently discarding the key. With a null prototype both behave as ordinary string keys.
|
|
8
|
+
*/ export { };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kokuin/electron",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Electron keystore",
|
|
6
6
|
"keywords": [
|
|
@@ -26,14 +26,16 @@
|
|
|
26
26
|
"sideEffects": false,
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@sozai/codec": "^0.2.0",
|
|
29
|
-
"@sozai/
|
|
30
|
-
"@sozai/
|
|
29
|
+
"@sozai/lock": "^0.1.0",
|
|
30
|
+
"@sozai/log": "^0.2.0",
|
|
31
|
+
"@sozai/otel": "^0.3.0",
|
|
31
32
|
"electron-store": "^11.0.2",
|
|
32
|
-
"@kokuin/otel": "^0.
|
|
33
|
-
"@kokuin/token": "^0.
|
|
33
|
+
"@kokuin/otel": "^0.3.0",
|
|
34
|
+
"@kokuin/token": "^0.3.0"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
36
|
-
"electron": "^43.1.0"
|
|
37
|
+
"electron": "^43.1.0",
|
|
38
|
+
"@kokuin/keystore-conformance": "^0.0.0"
|
|
37
39
|
},
|
|
38
40
|
"scripts": {
|
|
39
41
|
"build:clean": "del lib",
|
package/lib/identity.d.ts
DELETED
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import { type FullIdentity } from '@kokuin/token';
|
|
2
|
-
import { ElectronKeyStore } from './store.js';
|
|
3
|
-
export declare function provideFullIdentity(store: ElectronKeyStore | string, keyID: string): FullIdentity;
|
|
4
|
-
export declare function provideFullIdentityAsync(store: ElectronKeyStore | string, keyID: string): Promise<FullIdentity>;
|
package/lib/identity.js
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { createTracer, KokuinAttributeKeys, KokuinSpanNames } from '@kokuin/otel';
|
|
2
|
-
import { createFullIdentity, decodePrivateKey } from '@kokuin/token';
|
|
3
|
-
import { getLogger } from '@sozai/log';
|
|
4
|
-
import { withSpan, withSyncSpan } from '@sozai/otel';
|
|
5
|
-
import { ElectronKeyStore } from './store.js';
|
|
6
|
-
const tracer = createTracer('keystore.electron');
|
|
7
|
-
const logger = getLogger([
|
|
8
|
-
'kokuin',
|
|
9
|
-
'electron'
|
|
10
|
-
]);
|
|
11
|
-
function getStore(store) {
|
|
12
|
-
return typeof store === 'string' ? ElectronKeyStore.open(store) : store;
|
|
13
|
-
}
|
|
14
|
-
export function provideFullIdentity(store, keyID) {
|
|
15
|
-
return withSyncSpan(tracer, KokuinSpanNames.KEYSTORE_GET_OR_CREATE, {
|
|
16
|
-
attributes: {
|
|
17
|
-
[KokuinAttributeKeys.KEYSTORE_STORE_TYPE]: 'electron'
|
|
18
|
-
}
|
|
19
|
-
}, (span)=>{
|
|
20
|
-
const entry = getStore(store).entry(keyID);
|
|
21
|
-
const existing = entry.get();
|
|
22
|
-
if (existing != null) {
|
|
23
|
-
const identity = createFullIdentity(decodePrivateKey(existing));
|
|
24
|
-
span.setAttribute(KokuinAttributeKeys.AUTH_DID, identity.id);
|
|
25
|
-
span.setAttribute(KokuinAttributeKeys.KEYSTORE_KEY_CREATED, false);
|
|
26
|
-
return identity;
|
|
27
|
-
}
|
|
28
|
-
const key = entry.provide();
|
|
29
|
-
const identity = createFullIdentity(decodePrivateKey(key));
|
|
30
|
-
span.setAttribute(KokuinAttributeKeys.AUTH_DID, identity.id);
|
|
31
|
-
span.setAttribute(KokuinAttributeKeys.KEYSTORE_KEY_CREATED, true);
|
|
32
|
-
logger.info('New identity generated: {did}', {
|
|
33
|
-
did: identity.id
|
|
34
|
-
});
|
|
35
|
-
return identity;
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
export async function provideFullIdentityAsync(store, keyID) {
|
|
39
|
-
return withSpan(tracer, KokuinSpanNames.KEYSTORE_GET_OR_CREATE, {
|
|
40
|
-
attributes: {
|
|
41
|
-
[KokuinAttributeKeys.KEYSTORE_STORE_TYPE]: 'electron'
|
|
42
|
-
}
|
|
43
|
-
}, async (span)=>{
|
|
44
|
-
const entry = getStore(store).entry(keyID);
|
|
45
|
-
const existing = await entry.getAsync();
|
|
46
|
-
if (existing != null) {
|
|
47
|
-
const identity = createFullIdentity(decodePrivateKey(existing));
|
|
48
|
-
span.setAttribute(KokuinAttributeKeys.AUTH_DID, identity.id);
|
|
49
|
-
span.setAttribute(KokuinAttributeKeys.KEYSTORE_KEY_CREATED, false);
|
|
50
|
-
return identity;
|
|
51
|
-
}
|
|
52
|
-
const key = await entry.provideAsync();
|
|
53
|
-
const identity = createFullIdentity(decodePrivateKey(key));
|
|
54
|
-
span.setAttribute(KokuinAttributeKeys.AUTH_DID, identity.id);
|
|
55
|
-
span.setAttribute(KokuinAttributeKeys.KEYSTORE_KEY_CREATED, true);
|
|
56
|
-
logger.info('New identity generated: {did}', {
|
|
57
|
-
did: identity.id
|
|
58
|
-
});
|
|
59
|
-
return identity;
|
|
60
|
-
});
|
|
61
|
-
}
|