@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 +69 -10
- package/README.md +69 -14
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +264 -7
- package/dist/index.mjs +389 -194
- package/dist/index.mjs.map +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/index.umd.js.map +1 -1
- package/package.json +2 -2
package/README.ko.md
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
# @time-file/browser-file-crypto
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
|
-
<
|
|
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
|
-
<
|
|
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="
|
|
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
|
-
<
|
|
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 압축 시
|
|
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
|
-
<
|
|
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
|
-
<
|
|
285
|
-
|
|
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
|
-
<
|
|
5
|
-
|
|
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
|
-
<
|
|
9
|
-
|
|
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="
|
|
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
|
-
<
|
|
31
|
-
|
|
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** - <
|
|
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
|
-
<
|
|
232
|
-
|
|
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
|
-
<
|
|
290
|
-
|
|
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
|