@time-file/browser-file-crypto 1.0.0 → 1.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.ko.md CHANGED
@@ -1,10 +1,18 @@
1
1
  # @time-file/browser-file-crypto
2
2
 
3
3
  <p align="center">
4
- <img src="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/og-image.png" alt="browser-file-crypto" width="100%" />
4
+ <picture>
5
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/og-image-dark.png" />
6
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/og-image.png" />
7
+ <img src="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/og-image.png" alt="browser-file-crypto" width="100%" />
8
+ </picture>
5
9
  </p>
6
10
  <p align="center">
7
- <img src="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/hero.ko.png" alt="Hero" width="100%" />
11
+ <picture>
12
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/hero-dark.ko.png" />
13
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/hero.ko.png" />
14
+ <img src="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/hero.ko.png" alt="Hero" width="100%" />
15
+ </picture>
8
16
  </p>
9
17
 
10
18
  <p align="center">
@@ -19,22 +27,27 @@
19
27
  </p>
20
28
 
21
29
  <p align="center">
22
- <a href="./README.md">English</a> | <strong>한국어</strong>
30
+ <a href="https://github.com/Time-File/browser-file-crypto/blob/main/README.md">English</a> | <strong>한국어</strong>
23
31
  </p>
24
32
 
25
33
  ## 특징
26
34
 
27
35
  <p align="center">
28
- <img src="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/features-grid.ko.png" alt="Features" width="100%" />
36
+ <picture>
37
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/features-grid-dark.ko.png" />
38
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/features-grid.ko.png" />
39
+ <img src="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/features-grid.ko.png" alt="Features" width="100%" />
40
+ </picture>
29
41
  </p>
30
42
 
31
43
  - **Zero-Knowledge** - 클라이언트 측 암호화로 서버는 평문을 볼 수 없습니다.
32
44
  - **Zero-Dependency** - 네이티브 Web Crypto API만 사용합니다.
33
45
  - **AES-256-GCM** - 업계 표준 인증 암호화 방식입니다.
34
46
  - **비밀번호 & 키파일** - 용도에 따라 두 가지 모드를 지원합니다.
47
+ - **스트리밍 지원** - 메모리 효율적인 대용량 파일 처리 (v1.1.0+)
35
48
  - **진행률 콜백** - 암호화/복호화 진행 상황을 추적할 수 있습니다.
36
49
  - **TypeScript** - 완전한 타입 정의가 포함되어 있습니다.
37
- - **초경량** - gzip 압축 시 4KB 미만의 용량을 자랑합니다.
50
+ - **초경량** - gzip 압축 시 5KB 미만의 용량을 자랑합니다.
38
51
 
39
52
  ## 왜 사용해야 하나요?
40
53
 
@@ -150,6 +163,32 @@ const decrypted = await decryptFile(encrypted, {
150
163
  });
151
164
  ```
152
165
 
166
+ ### 스트리밍 암호화 (v1.1.0+)
167
+
168
+ 메모리에 맞지 않는 대용량 파일을 처리할 때:
169
+
170
+ ```typescript
171
+ import { encryptFileStream, decryptFileStream } from '@time-file/browser-file-crypto';
172
+
173
+ // 대용량 파일 암호화 (메모리 효율적)
174
+ const encryptedStream = await encryptFileStream(largeFile, {
175
+ password: 'secret',
176
+ chunkSize: 1024 * 1024, // 1MB 청크 (기본값: 64KB)
177
+ onProgress: ({ processedBytes, totalBytes }) => {
178
+ console.log(`${Math.round(processedBytes / totalBytes * 100)}%`);
179
+ }
180
+ });
181
+
182
+ // 스트림을 Blob으로 변환
183
+ const response = new Response(encryptedStream);
184
+ const encryptedBlob = await response.blob();
185
+
186
+ // 복호화
187
+ const decryptedStream = decryptFileStream(encryptedBlob, { password: 'secret' });
188
+ const decryptResponse = new Response(decryptedStream);
189
+ const decryptedBlob = await decryptResponse.blob();
190
+ ```
191
+
153
192
  ### 키파일 모드
154
193
 
155
194
  비밀번호를 기억할 필요가 없습니다:
@@ -178,7 +217,7 @@ if (loaded) {
178
217
  ```typescript
179
218
  import { getEncryptionType, isEncryptedFile, generateRandomPassword } from '@time-file/browser-file-crypto';
180
219
 
181
- await getEncryptionType(blob); // 'password' | 'keyfile' | 'unknown'
220
+ await getEncryptionType(blob); // 'password' | 'keyfile' | 'password-stream' | 'keyfile-stream' | 'unknown'
182
221
  await isEncryptedFile(blob); // true | false
183
222
  generateRandomPassword(24); // 'Kx9#mP2$vL5@nQ8!...'
184
223
  ```
@@ -224,7 +263,11 @@ try {
224
263
  ### 파일 포맷
225
264
 
226
265
  <p align="center">
227
- <img src="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/file-format.ko.png" alt="File Format" width="100%" />
266
+ <picture>
267
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/file-format-dark.ko.png" />
268
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/file-format.ko.png" />
269
+ <img src="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/file-format.ko.png" alt="File Format" width="100%" />
270
+ </picture>
228
271
  </p>
229
272
 
230
273
  ```
@@ -233,6 +276,15 @@ try {
233
276
 
234
277
  키파일 암호화:
235
278
  => [0x02] + [iv:12] + [ciphertext + auth_tag:16]
279
+
280
+ 비밀번호 암호화 (스트리밍):
281
+ => [0x11] + [version:1] + [chunkSize:4] + [salt:16] + [baseIV:12] + [chunks...]
282
+
283
+ 키파일 암호화 (스트리밍):
284
+ => [0x12] + [version:1] + [chunkSize:4] + [baseIV:12] + [chunks...]
285
+
286
+ 각 스트리밍 청크:
287
+ => [chunkLength:4] + [ciphertext + auth_tag:16]
236
288
  ```
237
289
 
238
290
  ### 참고 사항
@@ -262,7 +314,11 @@ import type {
262
314
  Progress,
263
315
  KeyFile,
264
316
  EncryptionType,
265
- CryptoErrorCode
317
+ CryptoErrorCode,
318
+ // 스트리밍 타입 (v1.1.0+)
319
+ StreamEncryptOptions,
320
+ StreamDecryptOptions,
321
+ StreamProgress
266
322
  } from '@time-file/browser-file-crypto';
267
323
  ```
268
324
 
@@ -281,7 +337,10 @@ import type {
281
337
 
282
338
  <p align="center">
283
339
  <a href="https://timefile.co/ko">
284
- <img src="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/timefile-footer.ko.png#gh-light-mode-only" alt="Made by timefile.co" />
285
- <img src="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/timefile-footer-dark.ko.png#gh-dark-mode-only" alt="Made by timefile.co" />
340
+ <picture>
341
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/timefile-footer-dark.ko.png" />
342
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/timefile-footer.ko.png" />
343
+ <img src="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/timefile-footer.ko.png" alt="Made by timefile.co" />
344
+ </picture>
286
345
  </a>
287
346
  </p>
package/README.md CHANGED
@@ -1,12 +1,18 @@
1
1
  # @time-file/browser-file-crypto
2
2
 
3
3
  <p align="center">
4
- <img src="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/og-image.png#gh-light-mode-only" alt="browser-file-crypto" width="100%" />
5
- <img src="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/og-image-dark.png#gh-dark-mode-only" alt="browser-file-crypto" width="100%" />
4
+ <picture>
5
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/og-image-dark.png" />
6
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/og-image.png" />
7
+ <img src="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/og-image.png" alt="browser-file-crypto" width="100%" />
8
+ </picture>
6
9
  </p>
7
10
  <p align="center">
8
- <img src="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/encryption-structure.png#gh-light-mode-only" alt="Encryption Flow" width="100%" />
9
- <img src="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/encryption-structure-dark.png#gh-dark-mode-only" alt="Encryption Flow" width="100%" />
11
+ <picture>
12
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/encryption-structure-dark.png" />
13
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/encryption-structure.png" />
14
+ <img src="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/encryption-structure.png" alt="Encryption Flow" width="100%" />
15
+ </picture>
10
16
  </p>
11
17
 
12
18
  <p align="center">
@@ -21,23 +27,27 @@
21
27
  </p>
22
28
 
23
29
  <p align="center">
24
- <strong>English</strong> | <a href="./README.ko.md">한국어</a>
30
+ <strong>English</strong> | <a href="https://github.com/Time-File/browser-file-crypto/blob/main/README.ko.md">한국어</a>
25
31
  </p>
26
32
 
27
33
  ## Features
28
34
 
29
35
  <p align="center">
30
- <img src="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/features-grid.png#gh-light-mode-only" alt="Features" width="100%" />
31
- <img src="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/features-grid-dark.png#gh-dark-mode-only" alt="Features" width="100%" />
36
+ <picture>
37
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/features-grid-dark.png" />
38
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/features-grid.png" />
39
+ <img src="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/features-grid.png" alt="Features" width="100%" />
40
+ </picture>
32
41
  </p>
33
42
 
34
43
  - **Zero-Knowledge** - Client-side encryption, server never sees plaintext
35
44
  - **Zero-Dependency** - Native Web Crypto API only
36
45
  - **AES-256-GCM** - Industry-standard authenticated encryption
37
46
  - **Password & Keyfile** - Two modes for different use cases
47
+ - **Streaming Support** - Memory-efficient large file handling (v1.1.0+)
38
48
  - **Progress Callbacks** - Track encryption/decryption progress
39
49
  - **TypeScript** - Full type definitions
40
- - **Tiny** - < 4KB gzipped
50
+ - **Tiny** - < 5KB gzipped
41
51
 
42
52
  ## Why?
43
53
 
@@ -154,6 +164,32 @@ const decrypted = await decryptFile(encrypted, {
154
164
  });
155
165
  ```
156
166
 
167
+ ### Streaming Encryption (v1.1.0+)
168
+
169
+ For large files that don't fit in memory:
170
+
171
+ ```typescript
172
+ import { encryptFileStream, decryptFileStream } from '@time-file/browser-file-crypto';
173
+
174
+ // Encrypt large file (memory-efficient)
175
+ const encryptedStream = await encryptFileStream(largeFile, {
176
+ password: 'secret',
177
+ chunkSize: 1024 * 1024, // 1MB chunks (default: 64KB)
178
+ onProgress: ({ processedBytes, totalBytes }) => {
179
+ console.log(`${Math.round(processedBytes / totalBytes * 100)}%`);
180
+ }
181
+ });
182
+
183
+ // Convert stream to Blob
184
+ const response = new Response(encryptedStream);
185
+ const encryptedBlob = await response.blob();
186
+
187
+ // Decrypt
188
+ const decryptedStream = decryptFileStream(encryptedBlob, { password: 'secret' });
189
+ const decryptResponse = new Response(decryptedStream);
190
+ const decryptedBlob = await decryptResponse.blob();
191
+ ```
192
+
157
193
  ### Keyfile Mode
158
194
 
159
195
  No password to remember:
@@ -182,7 +218,7 @@ if (loaded) {
182
218
  ```typescript
183
219
  import { getEncryptionType, isEncryptedFile, generateRandomPassword } from '@time-file/browser-file-crypto';
184
220
 
185
- await getEncryptionType(blob); // 'password' | 'keyfile' | 'unknown'
221
+ await getEncryptionType(blob); // 'password' | 'keyfile' | 'password-stream' | 'keyfile-stream' | 'unknown'
186
222
  await isEncryptedFile(blob); // true | false
187
223
  generateRandomPassword(24); // 'Kx9#mP2$vL5@nQ8!...'
188
224
  ```
@@ -228,8 +264,11 @@ try {
228
264
  ### File Format
229
265
 
230
266
  <p align="center">
231
- <img src="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/file-format.png#gh-light-mode-only" alt="File Format" width="100%" />
232
- <img src="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/file-format-dark.png#gh-dark-mode-only" alt="File Format" width="100%" />
267
+ <picture>
268
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/file-format-dark.png" />
269
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/file-format.png" />
270
+ <img src="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/file-format.png" alt="File Format" width="100%" />
271
+ </picture>
233
272
  </p>
234
273
 
235
274
  ```
@@ -238,6 +277,15 @@ Password-encrypted
238
277
 
239
278
  Keyfile-encrypted
240
279
  => [0x02] + [iv:12] + [ciphertext + auth_tag:16]
280
+
281
+ Password-encrypted (streaming)
282
+ => [0x11] + [version:1] + [chunkSize:4] + [salt:16] + [baseIV:12] + [chunks...]
283
+
284
+ Keyfile-encrypted (streaming)
285
+ => [0x12] + [version:1] + [chunkSize:4] + [baseIV:12] + [chunks...]
286
+
287
+ Each streaming chunk:
288
+ => [chunkLength:4] + [ciphertext + auth_tag:16]
241
289
  ```
242
290
 
243
291
  ### Notes
@@ -267,7 +315,11 @@ import type {
267
315
  Progress,
268
316
  KeyFile,
269
317
  EncryptionType,
270
- CryptoErrorCode
318
+ CryptoErrorCode,
319
+ // Streaming types (v1.1.0+)
320
+ StreamEncryptOptions,
321
+ StreamDecryptOptions,
322
+ StreamProgress
271
323
  } from '@time-file/browser-file-crypto';
272
324
  ```
273
325
 
@@ -286,7 +338,10 @@ import type {
286
338
 
287
339
  <p align="center">
288
340
  <a href="https://timefile.co/en">
289
- <img src="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/timefile-footer.png#gh-light-mode-only" alt="Made by timefile.co" />
290
- <img src="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/timefile-footer-dark.png#gh-dark-mode-only" alt="Made by timefile.co" />
341
+ <picture>
342
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/timefile-footer-dark.png" />
343
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/timefile-footer.png" />
344
+ <img src="https://raw.githubusercontent.com/Time-File/browser-file-crypto/refs/heads/main/public/timefile-footer.png" alt="Made by timefile.co" />
345
+ </picture>
291
346
  </a>
292
347
  </p>
package/dist/index.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const l=16,p=12,_=256,N=1e5,m=32,d=1,h=2,D=16,R=1+l+p+D,L=1+p+D,f="AES-GCM",S="SHA-256",U={INVALID_INPUT:"Input must be a File, Blob, or ArrayBuffer.",PASSWORD_REQUIRED:"Password or keyfile is required for encryption.",KEYFILE_REQUIRED:"Keyfile is required to decrypt this file.",INVALID_PASSWORD:"Decryption failed: incorrect password.",INVALID_KEYFILE:"Decryption failed: incorrect keyfile.",INVALID_ENCRYPTED_DATA:"Invalid encrypted data: file may be corrupted.",ENCRYPTION_FAILED:"Encryption failed.",DECRYPTION_FAILED:"Decryption failed.",DOWNLOAD_FAILED:"File download failed.",UNSUPPORTED_FORMAT:"Unsupported encryption format."};class y extends Error{constructor(a,t){super(t??U[a]),this.name="CryptoError",this.code=a,Error.captureStackTrace&&Error.captureStackTrace(this,y)}}function C(n){return n instanceof y}async function w(n){if(n instanceof ArrayBuffer)return n;if(n instanceof Blob)return n.arrayBuffer();throw new y("INVALID_INPUT")}function E(n){return n.buffer.slice(n.byteOffset,n.byteOffset+n.byteLength)}function Y(n){const a=new Uint8Array(n);let t="";for(let e=0;e<a.byteLength;e++)t+=String.fromCharCode(a[e]);return btoa(t)}function b(n){const a=atob(n),t=new Uint8Array(a.length);for(let e=0;e<a.length;e++)t[e]=a.charCodeAt(e);return t.buffer}async function O(n,a){const t=new TextEncoder,e=await crypto.subtle.importKey("raw",t.encode(n),"PBKDF2",!1,["deriveKey"]);return crypto.subtle.deriveKey({name:"PBKDF2",salt:E(a),iterations:N,hash:S},e,{name:f,length:_},!1,["encrypt","decrypt"])}async function F(n){try{const a=b(n);return await crypto.subtle.importKey("raw",a,{name:f,length:_},!1,["encrypt","decrypt"])}catch{throw new y("INVALID_KEYFILE")}}function A(n){return crypto.getRandomValues(new Uint8Array(n))}function v(){return A(l)}function k(){return A(p)}const T="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*",H=16;function M(n=H){const a=Math.max(8,Math.min(128,n)),t=A(a);let e="";for(let i=0;i<a;i++){const c=t[i]%T.length;e+=T[c]}return e}async function B(n,a){const{password:t,keyData:e,onProgress:i}=a;if(!t&&!e)throw new y("PASSWORD_REQUIRED");try{i==null||i({phase:"deriving_key",progress:0});const c=await w(n);return i==null||i({phase:"deriving_key",progress:10}),e?await W(c,e,i):await G(c,t,i)}catch(c){throw c instanceof y?c:new y("ENCRYPTION_FAILED")}}async function G(n,a,t){const e=v(),i=k();t==null||t({phase:"deriving_key",progress:20});const c=await O(a,e);t==null||t({phase:"encrypting",progress:30});const r=await crypto.subtle.encrypt({name:f,iv:E(i)},c,n);t==null||t({phase:"encrypting",progress:90});const o=new Uint8Array(1+l+p+r.byteLength);return o[0]=d,o.set(e,1),o.set(i,1+l),o.set(new Uint8Array(r),1+l+p),t==null||t({phase:"complete",progress:100}),new Blob([o],{type:"application/octet-stream"})}async function W(n,a,t){const e=k();t==null||t({phase:"deriving_key",progress:20});const i=await F(a);t==null||t({phase:"encrypting",progress:30});const c=await crypto.subtle.encrypt({name:f,iv:E(e)},i,n);t==null||t({phase:"encrypting",progress:90});const r=new Uint8Array(1+p+c.byteLength);return r[0]=h,r.set(e,1),r.set(new Uint8Array(c),1+p),t==null||t({phase:"complete",progress:100}),new Blob([r],{type:"application/octet-stream"})}async function K(n,a){const{password:t,keyData:e,onProgress:i}=a;try{i==null||i({phase:"decrypting",progress:0});const c=new Uint8Array(await w(n));if(i==null||i({phase:"decrypting",progress:5}),c.length<1)throw new y("INVALID_ENCRYPTED_DATA");const r=c[0];if(r===d){if(!t)throw new y("PASSWORD_REQUIRED");return await V(c,t,i)}else if(r===h){if(!e)throw new y("KEYFILE_REQUIRED");return await x(c,e,i)}else throw new y("UNSUPPORTED_FORMAT")}catch(c){throw c instanceof y?c:new y("DECRYPTION_FAILED")}}async function V(n,a,t){if(n.length<R)throw new y("INVALID_ENCRYPTED_DATA");t==null||t({phase:"deriving_key",progress:10});const e=n.slice(1,1+l),i=n.slice(1+l,1+l+p),c=n.slice(1+l+p),r=await O(a,e);t==null||t({phase:"decrypting",progress:30});try{const o=await crypto.subtle.decrypt({name:f,iv:E(i)},r,c);return t==null||t({phase:"complete",progress:100}),new Blob([o])}catch{throw new y("INVALID_PASSWORD")}}async function x(n,a,t){if(n.length<L)throw new y("INVALID_ENCRYPTED_DATA");t==null||t({phase:"decrypting",progress:10});const e=n.slice(1,1+p),i=n.slice(1+p),c=await F(a);t==null||t({phase:"decrypting",progress:30});try{const r=await crypto.subtle.decrypt({name:f,iv:E(e)},c,i);return t==null||t({phase:"complete",progress:100}),new Blob([r])}catch{throw new y("INVALID_KEYFILE")}}async function g(n){const a=await w(n),t=new Uint8Array(a);if(t.length<1)return"unknown";const e=t[0];return e===d?"password":e===h?"keyfile":"unknown"}async function j(n){const a=await w(n),t=new Uint8Array(a);if(t.length<1)return!1;const e=t[0];return e===d&&t.length>=R||e===h&&t.length>=L}function Q(){const n=A(m);return{version:1,algorithm:"AES-256-GCM",key:Y(n.buffer),createdAt:new Date().toISOString()}}function Z(n){try{const a=JSON.parse(n);return $(a)?a:null}catch{return null}}function $(n){if(typeof n!="object"||n===null)return!1;const a=n;return a.version===1&&a.algorithm==="AES-256-GCM"&&typeof a.key=="string"&&a.key.length>0&&typeof a.createdAt=="string"}function q(n,a,t="key"){const e={version:1,algorithm:"AES-256-GCM",key:n,createdAt:new Date().toISOString()},i=JSON.stringify(e,null,2),c=new Blob([i],{type:"application/json"}),r=URL.createObjectURL(c),o=document.createElement("a");o.href=r,o.download=`${a}.${t}`,o.style.display="none",document.body.appendChild(o),o.click(),document.body.removeChild(o),URL.revokeObjectURL(r)}async function J(n){const a=b(n),t=await crypto.subtle.digest("SHA-256",a),e=new Uint8Array(t);return Array.from(e).map(i=>i.toString(16).padStart(2,"0")).join("")}async function z(n,a){const{fileName:t,onProgress:e,...i}=a;try{e==null||e({phase:"downloading",progress:0});const c=await fetch(n);if(!c.ok)throw new y("DOWNLOAD_FAILED",`Download failed: ${c.status} ${c.statusText}`);const r=c.headers.get("content-length");let o;r&&c.body?o=await X(c.body,parseInt(r,10),s=>{e==null||e({phase:"downloading",progress:Math.round(s*50)})}):(e==null||e({phase:"downloading",progress:25}),o=await c.arrayBuffer()),e==null||e({phase:"downloading",progress:50});const u=await K(o,{...i,onProgress:s=>{const I=50+Math.round(s.progress*.45);e==null||e({phase:s.phase==="complete"?"complete":s.phase,progress:s.phase==="complete"?100:I})}});P(u,t),e==null||e({phase:"complete",progress:100})}catch(c){throw c instanceof y?c:new y("DOWNLOAD_FAILED")}}async function X(n,a,t){const e=n.getReader(),i=[];let c=0;for(;;){const{done:u,value:s}=await e.read();if(u)break;i.push(s),c+=s.length;const I=c/a;t(Math.min(I,1))}const r=new Uint8Array(c);let o=0;for(const u of i)r.set(u,o),o+=u.length;return r.buffer}function P(n,a){const t=URL.createObjectURL(n),e=document.createElement("a");e.href=t,e.download=a,e.style.display="none",document.body.appendChild(e),e.click(),document.body.removeChild(e),URL.revokeObjectURL(t)}exports.ALGORITHM=f;exports.AUTH_TAG_LENGTH=D;exports.CryptoError=y;exports.ENCRYPTION_MARKER_KEYFILE=h;exports.ENCRYPTION_MARKER_PASSWORD=d;exports.HASH_ALGORITHM=S;exports.IV_LENGTH=p;exports.KEYFILE_KEY_LENGTH=m;exports.KEY_LENGTH=_;exports.MIN_ENCRYPTED_SIZE_KEYFILE=L;exports.MIN_ENCRYPTED_SIZE_PASSWORD=R;exports.PBKDF2_ITERATIONS=N;exports.SALT_LENGTH=l;exports.computeKeyFileHash=J;exports.decryptFile=K;exports.downloadAndDecrypt=z;exports.downloadKeyFile=q;exports.encryptFile=B;exports.generateKeyFile=Q;exports.generateRandomPassword=M;exports.getEncryptionType=g;exports.isCryptoError=C;exports.isEncryptedFile=j;exports.parseKeyFile=Z;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const h=16,f=12,U=256,H=1e5,B=32,D=1,m=2,S=17,T=18,V=64*1024,N=1,F=16,C=1+h+f+F,K=1+f+F,A="AES-GCM",W="SHA-256",$={INVALID_INPUT:"Input must be a File, Blob, or ArrayBuffer.",PASSWORD_REQUIRED:"Password or keyfile is required for encryption.",KEYFILE_REQUIRED:"Keyfile is required to decrypt this file.",INVALID_PASSWORD:"Decryption failed: incorrect password.",INVALID_KEYFILE:"Decryption failed: incorrect keyfile.",INVALID_ENCRYPTED_DATA:"Invalid encrypted data: file may be corrupted.",ENCRYPTION_FAILED:"Encryption failed.",DECRYPTION_FAILED:"Decryption failed.",DOWNLOAD_FAILED:"File download failed.",UNSUPPORTED_FORMAT:"Unsupported encryption format."};class o extends Error{constructor(a,e){super(e??$[a]),this.name="CryptoError",this.code=a,Error.captureStackTrace&&Error.captureStackTrace(this,o)}}function J(n){return n instanceof o}async function L(n){if(n instanceof ArrayBuffer)return n;if(n instanceof Blob)return n.arrayBuffer();throw new o("INVALID_INPUT")}function E(n){return n.buffer.slice(n.byteOffset,n.byteOffset+n.byteLength)}function X(n){const a=new Uint8Array(n);let e="";for(let t=0;t<a.byteLength;t++)e+=String.fromCharCode(a[t]);return btoa(e)}function G(n){const a=atob(n),e=new Uint8Array(a.length);for(let t=0;t<a.length;t++)e[t]=a.charCodeAt(t);return e.buffer}async function b(n,a){const e=new TextEncoder,t=await crypto.subtle.importKey("raw",e.encode(n),"PBKDF2",!1,["deriveKey"]);return crypto.subtle.deriveKey({name:"PBKDF2",salt:E(a),iterations:H,hash:W},t,{name:A,length:U},!1,["encrypt","decrypt"])}async function k(n){try{const a=G(n);return await crypto.subtle.importKey("raw",a,{name:A,length:U},!1,["encrypt","decrypt"])}catch{throw new o("INVALID_KEYFILE")}}function O(n){return crypto.getRandomValues(new Uint8Array(n))}function x(){return O(h)}function M(){return O(f)}const Y="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*",P=16;function ee(n=P){const a=Math.max(8,Math.min(128,n)),e=O(a);let t="";for(let r=0;r<a;r++){const c=e[r]%Y.length;t+=Y[c]}return t}async function te(n,a){const{password:e,keyData:t,onProgress:r}=a;if(!e&&!t)throw new o("PASSWORD_REQUIRED");try{r==null||r({phase:"deriving_key",progress:0});const c=await L(n);return r==null||r({phase:"deriving_key",progress:10}),t?await re(c,t,r):await ne(c,e,r)}catch(c){throw c instanceof o?c:new o("ENCRYPTION_FAILED")}}async function ne(n,a,e){const t=x(),r=M();e==null||e({phase:"deriving_key",progress:20});const c=await b(a,t);e==null||e({phase:"encrypting",progress:30});const i=await crypto.subtle.encrypt({name:A,iv:E(r)},c,n);e==null||e({phase:"encrypting",progress:90});const s=new Uint8Array(1+h+f+i.byteLength);return s[0]=D,s.set(t,1),s.set(r,1+h),s.set(new Uint8Array(i),1+h+f),e==null||e({phase:"complete",progress:100}),new Blob([s],{type:"application/octet-stream"})}async function re(n,a,e){const t=M();e==null||e({phase:"deriving_key",progress:20});const r=await k(a);e==null||e({phase:"encrypting",progress:30});const c=await crypto.subtle.encrypt({name:A,iv:E(t)},r,n);e==null||e({phase:"encrypting",progress:90});const i=new Uint8Array(1+f+c.byteLength);return i[0]=m,i.set(t,1),i.set(new Uint8Array(c),1+f),e==null||e({phase:"complete",progress:100}),new Blob([i],{type:"application/octet-stream"})}async function z(n,a){const{password:e,keyData:t,onProgress:r}=a;try{r==null||r({phase:"decrypting",progress:0});const c=new Uint8Array(await L(n));if(r==null||r({phase:"decrypting",progress:5}),c.length<1)throw new o("INVALID_ENCRYPTED_DATA");const i=c[0];if(i===D){if(!e)throw new o("PASSWORD_REQUIRED");return await ae(c,e,r)}else if(i===m){if(!t)throw new o("KEYFILE_REQUIRED");return await ce(c,t,r)}else throw new o("UNSUPPORTED_FORMAT")}catch(c){throw c instanceof o?c:new o("DECRYPTION_FAILED")}}async function ae(n,a,e){if(n.length<C)throw new o("INVALID_ENCRYPTED_DATA");e==null||e({phase:"deriving_key",progress:10});const t=n.slice(1,1+h),r=n.slice(1+h,1+h+f),c=n.slice(1+h+f),i=await b(a,t);e==null||e({phase:"decrypting",progress:30});try{const s=await crypto.subtle.decrypt({name:A,iv:E(r)},i,c);return e==null||e({phase:"complete",progress:100}),new Blob([s])}catch{throw new o("INVALID_PASSWORD")}}async function ce(n,a,e){if(n.length<K)throw new o("INVALID_ENCRYPTED_DATA");e==null||e({phase:"decrypting",progress:10});const t=n.slice(1,1+f),r=n.slice(1+f),c=await k(a);e==null||e({phase:"decrypting",progress:30});try{const i=await crypto.subtle.decrypt({name:A,iv:E(t)},c,r);return e==null||e({phase:"complete",progress:100}),new Blob([i])}catch{throw new o("INVALID_KEYFILE")}}function Q(n,a){const e=new Uint8Array(12);e.set(n);const t=new DataView(e.buffer),r=t.getUint32(8,!0);return t.setUint32(8,r^a,!0),e}function j(n,a){const e=new Uint8Array(n.length+a.length);return e.set(n,0),e.set(a,n.length),e}async function g(n,a,e,t){const r=Q(e,t),c=await crypto.subtle.encrypt({name:A,iv:E(r)},a,E(n)),i=new Uint8Array(4+c.byteLength);return new DataView(i.buffer).setUint32(0,c.byteLength,!0),i.set(new Uint8Array(c),4),i}async function ie(n,a,e,t){const r=Q(e,t);try{const c=await crypto.subtle.decrypt({name:A,iv:E(r)},a,E(n));return new Uint8Array(c)}catch{throw new o("DECRYPTION_FAILED")}}function se(n,a,e,t){if(n&&e){const r=new Uint8Array(34);return r[0]=S,r[1]=N,new DataView(r.buffer).setUint32(2,a,!0),r.set(e,6),r.set(t,22),r}else{const r=new Uint8Array(18);return r[0]=T,r[1]=N,new DataView(r.buffer).setUint32(2,a,!0),r.set(t,6),r}}function oe(n){const a=n[0],e=a===S;if(a!==S&&a!==T)throw new o("UNSUPPORTED_FORMAT");const t=n[1];if(t!==N)throw new o("UNSUPPORTED_FORMAT");const r=new DataView(n.buffer,n.byteOffset).getUint32(2,!0);if(e){const c=n.slice(6,22),i=n.slice(22,34);return{isPassword:e,version:t,chunkSize:r,salt:c,baseIV:i,headerSize:34}}else{const c=n.slice(6,18);return{isPassword:e,version:t,chunkSize:r,salt:null,baseIV:c,headerSize:18}}}async function q(n){const{password:a,keyData:e,chunkSize:t=V,onProgress:r}=n;if(!a&&!e)throw new o("PASSWORD_REQUIRED");const c=!!a,i=c?x():null,s=M();r==null||r({phase:"deriving_key",processedBytes:0,processedChunks:0});const p=c?await b(a,i):await k(e),y=se(c,t,i,s);let l=new Uint8Array(0),u=0,R=0;return{stream:new TransformStream({async transform(I,d){for(l=j(l,I);l.length>=t;){const _=l.slice(0,t);l=l.slice(t);const w=await g(_,p,s,u);d.enqueue(w),u++,R+=_.length,r==null||r({phase:"processing",processedBytes:R,processedChunks:u})}},async flush(I){if(l.length>0){const d=await g(l,p,s,u);I.enqueue(d),R+=l.length,u++}r==null||r({phase:"complete",processedBytes:R,processedChunks:u,progress:100})}}),header:y}}function Z(n){const{password:a,keyData:e,onProgress:t}=n;let r=new Uint8Array(0),c=!1,i=null,s=null,p=0,y=0,l=!1,u=0;return new TransformStream({async transform(R,v){if(r=j(r,R),!c){if(r.length<2)return;const d=r[0]===S?34:18;if(r.length<d)return;const _=r.slice(0,d),w=oe(_);if(l=w.isPassword,s=w.baseIV,u=w.headerSize,l&&!a)throw new o("PASSWORD_REQUIRED");if(!l&&!e)throw new o("KEYFILE_REQUIRED");t==null||t({phase:"deriving_key",processedBytes:0,processedChunks:0}),i=l?await b(a,w.salt):await k(e),r=r.slice(u),c=!0}for(;r.length>=4;){const d=4+new DataView(r.buffer,r.byteOffset).getUint32(0,!0);if(r.length<d)break;const _=r.slice(4,d);r=r.slice(d);const w=await ie(_,i,s,p);v.enqueue(w),y+=w.length,p++,t==null||t({phase:"processing",processedBytes:y,processedChunks:p})}},async flush(){if(r.length>0)throw new o("INVALID_ENCRYPTED_DATA");t==null||t({phase:"complete",processedBytes:y,processedChunks:p,progress:100})}})}async function le(n,a){const e=n.size,t=a.onProgress?l=>{a.onProgress({...l,totalBytes:e,progress:l.phase==="complete"?100:Math.round(l.processedBytes/e*100)})}:void 0,{stream:r,header:c}=await q({...a,onProgress:t}),s=n.stream().pipeThrough(r);let p=!1;const y=s.getReader();return new ReadableStream({async pull(l){if(!p){l.enqueue(c),p=!0;return}try{const{done:u,value:R}=await y.read();u?l.close():l.enqueue(R)}catch(u){l.error(u)}},cancel(){y.releaseLock()}})}function ye(n,a){const e=Z(a);return n instanceof ReadableStream?n.pipeThrough(e):n.stream().pipeThrough(e)}async function pe(n){const a=await L(n),e=new Uint8Array(a);if(e.length<1)return"unknown";const t=e[0];return t===D?"password":t===m?"keyfile":t===S?"password-stream":t===T?"keyfile-stream":"unknown"}const ue=34,fe=18;async function de(n){const a=await L(n),e=new Uint8Array(a);if(e.length<1)return!1;const t=e[0];return t===D&&e.length>=C||t===m&&e.length>=K||t===S&&e.length>=ue||t===T&&e.length>=fe}function he(n){return n==="password-stream"||n==="keyfile-stream"}function we(){const n=O(B);return{version:1,algorithm:"AES-256-GCM",key:X(n.buffer),createdAt:new Date().toISOString()}}function Ee(n){try{const a=JSON.parse(n);return Ae(a)?a:null}catch{return null}}function Ae(n){if(typeof n!="object"||n===null)return!1;const a=n;return a.version===1&&a.algorithm==="AES-256-GCM"&&typeof a.key=="string"&&a.key.length>0&&typeof a.createdAt=="string"}function Re(n,a,e="key"){const t={version:1,algorithm:"AES-256-GCM",key:n,createdAt:new Date().toISOString()},r=JSON.stringify(t,null,2),c=new Blob([r],{type:"application/json"}),i=URL.createObjectURL(c),s=document.createElement("a");s.href=i,s.download=`${a}.${e}`,s.style.display="none",document.body.appendChild(s),s.click(),document.body.removeChild(s),URL.revokeObjectURL(i)}async function Se(n){const a=G(n),e=await crypto.subtle.digest("SHA-256",a),t=new Uint8Array(e);return Array.from(t).map(r=>r.toString(16).padStart(2,"0")).join("")}async function _e(n,a){const{fileName:e,onProgress:t,...r}=a;try{t==null||t({phase:"downloading",progress:0});const c=await fetch(n);if(!c.ok)throw new o("DOWNLOAD_FAILED",`Download failed: ${c.status} ${c.statusText}`);const i=c.headers.get("content-length");let s;i&&c.body?s=await Ie(c.body,parseInt(i,10),y=>{t==null||t({phase:"downloading",progress:Math.round(y*50)})}):(t==null||t({phase:"downloading",progress:25}),s=await c.arrayBuffer()),t==null||t({phase:"downloading",progress:50});const p=await z(s,{...r,onProgress:y=>{const l=50+Math.round(y.progress*.45);t==null||t({phase:y.phase==="complete"?"complete":y.phase,progress:y.phase==="complete"?100:l})}});De(p,e),t==null||t({phase:"complete",progress:100})}catch(c){throw c instanceof o?c:new o("DOWNLOAD_FAILED")}}async function Ie(n,a,e){const t=n.getReader(),r=[];let c=0,i=!1;for(;!i;){const y=await t.read();if(i=y.done,y.value){r.push(y.value),c+=y.value.length;const l=c/a;e(Math.min(l,1))}}const s=new Uint8Array(c);let p=0;for(const y of r)s.set(y,p),p+=y.length;return s.buffer}function De(n,a){const e=URL.createObjectURL(n),t=document.createElement("a");t.href=e,t.download=a,t.style.display="none",document.body.appendChild(t),t.click(),document.body.removeChild(t),URL.revokeObjectURL(e)}exports.ALGORITHM=A;exports.AUTH_TAG_LENGTH=F;exports.CryptoError=o;exports.DEFAULT_CHUNK_SIZE=V;exports.ENCRYPTION_MARKER_KEYFILE=m;exports.ENCRYPTION_MARKER_KEYFILE_STREAM=T;exports.ENCRYPTION_MARKER_PASSWORD=D;exports.ENCRYPTION_MARKER_PASSWORD_STREAM=S;exports.HASH_ALGORITHM=W;exports.IV_LENGTH=f;exports.KEYFILE_KEY_LENGTH=B;exports.KEY_LENGTH=U;exports.MIN_ENCRYPTED_SIZE_KEYFILE=K;exports.MIN_ENCRYPTED_SIZE_PASSWORD=C;exports.PBKDF2_ITERATIONS=H;exports.SALT_LENGTH=h;exports.STREAM_FORMAT_VERSION=N;exports.computeKeyFileHash=Se;exports.createDecryptStream=Z;exports.createEncryptStream=q;exports.decryptFile=z;exports.decryptFileStream=ye;exports.downloadAndDecrypt=_e;exports.downloadKeyFile=Re;exports.encryptFile=te;exports.encryptFileStream=le;exports.generateKeyFile=we;exports.generateRandomPassword=ee;exports.getEncryptionType=pe;exports.isCryptoError=J;exports.isEncryptedFile=de;exports.isStreamingEncryption=he;exports.parseKeyFile=Ee;
2
2
  //# sourceMappingURL=index.cjs.map