@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.
@@ -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
+ }
@@ -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
- * Whether this provider is backed by hardware (TPM, Secure Enclave, StrongBox)
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?: { label?: string; passphrase?: string }
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
  }