@time-file/browser-file-crypto 1.0.3 → 1.1.1

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
@@ -44,9 +44,10 @@
44
44
  - **Zero-Dependency** - 네이티브 Web Crypto API만 사용합니다.
45
45
  - **AES-256-GCM** - 업계 표준 인증 암호화 방식입니다.
46
46
  - **비밀번호 & 키파일** - 용도에 따라 두 가지 모드를 지원합니다.
47
+ - **스트리밍 지원** - 메모리 효율적인 대용량 파일 처리 (v1.1.0+)
47
48
  - **진행률 콜백** - 암호화/복호화 진행 상황을 추적할 수 있습니다.
48
49
  - **TypeScript** - 완전한 타입 정의가 포함되어 있습니다.
49
- - **초경량** - gzip 압축 시 4KB 미만의 용량을 자랑합니다.
50
+ - **초경량** - gzip 압축 시 5KB 미만의 용량을 자랑합니다.
50
51
 
51
52
  ## 왜 사용해야 하나요?
52
53
 
@@ -162,6 +163,55 @@ const decrypted = await decryptFile(encrypted, {
162
163
  });
163
164
  ```
164
165
 
166
+ ### 하이브리드 암호화 (v1.1.1+)
167
+
168
+ 파일 크기에 따라 자동으로 일반/스트리밍 암호화를 전환합니다:
169
+
170
+ ```typescript
171
+ import { encryptFileAuto } from '@time-file/browser-file-crypto';
172
+
173
+ // 100MB 이상 파일은 자동으로 스트리밍 사용
174
+ const encrypted = await encryptFileAuto(file, {
175
+ password: 'secret',
176
+ autoStreaming: true,
177
+ streamingThreshold: 100 * 1024 * 1024, // 100MB (기본값)
178
+ chunkSize: 1024 * 1024, // 스트리밍 시 1MB 청크
179
+ onProgress: ({ phase, progress }) => console.log(`${phase}: ${progress}%`)
180
+ });
181
+
182
+ // decryptFile이 자동으로 두 포맷 모두 처리합니다!
183
+ const decrypted = await decryptFile(encrypted, { password: 'secret' });
184
+ ```
185
+
186
+ ### 스트리밍 암호화 (v1.1.0+)
187
+
188
+ 메모리에 맞지 않는 대용량 파일을 처리할 때:
189
+
190
+ ```typescript
191
+ import { encryptFileStream, decryptFileStream } from '@time-file/browser-file-crypto';
192
+
193
+ // 대용량 파일 암호화 (메모리 효율적)
194
+ const encryptedStream = await encryptFileStream(largeFile, {
195
+ password: 'secret',
196
+ chunkSize: 1024 * 1024, // 1MB 청크 (기본값: 64KB)
197
+ onProgress: ({ processedBytes, totalBytes }) => {
198
+ console.log(`${Math.round(processedBytes / totalBytes * 100)}%`);
199
+ }
200
+ });
201
+
202
+ // 스트림을 Blob으로 변환
203
+ const response = new Response(encryptedStream);
204
+ const encryptedBlob = await response.blob();
205
+
206
+ // 복호화 - decryptFile이 스트리밍 포맷을 자동 감지합니다!
207
+ const decrypted = await decryptFile(encryptedBlob, { password: 'secret' });
208
+
209
+ // 또는 스트리밍 복호화를 직접 사용
210
+ const decryptedStream = decryptFileStream(encryptedBlob, { password: 'secret' });
211
+ const decryptResponse = new Response(decryptedStream);
212
+ const decryptedBlob = await decryptResponse.blob();
213
+ ```
214
+
165
215
  ### 키파일 모드
166
216
 
167
217
  비밀번호를 기억할 필요가 없습니다:
@@ -190,7 +240,7 @@ if (loaded) {
190
240
  ```typescript
191
241
  import { getEncryptionType, isEncryptedFile, generateRandomPassword } from '@time-file/browser-file-crypto';
192
242
 
193
- await getEncryptionType(blob); // 'password' | 'keyfile' | 'unknown'
243
+ await getEncryptionType(blob); // 'password' | 'keyfile' | 'password-stream' | 'keyfile-stream' | 'unknown'
194
244
  await isEncryptedFile(blob); // true | false
195
245
  generateRandomPassword(24); // 'Kx9#mP2$vL5@nQ8!...'
196
246
  ```
@@ -198,13 +248,21 @@ generateRandomPassword(24); // 'Kx9#mP2$vL5@nQ8!...'
198
248
  ### 다운로드 & 복호화
199
249
 
200
250
  ```typescript
201
- import { downloadAndDecrypt } from '@time-file/browser-file-crypto';
251
+ import { downloadAndDecrypt, downloadAndDecryptStream } from '@time-file/browser-file-crypto';
202
252
 
253
+ // 일반 다운로드 & 복호화
203
254
  await downloadAndDecrypt('https://example.com/secret.enc', {
204
255
  password: 'secret',
205
256
  fileName: 'document.pdf',
206
257
  onProgress: ({ phase, progress }) => console.log(`${phase}: ${progress}%`)
207
258
  });
259
+
260
+ // 스트리밍 다운로드 & 복호화 (v1.1.1+) - 대용량 파일용
261
+ await downloadAndDecryptStream('https://example.com/large-file.enc', {
262
+ password: 'secret',
263
+ fileName: 'video.mp4',
264
+ onProgress: ({ phase, processedBytes }) => console.log(`${phase}: ${processedBytes} 바이트`)
265
+ });
208
266
  ```
209
267
 
210
268
  ### 에러 처리
@@ -249,6 +307,15 @@ try {
249
307
 
250
308
  키파일 암호화:
251
309
  => [0x02] + [iv:12] + [ciphertext + auth_tag:16]
310
+
311
+ 비밀번호 암호화 (스트리밍):
312
+ => [0x11] + [version:1] + [chunkSize:4] + [salt:16] + [baseIV:12] + [chunks...]
313
+
314
+ 키파일 암호화 (스트리밍):
315
+ => [0x12] + [version:1] + [chunkSize:4] + [baseIV:12] + [chunks...]
316
+
317
+ 각 스트리밍 청크:
318
+ => [chunkLength:4] + [ciphertext + auth_tag:16]
252
319
  ```
253
320
 
254
321
  ### 참고 사항
@@ -278,7 +345,14 @@ import type {
278
345
  Progress,
279
346
  KeyFile,
280
347
  EncryptionType,
281
- CryptoErrorCode
348
+ CryptoErrorCode,
349
+ // 스트리밍 타입 (v1.1.0+)
350
+ StreamEncryptOptions,
351
+ StreamDecryptOptions,
352
+ StreamProgress,
353
+ // v1.1.1+
354
+ AutoEncryptOptions,
355
+ DownloadDecryptStreamOptions
282
356
  } from '@time-file/browser-file-crypto';
283
357
  ```
284
358
 
package/README.md CHANGED
@@ -44,9 +44,10 @@
44
44
  - **Zero-Dependency** - Native Web Crypto API only
45
45
  - **AES-256-GCM** - Industry-standard authenticated encryption
46
46
  - **Password & Keyfile** - Two modes for different use cases
47
+ - **Streaming Support** - Memory-efficient large file handling (v1.1.0+)
47
48
  - **Progress Callbacks** - Track encryption/decryption progress
48
49
  - **TypeScript** - Full type definitions
49
- - **Tiny** - < 4KB gzipped
50
+ - **Tiny** - < 5KB gzipped
50
51
 
51
52
  ## Why?
52
53
 
@@ -163,6 +164,55 @@ const decrypted = await decryptFile(encrypted, {
163
164
  });
164
165
  ```
165
166
 
167
+ ### Hybrid Encryption (v1.1.1+)
168
+
169
+ Auto-switch between standard and streaming encryption based on file size:
170
+
171
+ ```typescript
172
+ import { encryptFileAuto } from '@time-file/browser-file-crypto';
173
+
174
+ // Automatically uses streaming for files > 100MB
175
+ const encrypted = await encryptFileAuto(file, {
176
+ password: 'secret',
177
+ autoStreaming: true,
178
+ streamingThreshold: 100 * 1024 * 1024, // 100MB (default)
179
+ chunkSize: 1024 * 1024, // 1MB chunks for streaming
180
+ onProgress: ({ phase, progress }) => console.log(`${phase}: ${progress}%`)
181
+ });
182
+
183
+ // decryptFile automatically handles both formats!
184
+ const decrypted = await decryptFile(encrypted, { password: 'secret' });
185
+ ```
186
+
187
+ ### Streaming Encryption (v1.1.0+)
188
+
189
+ For large files that don't fit in memory:
190
+
191
+ ```typescript
192
+ import { encryptFileStream, decryptFileStream } from '@time-file/browser-file-crypto';
193
+
194
+ // Encrypt large file (memory-efficient)
195
+ const encryptedStream = await encryptFileStream(largeFile, {
196
+ password: 'secret',
197
+ chunkSize: 1024 * 1024, // 1MB chunks (default: 64KB)
198
+ onProgress: ({ processedBytes, totalBytes }) => {
199
+ console.log(`${Math.round(processedBytes / totalBytes * 100)}%`);
200
+ }
201
+ });
202
+
203
+ // Convert stream to Blob
204
+ const response = new Response(encryptedStream);
205
+ const encryptedBlob = await response.blob();
206
+
207
+ // Decrypt - decryptFile auto-detects streaming format!
208
+ const decrypted = await decryptFile(encryptedBlob, { password: 'secret' });
209
+
210
+ // Or use streaming decryption directly
211
+ const decryptedStream = decryptFileStream(encryptedBlob, { password: 'secret' });
212
+ const decryptResponse = new Response(decryptedStream);
213
+ const decryptedBlob = await decryptResponse.blob();
214
+ ```
215
+
166
216
  ### Keyfile Mode
167
217
 
168
218
  No password to remember:
@@ -191,7 +241,7 @@ if (loaded) {
191
241
  ```typescript
192
242
  import { getEncryptionType, isEncryptedFile, generateRandomPassword } from '@time-file/browser-file-crypto';
193
243
 
194
- await getEncryptionType(blob); // 'password' | 'keyfile' | 'unknown'
244
+ await getEncryptionType(blob); // 'password' | 'keyfile' | 'password-stream' | 'keyfile-stream' | 'unknown'
195
245
  await isEncryptedFile(blob); // true | false
196
246
  generateRandomPassword(24); // 'Kx9#mP2$vL5@nQ8!...'
197
247
  ```
@@ -199,13 +249,21 @@ generateRandomPassword(24); // 'Kx9#mP2$vL5@nQ8!...'
199
249
  ### Download & Decrypt
200
250
 
201
251
  ```typescript
202
- import { downloadAndDecrypt } from '@time-file/browser-file-crypto';
252
+ import { downloadAndDecrypt, downloadAndDecryptStream } from '@time-file/browser-file-crypto';
203
253
 
254
+ // Standard download & decrypt
204
255
  await downloadAndDecrypt('https://example.com/secret.enc', {
205
256
  password: 'secret',
206
257
  fileName: 'document.pdf',
207
258
  onProgress: ({ phase, progress }) => console.log(`${phase}: ${progress}%`)
208
259
  });
260
+
261
+ // Streaming download & decrypt (v1.1.1+) - for large files
262
+ await downloadAndDecryptStream('https://example.com/large-file.enc', {
263
+ password: 'secret',
264
+ fileName: 'video.mp4',
265
+ onProgress: ({ phase, processedBytes }) => console.log(`${phase}: ${processedBytes} bytes`)
266
+ });
209
267
  ```
210
268
 
211
269
  ### Error Handling
@@ -250,6 +308,15 @@ Password-encrypted
250
308
 
251
309
  Keyfile-encrypted
252
310
  => [0x02] + [iv:12] + [ciphertext + auth_tag:16]
311
+
312
+ Password-encrypted (streaming)
313
+ => [0x11] + [version:1] + [chunkSize:4] + [salt:16] + [baseIV:12] + [chunks...]
314
+
315
+ Keyfile-encrypted (streaming)
316
+ => [0x12] + [version:1] + [chunkSize:4] + [baseIV:12] + [chunks...]
317
+
318
+ Each streaming chunk:
319
+ => [chunkLength:4] + [ciphertext + auth_tag:16]
253
320
  ```
254
321
 
255
322
  ### Notes
@@ -279,7 +346,14 @@ import type {
279
346
  Progress,
280
347
  KeyFile,
281
348
  EncryptionType,
282
- CryptoErrorCode
349
+ CryptoErrorCode,
350
+ // Streaming types (v1.1.0+)
351
+ StreamEncryptOptions,
352
+ StreamDecryptOptions,
353
+ StreamProgress,
354
+ // v1.1.1+
355
+ AutoEncryptOptions,
356
+ DownloadDecryptStreamOptions
283
357
  } from '@time-file/browser-file-crypto';
284
358
  ```
285
359
 
package/dist/index.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const s=16,p=12,_=256,N=1e5,m=32,u=1,d=2,D=16,R=1+s+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 h(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:h(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(s)}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:h(i)},c,n);t==null||t({phase:"encrypting",progress:90});const o=new Uint8Array(1+s+p+r.byteLength);return o[0]=u,o.set(e,1),o.set(i,1+s),o.set(new Uint8Array(r),1+s+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:h(e)},i,n);t==null||t({phase:"encrypting",progress:90});const r=new Uint8Array(1+p+c.byteLength);return r[0]=d,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===u){if(!t)throw new y("PASSWORD_REQUIRED");return await V(c,t,i)}else if(r===d){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+s),i=n.slice(1+s,1+s+p),c=n.slice(1+s+p),r=await O(a,e);t==null||t({phase:"decrypting",progress:30});try{const o=await crypto.subtle.decrypt({name:f,iv:h(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:h(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===u?"password":e===d?"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===u&&t.length>=R||e===d&&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),l=>{e==null||e({phase:"downloading",progress:Math.round(l*50)})}):(e==null||e({phase:"downloading",progress:25}),o=await c.arrayBuffer()),e==null||e({phase:"downloading",progress:50});const E=await k(o,{...i,onProgress:l=>{const I=50+Math.round(l.progress*.45);e==null||e({phase:l.phase==="complete"?"complete":l.phase,progress:l.phase==="complete"?100:I})}});P(E,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,r=!1;for(;!r;){const l=await e.read();if(r=l.done,l.value){i.push(l.value),c+=l.value.length;const I=c/a;t(Math.min(I,1))}}const o=new Uint8Array(c);let E=0;for(const l of i)o.set(l,E),E+=l.length;return o.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=d;exports.ENCRYPTION_MARKER_PASSWORD=u;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=s;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 E=16,f=12,U=256,V=1e5,W=32,_=1,T=2,D=17,I=18,C=64*1024,b=1,K=16,M=1+E+f+K,g=1+f+K,S="AES-GCM",G="SHA-256",ee={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??ee[a]),this.name="CryptoError",this.code=a,Error.captureStackTrace&&Error.captureStackTrace(this,o)}}function te(n){return n instanceof o}async function N(n){if(n instanceof ArrayBuffer)return n;if(n instanceof Blob)return n.arrayBuffer();throw new o("INVALID_INPUT")}function R(n){return n.buffer.slice(n.byteOffset,n.byteOffset+n.byteLength)}function ne(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 x(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 O(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:R(a),iterations:V,hash:G},t,{name:S,length:U},!1,["encrypt","decrypt"])}async function k(n){try{const a=x(n);return await crypto.subtle.importKey("raw",a,{name:S,length:U},!1,["encrypt","decrypt"])}catch{throw new o("INVALID_KEYFILE")}}function F(n){return crypto.getRandomValues(new Uint8Array(n))}function z(){return F(E)}function v(){return F(f)}const Y="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*",re=16;function ae(n=re){const a=Math.max(8,Math.min(128,n)),e=F(a);let t="";for(let r=0;r<a;r++){const c=e[r]%Y.length;t+=Y[c]}return t}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 H(n,a,e,t){const r=Q(e,t),c=await crypto.subtle.encrypt({name:S,iv:R(r)},a,R(n)),s=new Uint8Array(4+c.byteLength);return new DataView(s.buffer).setUint32(0,c.byteLength,!0),s.set(new Uint8Array(c),4),s}async function ce(n,a,e,t,r){const c=Q(e,t);try{const s=await crypto.subtle.decrypt({name:S,iv:R(c)},a,R(n));return new Uint8Array(s)}catch{throw t===0?new o(r?"INVALID_PASSWORD":"INVALID_KEYFILE"):new o("DECRYPTION_FAILED")}}function se(n,a,e,t){if(n&&e){const r=new Uint8Array(34);return r[0]=D,r[1]=b,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]=I,r[1]=b,new DataView(r.buffer).setUint32(2,a,!0),r.set(t,6),r}}function ie(n){const a=n[0],e=a===D;if(a!==D&&a!==I)throw new o("UNSUPPORTED_FORMAT");const t=n[1];if(t!==b)throw new o("UNSUPPORTED_FORMAT");const r=new DataView(n.buffer,n.byteOffset).getUint32(2,!0);if(e){const c=n.slice(6,22),s=n.slice(22,34);return{isPassword:e,version:t,chunkSize:r,salt:c,baseIV:s,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=C,onProgress:r}=n;if(!a&&!e)throw new o("PASSWORD_REQUIRED");const c=!!a,s=c?z():null,i=v();r==null||r({phase:"deriving_key",processedBytes:0,processedChunks:0});const y=c?await O(a,s):await k(e),p=se(c,t,s,i);let l=new Uint8Array(0),u=0,d=0;return{stream:new TransformStream({async transform(h,w){for(l=j(l,h);l.length>=t;){const m=l.slice(0,t);l=l.slice(t);const A=await H(m,y,i,u);w.enqueue(A),u++,d+=m.length,r==null||r({phase:"encrypting",processedBytes:d,processedChunks:u})}},async flush(h){if(l.length>0){const w=await H(l,y,i,u);h.enqueue(w),d+=l.length,u++}r==null||r({phase:"complete",processedBytes:d,processedChunks:u,progress:100})}}),header:p}}function Z(n){const{password:a,keyData:e,onProgress:t}=n;let r=new Uint8Array(0),c=!1,s=null,i=null,y=0,p=0,l=!1,u=0;return new TransformStream({async transform(d,L){if(r=j(r,d),!c){if(r.length<2)return;const w=r[0]===D?34:18;if(r.length<w)return;const m=r.slice(0,w),A=ie(m);if(l=A.isPassword,i=A.baseIV,u=A.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}),s=l?await O(a,A.salt):await k(e),r=r.slice(u),c=!0}for(;r.length>=4;){const w=4+new DataView(r.buffer,r.byteOffset).getUint32(0,!0);if(r.length<w)break;const m=r.slice(4,w);r=r.slice(w);const A=await ce(m,s,i,y,l);L.enqueue(A),p+=A.length,y++,t==null||t({phase:"decrypting",processedBytes:p,processedChunks:y})}},async flush(){if(r.length>0)throw new o("INVALID_ENCRYPTED_DATA");t==null||t({phase:"complete",processedBytes:p,processedChunks:y,progress:100})}})}async function $(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}),i=n.stream().pipeThrough(r);let y=!1;const p=i.getReader();return new ReadableStream({async pull(l){if(!y){l.enqueue(c),y=!0;return}try{const{done:u,value:d}=await p.read();u?l.close():l.enqueue(d)}catch(u){l.error(u)}},cancel(){p.releaseLock()}})}function B(n,a){const e=Z(a);return n instanceof ReadableStream?n.pipeThrough(e):n.stream().pipeThrough(e)}const oe=100*1024*1024;async function J(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 N(n);return r==null||r({phase:"deriving_key",progress:10}),t?await pe(c,t,r):await le(c,e,r)}catch(c){throw c instanceof o?c:new o("ENCRYPTION_FAILED")}}async function le(n,a,e){const t=z(),r=v();e==null||e({phase:"deriving_key",progress:20});const c=await O(a,t);e==null||e({phase:"encrypting",progress:30});const s=await crypto.subtle.encrypt({name:S,iv:R(r)},c,n);e==null||e({phase:"encrypting",progress:90});const i=new Uint8Array(1+E+f+s.byteLength);return i[0]=_,i.set(t,1),i.set(r,1+E),i.set(new Uint8Array(s),1+E+f),e==null||e({phase:"complete",progress:100}),new Blob([i],{type:"application/octet-stream"})}async function pe(n,a,e){const t=v();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:S,iv:R(t)},r,n);e==null||e({phase:"encrypting",progress:90});const s=new Uint8Array(1+f+c.byteLength);return s[0]=T,s.set(t,1),s.set(new Uint8Array(c),1+f),e==null||e({phase:"complete",progress:100}),new Blob([s],{type:"application/octet-stream"})}async function ye(n,a){const{password:e,keyData:t,onProgress:r,autoStreaming:c=!1,streamingThreshold:s=oe,chunkSize:i=C}=a;if(!e&&!t)throw new o("PASSWORD_REQUIRED");if(c&&n.size>s){const l=await $(n,{password:e,keyData:t,chunkSize:i,onProgress:r?d=>{r({phase:d.phase,progress:d.progress??Math.round(d.processedBytes/n.size*100)})}:void 0});return await new Response(l).blob()}else return await J(n,{password:e,keyData:t,onProgress:r})}async function X(n,a){const{password:e,keyData:t,onProgress:r}=a;try{r==null||r({phase:"decrypting",progress:0});const c=new Uint8Array(await N(n));if(r==null||r({phase:"decrypting",progress:5}),c.length<1)throw new o("INVALID_ENCRYPTED_DATA");const s=c[0];if(s===_){if(!e)throw new o("PASSWORD_REQUIRED");return await ue(c,e,r)}else if(s===T){if(!t)throw new o("KEYFILE_REQUIRED");return await de(c,t,r)}else{if(s===D||s===I)return await fe(n,a);throw new o("UNSUPPORTED_FORMAT")}}catch(c){throw c instanceof o?c:new o("DECRYPTION_FAILED")}}async function ue(n,a,e){if(n.length<M)throw new o("INVALID_ENCRYPTED_DATA");e==null||e({phase:"deriving_key",progress:10});const t=n.slice(1,1+E),r=n.slice(1+E,1+E+f),c=n.slice(1+E+f),s=await O(a,t);e==null||e({phase:"decrypting",progress:30});try{const i=await crypto.subtle.decrypt({name:S,iv:R(r)},s,c);return e==null||e({phase:"complete",progress:100}),new Blob([i])}catch{throw new o("INVALID_PASSWORD")}}async function de(n,a,e){if(n.length<g)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 s=await crypto.subtle.decrypt({name:S,iv:R(t)},c,r);return e==null||e({phase:"complete",progress:100}),new Blob([s])}catch{throw new o("INVALID_KEYFILE")}}async function fe(n,a){const{password:e,keyData:t,onProgress:r}=a,c=n instanceof Blob?n:new Blob([n]),i=B(c,{password:e,keyData:t,onProgress:r?p=>{r({phase:p.phase,progress:p.progress??Math.round(p.processedBytes/(p.totalBytes||1)*100)})}:void 0});return await new Response(i).blob()}async function he(n){const a=await N(n),e=new Uint8Array(a);if(e.length<1)return"unknown";const t=e[0];return t===_?"password":t===T?"keyfile":t===D?"password-stream":t===I?"keyfile-stream":"unknown"}const we=34,Ee=18;async function Ae(n){const a=await N(n),e=new Uint8Array(a);if(e.length<1)return!1;const t=e[0];return t===_&&e.length>=M||t===T&&e.length>=g||t===D&&e.length>=we||t===I&&e.length>=Ee}function Re(n){return n==="password-stream"||n==="keyfile-stream"}function Se(){const n=F(W);return{version:1,algorithm:"AES-256-GCM",key:ne(n.buffer),createdAt:new Date().toISOString()}}function De(n){try{const a=JSON.parse(n);return me(a)?a:null}catch{return null}}function me(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 Ie(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"}),s=URL.createObjectURL(c),i=document.createElement("a");i.href=s,i.download=`${a}.${e}`,i.style.display="none",document.body.appendChild(i),i.click(),document.body.removeChild(i),URL.revokeObjectURL(s)}async function _e(n){const a=x(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 Te(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 s=c.headers.get("content-length");let i;s&&c.body?i=await Le(c.body,parseInt(s,10),p=>{t==null||t({phase:"downloading",progress:Math.round(p*50)})}):(t==null||t({phase:"downloading",progress:25}),i=await c.arrayBuffer()),t==null||t({phase:"downloading",progress:50});const y=await X(i,{...r,onProgress:p=>{const l=50+Math.round(p.progress*.45);t==null||t({phase:p.phase==="complete"?"complete":p.phase,progress:p.phase==="complete"?100:l})}});P(y,e),t==null||t({phase:"complete",progress:100})}catch(c){throw c instanceof o?c:new o("DOWNLOAD_FAILED")}}async function Le(n,a,e){const t=n.getReader(),r=[];let c=0,s=!1;for(;!s;){const p=await t.read();if(s=p.done,p.value){r.push(p.value),c+=p.value.length;const l=c/a;e(Math.min(l,1))}}const i=new Uint8Array(c);let y=0;for(const p of r)i.set(p,y),y+=p.length;return i.buffer}function P(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)}async function be(n,a){const{fileName:e,password:t,keyData:r,onProgress:c}=a;try{c==null||c({phase:"downloading",processedBytes:0,processedChunks:0,progress:0});const s=await fetch(n);if(!s.ok)throw new o("DOWNLOAD_FAILED",`Download failed: ${s.status} ${s.statusText}`);const i=s.headers.get("content-length"),y=i?parseInt(i,10):void 0;c==null||c({phase:"downloading",processedBytes:0,processedChunks:0,totalBytes:y,progress:25});const p=await s.blob();let l=0;const u=B(p,{password:t,keyData:r,onProgress:h=>{l=h.processedBytes,c==null||c({...h,progress:h.phase==="complete"?100:50+Math.round((h.progress??0)*.45)})}}),L=await new Response(u).blob();P(L,e),c==null||c({phase:"complete",processedBytes:l,processedChunks:0,progress:100})}catch(s){throw s instanceof o?s:new o("DOWNLOAD_FAILED")}}exports.ALGORITHM=S;exports.AUTH_TAG_LENGTH=K;exports.CryptoError=o;exports.DEFAULT_CHUNK_SIZE=C;exports.ENCRYPTION_MARKER_KEYFILE=T;exports.ENCRYPTION_MARKER_KEYFILE_STREAM=I;exports.ENCRYPTION_MARKER_PASSWORD=_;exports.ENCRYPTION_MARKER_PASSWORD_STREAM=D;exports.HASH_ALGORITHM=G;exports.IV_LENGTH=f;exports.KEYFILE_KEY_LENGTH=W;exports.KEY_LENGTH=U;exports.MIN_ENCRYPTED_SIZE_KEYFILE=g;exports.MIN_ENCRYPTED_SIZE_PASSWORD=M;exports.PBKDF2_ITERATIONS=V;exports.SALT_LENGTH=E;exports.STREAM_FORMAT_VERSION=b;exports.computeKeyFileHash=_e;exports.createDecryptStream=Z;exports.createEncryptStream=q;exports.decryptFile=X;exports.decryptFileStream=B;exports.downloadAndDecrypt=Te;exports.downloadAndDecryptStream=be;exports.downloadKeyFile=Ie;exports.encryptFile=J;exports.encryptFileAuto=ye;exports.encryptFileStream=$;exports.generateKeyFile=Se;exports.generateRandomPassword=ae;exports.getEncryptionType=he;exports.isCryptoError=te;exports.isEncryptedFile=Ae;exports.isStreamingEncryption=Re;exports.parseKeyFile=De;
2
2
  //# sourceMappingURL=index.cjs.map