@wishknish/knishio-client-ts 1.0.0 → 1.2.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 +13 -0
- package/dist/index.cjs +1304 -121
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +152 -18
- package/dist/index.d.ts +152 -18
- package/dist/index.iife.js +3044 -1049
- package/dist/index.iife.js.map +1 -1
- package/dist/index.js +1295 -122
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/exception/SecretStorageException.ts +9 -0
- package/src/index.ts +16 -2
- package/src/libraries/GraphQLClient.ts +18 -5
- package/src/storage/FileStorageBackend.ts +176 -0
- package/src/storage/MemorySecretStorageProvider.ts +75 -3
- package/src/storage/NonExtractableKeySecretStorageProvider.ts +547 -0
- package/src/storage/WebAuthnPrfSecretStorageProvider.ts +679 -0
- package/src/storage/WebCryptoSecretStorageProvider.ts +105 -134
- package/src/storage/WebStorageBackend.ts +102 -0
- package/src/storage/index.ts +26 -2
- package/src/storage/secretEnvelope.ts +200 -0
- package/src/types/storage.ts +22 -2
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/*
|
|
2
|
+
(
|
|
3
|
+
(/(
|
|
4
|
+
(//(
|
|
5
|
+
(///(
|
|
6
|
+
(/////(
|
|
7
|
+
(//////( )
|
|
8
|
+
(////////( (/)
|
|
9
|
+
(////////( (///)
|
|
10
|
+
(//////////( (////)
|
|
11
|
+
(//////////( (//////)
|
|
12
|
+
(////////////( (///////)
|
|
13
|
+
(/////////////( (/////////)
|
|
14
|
+
(//////////////( (///////////)
|
|
15
|
+
(///////////////( (/////////////)
|
|
16
|
+
(////////////////( (//////////////)
|
|
17
|
+
((((((((((((((((((( (((((((((((((((
|
|
18
|
+
((((((((((((((((((( ((((((((((((((
|
|
19
|
+
((((((((((((((((((( ((((((((((((((
|
|
20
|
+
(((((((((((((((((((( (((((((((((((
|
|
21
|
+
(((((((((((((((((((( ((((((((((((
|
|
22
|
+
((((((((((((((((((( ((((((((((((
|
|
23
|
+
((((((((((((((((((( ((((((((((
|
|
24
|
+
((((((((((((((((((/ (((((((((
|
|
25
|
+
(((((((((((((((((( ((((((((
|
|
26
|
+
((((((((((((((((( (((((((
|
|
27
|
+
(((((((((((((((((( (((((
|
|
28
|
+
################# ##
|
|
29
|
+
################ #
|
|
30
|
+
################# ##
|
|
31
|
+
%################ ###
|
|
32
|
+
###############( ####
|
|
33
|
+
############### ####
|
|
34
|
+
############### ######
|
|
35
|
+
%#############( (#######
|
|
36
|
+
%############# #########
|
|
37
|
+
############( ##########
|
|
38
|
+
########### #############
|
|
39
|
+
######### ##############
|
|
40
|
+
%######
|
|
41
|
+
|
|
42
|
+
Powered by Knish.IO: Connecting a Decentralized World
|
|
43
|
+
|
|
44
|
+
Please visit https://github.com/WishKnish/KnishIO-Client-TS for information.
|
|
45
|
+
|
|
46
|
+
License: https://github.com/WishKnish/KnishIO-Client-TS/blob/master/LICENSE
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
import type { EncryptedSecretPayload, SecretStorageMetadata } from '@/types/storage'
|
|
50
|
+
import SecretStorageException from '@/exception/SecretStorageException'
|
|
51
|
+
import { zeroizeBytes } from '@/libraries/secureMemory'
|
|
52
|
+
|
|
53
|
+
export const ENVELOPE_ALGORITHM = 'AES-GCM'
|
|
54
|
+
export const DEFAULT_ITERATIONS = 100000
|
|
55
|
+
export const SECRET_KEY_PREFIX = 'knishio:secret:'
|
|
56
|
+
export const RECOVERY_KEY_PREFIX = 'knishio:recovery:'
|
|
57
|
+
const GCM_IV_LENGTH = 12
|
|
58
|
+
const SALT_LENGTH = 16
|
|
59
|
+
|
|
60
|
+
const textEncoder = new TextEncoder()
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Helper to convert Uint8Array to base64
|
|
64
|
+
*/
|
|
65
|
+
export function uint8ArrayToBase64(bytes: Uint8Array): string {
|
|
66
|
+
let binary = ''
|
|
67
|
+
const len = bytes.byteLength
|
|
68
|
+
for (let i = 0; i < len; i++) {
|
|
69
|
+
const byte = bytes[i]
|
|
70
|
+
if (byte !== undefined) {
|
|
71
|
+
binary += String.fromCharCode(byte)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return btoa(binary)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Helper to convert base64 to Uint8Array
|
|
79
|
+
*/
|
|
80
|
+
export function base64ToUint8Array(base64: string): Uint8Array {
|
|
81
|
+
const binary = atob(base64)
|
|
82
|
+
const len = binary.length
|
|
83
|
+
const bytes = new Uint8Array(len)
|
|
84
|
+
for (let i = 0; i < len; i++) {
|
|
85
|
+
bytes[i] = binary.charCodeAt(i)
|
|
86
|
+
}
|
|
87
|
+
return bytes
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Derive an AES-GCM CryptoKey from a passphrase and salt using PBKDF2
|
|
92
|
+
*/
|
|
93
|
+
export async function deriveEnvelopeKey(
|
|
94
|
+
passphrase: string,
|
|
95
|
+
salt: Uint8Array,
|
|
96
|
+
iterations = DEFAULT_ITERATIONS
|
|
97
|
+
): Promise<CryptoKey> {
|
|
98
|
+
if (typeof globalThis.crypto === 'undefined' || typeof globalThis.crypto.subtle === 'undefined') {
|
|
99
|
+
throw new SecretStorageException('WebCrypto API is not available')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const passphraseBytes = textEncoder.encode(passphrase)
|
|
103
|
+
try {
|
|
104
|
+
const baseKey = await globalThis.crypto.subtle.importKey(
|
|
105
|
+
'raw',
|
|
106
|
+
passphraseBytes,
|
|
107
|
+
'PBKDF2',
|
|
108
|
+
false,
|
|
109
|
+
['deriveKey']
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return await globalThis.crypto.subtle.deriveKey(
|
|
113
|
+
{
|
|
114
|
+
name: 'PBKDF2',
|
|
115
|
+
salt: salt as BufferSource,
|
|
116
|
+
iterations,
|
|
117
|
+
hash: 'SHA-256'
|
|
118
|
+
},
|
|
119
|
+
baseKey,
|
|
120
|
+
{ name: 'AES-GCM', length: 256 },
|
|
121
|
+
false,
|
|
122
|
+
['encrypt', 'decrypt']
|
|
123
|
+
)
|
|
124
|
+
} finally {
|
|
125
|
+
zeroizeBytes(passphraseBytes)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Seal a secret string into an EncryptedSecretPayload envelope
|
|
131
|
+
*/
|
|
132
|
+
export async function sealEnvelope(
|
|
133
|
+
secret: string,
|
|
134
|
+
passphrase: string,
|
|
135
|
+
metadata: SecretStorageMetadata
|
|
136
|
+
): Promise<EncryptedSecretPayload> {
|
|
137
|
+
if (typeof globalThis.crypto === 'undefined' || typeof globalThis.crypto.subtle === 'undefined') {
|
|
138
|
+
throw new SecretStorageException('WebCrypto API is not available')
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const salt = new Uint8Array(SALT_LENGTH)
|
|
142
|
+
const iv = new Uint8Array(GCM_IV_LENGTH)
|
|
143
|
+
globalThis.crypto.getRandomValues(salt)
|
|
144
|
+
globalThis.crypto.getRandomValues(iv)
|
|
145
|
+
|
|
146
|
+
const key = await deriveEnvelopeKey(passphrase, salt, DEFAULT_ITERATIONS)
|
|
147
|
+
const secretBytes = textEncoder.encode(secret)
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
const encryptedBuffer = await globalThis.crypto.subtle.encrypt(
|
|
151
|
+
{
|
|
152
|
+
name: ENVELOPE_ALGORITHM,
|
|
153
|
+
iv: iv as BufferSource
|
|
154
|
+
},
|
|
155
|
+
key,
|
|
156
|
+
secretBytes
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
const ciphertext = uint8ArrayToBase64(new Uint8Array(encryptedBuffer))
|
|
160
|
+
return {
|
|
161
|
+
version: 1,
|
|
162
|
+
ciphertext,
|
|
163
|
+
iv: uint8ArrayToBase64(iv),
|
|
164
|
+
salt: uint8ArrayToBase64(salt),
|
|
165
|
+
algorithm: ENVELOPE_ALGORITHM,
|
|
166
|
+
iterations: DEFAULT_ITERATIONS,
|
|
167
|
+
metadata
|
|
168
|
+
}
|
|
169
|
+
} finally {
|
|
170
|
+
zeroizeBytes(secretBytes)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Open an EncryptedSecretPayload envelope with a passphrase, returning the decrypted secret bytes
|
|
176
|
+
*/
|
|
177
|
+
export async function openEnvelope(
|
|
178
|
+
payload: EncryptedSecretPayload,
|
|
179
|
+
passphrase: string
|
|
180
|
+
): Promise<Uint8Array> {
|
|
181
|
+
if (typeof globalThis.crypto === 'undefined' || typeof globalThis.crypto.subtle === 'undefined') {
|
|
182
|
+
throw new SecretStorageException('WebCrypto API is not available')
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const salt = base64ToUint8Array(payload.salt)
|
|
186
|
+
const iv = base64ToUint8Array(payload.iv)
|
|
187
|
+
const ciphertext = base64ToUint8Array(payload.ciphertext)
|
|
188
|
+
|
|
189
|
+
const key = await deriveEnvelopeKey(passphrase, salt, payload.iterations ?? DEFAULT_ITERATIONS)
|
|
190
|
+
const decryptedBuffer = await globalThis.crypto.subtle.decrypt(
|
|
191
|
+
{
|
|
192
|
+
name: ENVELOPE_ALGORITHM,
|
|
193
|
+
iv: iv as BufferSource
|
|
194
|
+
},
|
|
195
|
+
key,
|
|
196
|
+
ciphertext as BufferSource
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
return new Uint8Array(decryptedBuffer)
|
|
200
|
+
}
|
package/src/types/storage.ts
CHANGED
|
@@ -69,6 +69,13 @@ export interface EncryptedSecretPayload {
|
|
|
69
69
|
iterations?: number
|
|
70
70
|
metadata: SecretStorageMetadata
|
|
71
71
|
}
|
|
72
|
+
export interface StorageOptions {
|
|
73
|
+
label?: string
|
|
74
|
+
passphrase?: string
|
|
75
|
+
recoveryPassphrase?: string
|
|
76
|
+
allowUnrecoverable?: boolean
|
|
77
|
+
}
|
|
78
|
+
|
|
72
79
|
|
|
73
80
|
export interface ISecretStorageProvider {
|
|
74
81
|
/**
|
|
@@ -77,7 +84,11 @@ export interface ISecretStorageProvider {
|
|
|
77
84
|
readonly providerType: string
|
|
78
85
|
|
|
79
86
|
/**
|
|
80
|
-
*
|
|
87
|
+
* True only when this provider holds a non-exportable key inside platform-secure
|
|
88
|
+
* hardware (Android TEE/StrongBox, Secure Enclave, TPM) and learned that from the
|
|
89
|
+
* platform itself — never from a caller argument. Software envelope providers
|
|
90
|
+
* return false. The value is persisted as `metadata.hardwareBacked` in every
|
|
91
|
+
* envelope this provider writes.
|
|
81
92
|
*/
|
|
82
93
|
isHardwareBacked(): boolean
|
|
83
94
|
|
|
@@ -92,7 +103,7 @@ export interface ISecretStorageProvider {
|
|
|
92
103
|
storeSecret(
|
|
93
104
|
bundleHash: string,
|
|
94
105
|
secret: string,
|
|
95
|
-
options?:
|
|
106
|
+
options?: StorageOptions
|
|
96
107
|
): Promise<void>
|
|
97
108
|
|
|
98
109
|
/**
|
|
@@ -126,4 +137,13 @@ export interface ISecretStorageProvider {
|
|
|
126
137
|
fn: (secret: string) => Promise<T> | T,
|
|
127
138
|
options?: { passphrase?: string }
|
|
128
139
|
): Promise<T>
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Recover a secret using its recovery envelope and re-enroll it under a fresh KEK
|
|
143
|
+
*/
|
|
144
|
+
recoverSecret(
|
|
145
|
+
bundleHash: string,
|
|
146
|
+
recoveryPassphrase: string,
|
|
147
|
+
options?: { label?: string }
|
|
148
|
+
): Promise<void>
|
|
129
149
|
}
|