@time-file/browser-file-crypto 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 TimeFile
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.ko.md ADDED
@@ -0,0 +1,287 @@
1
+ # @time-file/browser-file-crypto
2
+
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%" />
5
+ </p>
6
+ <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%" />
8
+ </p>
9
+
10
+ <p align="center">
11
+ Web Crypto API 기반의 브라우저 파일 암호화 라이브러리입니다.
12
+ </p>
13
+
14
+ <p align="center">
15
+ <a href="https://www.npmjs.com/package/@time-file/browser-file-crypto"><img src="https://img.shields.io/npm/v/@time-file/browser-file-crypto.svg" alt="npm version" /></a>
16
+ <a href="https://bundlephobia.com/package/@time-file/browser-file-crypto"><img src="https://img.shields.io/bundlephobia/minzip/@time-file/browser-file-crypto" alt="bundle size" /></a>
17
+ <a href="https://github.com/time-file/browser-file-crypto/stargazers"><img src="https://img.shields.io/github/stars/time-file/browser-file-crypto?style=flat" alt="stars" /></a>
18
+ <a href="./LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT" /></a>
19
+ </p>
20
+
21
+ <p align="center">
22
+ <a href="./README.md">English</a> | <strong>한국어</strong>
23
+ </p>
24
+
25
+ ## 특징
26
+
27
+ <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%" />
29
+ </p>
30
+
31
+ - **Zero-Knowledge** - 클라이언트 측 암호화로 서버는 평문을 볼 수 없습니다.
32
+ - **Zero-Dependency** - 네이티브 Web Crypto API만 사용합니다.
33
+ - **AES-256-GCM** - 업계 표준 인증 암호화 방식입니다.
34
+ - **비밀번호 & 키파일** - 용도에 따라 두 가지 모드를 지원합니다.
35
+ - **진행률 콜백** - 암호화/복호화 진행 상황을 추적할 수 있습니다.
36
+ - **TypeScript** - 완전한 타입 정의가 포함되어 있습니다.
37
+ - **초경량** - gzip 압축 시 4KB 미만의 용량을 자랑합니다.
38
+
39
+ ## 왜 사용해야 하나요?
40
+
41
+ Web Crypto API는 강력하지만 사용이 복잡합니다. 파일 하나를 암호화하는 데만 약 100줄의 보일러플레이트 코드가 필요하며, 치명적인 실수를 저지르기 쉽습니다.
42
+
43
+ - ❌ IV 재사용 (보안에 치명적)
44
+ - ❌ 낮은 PBKDF2 반복 횟수 (브루트포스 공격에 취약)
45
+ - ❌ salt/IV를 출력에 포함하지 않음 (복호화 불가)
46
+ - ❌ ArrayBuffer 슬라이싱 오류 (데이터 손상)
47
+
48
+ > 이 라이브러리가 모든 것을 처리합니다.
49
+
50
+ ```typescript
51
+ // ❌ Before - Raw Web Crypto API
52
+ const encoder = new TextEncoder();
53
+ const salt = crypto.getRandomValues(new Uint8Array(16));
54
+ const iv = crypto.getRandomValues(new Uint8Array(12));
55
+ const keyMaterial = await crypto.subtle.importKey(
56
+ 'raw', encoder.encode(password), 'PBKDF2', false, ['deriveKey']
57
+ );
58
+ const key = await crypto.subtle.deriveKey(
59
+ { name: 'PBKDF2', salt, iterations: 100000, hash: 'SHA-256' },
60
+ keyMaterial,
61
+ { name: 'AES-GCM', length: 256 },
62
+ false,
63
+ ['encrypt']
64
+ );
65
+ const arrayBuffer = await file.arrayBuffer();
66
+ const ciphertext = await crypto.subtle.encrypt(
67
+ { name: 'AES-GCM', iv },
68
+ key,
69
+ arrayBuffer
70
+ );
71
+ const result = new Uint8Array(1 + salt.length + iv.length + ciphertext.byteLength);
72
+ result.set([0x01], 0);
73
+ result.set(salt, 1);
74
+ result.set(iv, 17);
75
+ result.set(new Uint8Array(ciphertext), 29);
76
+ // ... 복호화도 30줄 이상 필요합니다
77
+ ```
78
+
79
+ ```typescript
80
+ // ✅ After - With this library
81
+ const encrypted = await encryptFile(file, { password: 'secret' });
82
+ const decrypted = await decryptFile(encrypted, { password: 'secret' });
83
+ ```
84
+
85
+ **끝입니다.**
86
+
87
+ ## 비교
88
+
89
+ | 기능 | crypto-js | @aws-crypto | Web Crypto (직접) | **browser-file-crypto** |
90
+ |------|-----------|-------------|-------------------|------------------------|
91
+ | 유지보수 | ❌ 중단됨 | ✅ | - | ✅ |
92
+ | 번들 크기 | ~50KB | ~200KB+ | 0 | **< 4KB** |
93
+ | 의존성 | 많음 | 많음 | 없음 | **없음** |
94
+ | 파일 특화 API | ❌ | ⚠️ | ❌ | **✅** |
95
+ | 진행률 콜백 | ❌ | ❌ | ❌ | **✅** |
96
+ | 키파일 모드 | ❌ | ❌ | ❌ | **✅** |
97
+ | 타입 감지 | ❌ | ❌ | ❌ | **✅** |
98
+ | TypeScript | ❌ | ✅ | - | **✅** |
99
+
100
+ ## 설치
101
+
102
+ ```bash
103
+ # npm
104
+ npm install @time-file/browser-file-crypto
105
+
106
+ # pnpm
107
+ pnpm add @time-file/browser-file-crypto
108
+
109
+ # yarn
110
+ yarn add @time-file/browser-file-crypto
111
+ ```
112
+
113
+ ## 빠른 시작
114
+
115
+ ```typescript
116
+ import { encryptFile, decryptFile } from '@time-file/browser-file-crypto';
117
+
118
+ // 암호화
119
+ const file = document.querySelector('input[type="file"]').files[0];
120
+ const encrypted = await encryptFile(file, {
121
+ password: 'my-secret-password',
122
+ onProgress: ({ phase, progress }) => console.log(`${phase}: ${progress}%`)
123
+ });
124
+
125
+ // 복호화
126
+ const decrypted = await decryptFile(encrypted, {
127
+ password: 'my-secret-password'
128
+ });
129
+ ```
130
+
131
+ ## API
132
+
133
+ ### `encryptFile(file, options)`
134
+
135
+ ```typescript
136
+ const encrypted = await encryptFile(file, {
137
+ password: 'secret', // 또는
138
+ keyData: keyFile.key, // 키파일 사용
139
+ onProgress: (p) => {} // 선택 사항
140
+ });
141
+ ```
142
+
143
+ ### `decryptFile(encrypted, options)`
144
+
145
+ ```typescript
146
+ const decrypted = await decryptFile(encrypted, {
147
+ password: 'secret', // 또는
148
+ keyData: keyFile.key,
149
+ onProgress: (p) => {}
150
+ });
151
+ ```
152
+
153
+ ### 키파일 모드
154
+
155
+ 비밀번호를 기억할 필요가 없습니다:
156
+
157
+ ```typescript
158
+ import { generateKeyFile, downloadKeyFile, parseKeyFile } from '@time-file/browser-file-crypto';
159
+
160
+ const keyFile = generateKeyFile();
161
+ downloadKeyFile(keyFile.key, 'my-secret-key'); // .key 파일로 저장 (기본값)
162
+
163
+ // 커스텀 확장자
164
+ downloadKeyFile(keyFile.key, 'my-secret-key', 'tfkey'); // .tfkey 파일로 저장
165
+
166
+ const encrypted = await encryptFile(file, { keyData: keyFile.key });
167
+
168
+ // 나중에 불러와서 사용
169
+ const content = await uploadedFile.text();
170
+ const loaded = parseKeyFile(content);
171
+ if (loaded) {
172
+ const decrypted = await decryptFile(encrypted, { keyData: loaded.key });
173
+ }
174
+ ```
175
+
176
+ ### 유틸리티
177
+
178
+ ```typescript
179
+ import { getEncryptionType, isEncryptedFile, generateRandomPassword } from '@time-file/browser-file-crypto';
180
+
181
+ await getEncryptionType(blob); // 'password' | 'keyfile' | 'unknown'
182
+ await isEncryptedFile(blob); // true | false
183
+ generateRandomPassword(24); // 'Kx9#mP2$vL5@nQ8!...'
184
+ ```
185
+
186
+ ### 다운로드 & 복호화
187
+
188
+ ```typescript
189
+ import { downloadAndDecrypt } from '@time-file/browser-file-crypto';
190
+
191
+ await downloadAndDecrypt('https://example.com/secret.enc', {
192
+ password: 'secret',
193
+ fileName: 'document.pdf',
194
+ onProgress: ({ phase, progress }) => console.log(`${phase}: ${progress}%`)
195
+ });
196
+ ```
197
+
198
+ ### 에러 처리
199
+
200
+ ```typescript
201
+ import { isCryptoError } from '@time-file/browser-file-crypto';
202
+
203
+ try {
204
+ await decryptFile(encrypted, { password: 'wrong' });
205
+ } catch (error) {
206
+ if (isCryptoError(error)) {
207
+ // error.code: 'INVALID_PASSWORD' | 'INVALID_KEYFILE' | 'INVALID_ENCRYPTED_DATA'
208
+ }
209
+ }
210
+ ```
211
+
212
+ ## 보안
213
+
214
+ ### 스펙
215
+
216
+ | 구성 요소 | 값 |
217
+ |-----------|------|
218
+ | 알고리즘 | AES-256-GCM |
219
+ | 키 유도 | PBKDF2 (SHA-256, 100,000회 반복) |
220
+ | Salt | 16바이트 (암호화마다 랜덤 생성) |
221
+ | IV | 12바이트 (암호화마다 랜덤 생성) |
222
+ | Auth Tag | 16바이트 |
223
+
224
+ ### 파일 포맷
225
+
226
+ <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%" />
228
+ </p>
229
+
230
+ ```
231
+ 비밀번호 암호화:
232
+ => [0x01] + [salt:16] + [iv:12] + [ciphertext + auth_tag:16]
233
+
234
+ 키파일 암호화:
235
+ => [0x02] + [iv:12] + [ciphertext + auth_tag:16]
236
+ ```
237
+
238
+ ### 참고 사항
239
+
240
+ - 키는 추출이 불가능합니다 (`extractable: false`)
241
+ - 매번 랜덤 IV/salt를 사용하므로 동일한 암호문이 생성되지 않습니다
242
+ - AES-GCM은 인증 암호화 방식입니다 (변조 감지 가능)
243
+ - 100,000회 PBKDF2 반복으로 브루트포스 공격에 강합니다
244
+
245
+ ## 브라우저 지원
246
+
247
+ | 브라우저 | 버전 |
248
+ |---------|------|
249
+ | Chrome | 80+ |
250
+ | Firefox | 74+ |
251
+ | Safari | 14+ |
252
+ | Edge | 80+ |
253
+
254
+ Node.js 18+, Deno, Cloudflare Workers에서도 동작합니다.
255
+
256
+ ## TypeScript
257
+
258
+ ```typescript
259
+ import type {
260
+ EncryptOptions,
261
+ DecryptOptions,
262
+ Progress,
263
+ KeyFile,
264
+ EncryptionType,
265
+ CryptoErrorCode
266
+ } from '@time-file/browser-file-crypto';
267
+ ```
268
+
269
+ ## 링크
270
+
271
+ - [변경 이력](./CHANGELOG.md)
272
+ - [라이선스](./LICENSE)
273
+ - [npm](https://www.npmjs.com/package/@time-file/browser-file-crypto)
274
+ - [GitHub](https://github.com/time-file/browser-file-crypto)
275
+
276
+ ## 라이선스
277
+
278
+ [MIT](./LICENSE)
279
+
280
+ ---
281
+
282
+ <p align="center">
283
+ <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" />
286
+ </a>
287
+ </p>
package/README.md ADDED
@@ -0,0 +1,292 @@
1
+ # @time-file/browser-file-crypto
2
+
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%" />
6
+ </p>
7
+ <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%" />
10
+ </p>
11
+
12
+ <p align="center">
13
+ Zero-dependency file encryption for browsers using Web Crypto API.
14
+ </p>
15
+
16
+ <p align="center">
17
+ <a href="https://www.npmjs.com/package/@time-file/browser-file-crypto"><img src="https://img.shields.io/npm/v/@time-file/browser-file-crypto.svg" alt="npm version" /></a>
18
+ <a href="https://bundlephobia.com/package/@time-file/browser-file-crypto"><img src="https://img.shields.io/bundlephobia/minzip/@time-file/browser-file-crypto" alt="bundle size" /></a>
19
+ <a href="https://github.com/time-file/browser-file-crypto/stargazers"><img src="https://img.shields.io/github/stars/time-file/browser-file-crypto?style=flat" alt="stars" /></a>
20
+ <a href="./LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT" /></a>
21
+ </p>
22
+
23
+ <p align="center">
24
+ <strong>English</strong> | <a href="./README.ko.md">한국어</a>
25
+ </p>
26
+
27
+ ## Features
28
+
29
+ <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%" />
32
+ </p>
33
+
34
+ - **Zero-Knowledge** - Client-side encryption, server never sees plaintext
35
+ - **Zero-Dependency** - Native Web Crypto API only
36
+ - **AES-256-GCM** - Industry-standard authenticated encryption
37
+ - **Password & Keyfile** - Two modes for different use cases
38
+ - **Progress Callbacks** - Track encryption/decryption progress
39
+ - **TypeScript** - Full type definitions
40
+ - **Tiny** - < 4KB gzipped
41
+
42
+ ## Why?
43
+
44
+ Web Crypto API is powerful but verbose. You need ~100 lines of boilerplate just to encrypt a file, and it's easy to make critical mistakes.
45
+
46
+ - ❌ Reusing IV (catastrophic for security)
47
+ - ❌ Low PBKDF2 iterations (brute-forceable)
48
+ - ❌ Missing salt/IV in output (can't decrypt later)
49
+ - ❌ Wrong ArrayBuffer slicing (corrupted data)
50
+
51
+
52
+ > This library handles it all.
53
+
54
+ ```typescript
55
+ // ❌ Before - Raw Web Crypto API
56
+ const encoder = new TextEncoder();
57
+ const salt = crypto.getRandomValues(new Uint8Array(16));
58
+ const iv = crypto.getRandomValues(new Uint8Array(12));
59
+ const keyMaterial = await crypto.subtle.importKey(
60
+ 'raw', encoder.encode(password), 'PBKDF2', false, ['deriveKey']
61
+ );
62
+ const key = await crypto.subtle.deriveKey(
63
+ { name: 'PBKDF2', salt, iterations: 100000, hash: 'SHA-256' },
64
+ keyMaterial,
65
+ { name: 'AES-GCM', length: 256 },
66
+ false,
67
+ ['encrypt']
68
+ );
69
+ const arrayBuffer = await file.arrayBuffer();
70
+ const ciphertext = await crypto.subtle.encrypt(
71
+ { name: 'AES-GCM', iv },
72
+ key,
73
+ arrayBuffer
74
+ );
75
+ const result = new Uint8Array(1 + salt.length + iv.length + ciphertext.byteLength);
76
+ result.set([0x01], 0);
77
+ result.set(salt, 1);
78
+ result.set(iv, 17);
79
+ result.set(new Uint8Array(ciphertext), 29);
80
+ // ... and decryption is another 30 lines
81
+ ```
82
+
83
+ ```typescript
84
+ // ✅ After - With this library
85
+ const encrypted = await encryptFile(file, { password: 'secret' });
86
+ const decrypted = await decryptFile(encrypted, { password: 'secret' });
87
+ ```
88
+
89
+ **Done.**
90
+
91
+ ## Comparison
92
+
93
+ | Feature | crypto-js | @aws-crypto | Web Crypto (direct) | **browser-file-crypto** |
94
+ |---------|-----------|-------------|---------------------|------------------------|
95
+ | Maintained | ❌ Deprecated | ✅ | - | ✅ |
96
+ | Bundle size | ~50KB | ~200KB+ | 0 | **< 4KB** |
97
+ | Dependencies | Many | Many | None | **None** |
98
+ | File-focused API | ❌ | ⚠️ | ❌ | **✅** |
99
+ | Progress callbacks | ❌ | ❌ | ❌ | **✅** |
100
+ | Keyfile mode | ❌ | ❌ | ❌ | **✅** |
101
+ | Type detection | ❌ | ❌ | ❌ | **✅** |
102
+ | TypeScript | ❌ | ✅ | - | **✅** |
103
+
104
+ ## Install
105
+
106
+ ```bash
107
+ # npm
108
+ npm install @time-file/browser-file-crypto
109
+
110
+ # pnpm
111
+ pnpm add @time-file/browser-file-crypto
112
+
113
+ # yarn
114
+ yarn add @time-file/browser-file-crypto
115
+ ```
116
+
117
+ ## Quick Start
118
+
119
+ ```typescript
120
+ import { encryptFile, decryptFile } from '@time-file/browser-file-crypto';
121
+
122
+ // Encrypt
123
+ const file = document.querySelector('input[type="file"]').files[0];
124
+ const encrypted = await encryptFile(file, {
125
+ password: 'my-secret-password',
126
+ onProgress: ({ phase, progress }) => console.log(`${phase}: ${progress}%`)
127
+ });
128
+
129
+ // Decrypt
130
+ const decrypted = await decryptFile(encrypted, {
131
+ password: 'my-secret-password'
132
+ });
133
+ ```
134
+
135
+ ## API
136
+
137
+ ### `encryptFile(file, options)`
138
+
139
+ ```typescript
140
+ const encrypted = await encryptFile(file, {
141
+ password: 'secret', // OR
142
+ keyData: keyFile.key, // use keyfile
143
+ onProgress: (p) => {} // optional
144
+ });
145
+ ```
146
+
147
+ ### `decryptFile(encrypted, options)`
148
+
149
+ ```typescript
150
+ const decrypted = await decryptFile(encrypted, {
151
+ password: 'secret', // OR
152
+ keyData: keyFile.key,
153
+ onProgress: (p) => {}
154
+ });
155
+ ```
156
+
157
+ ### Keyfile Mode
158
+
159
+ No password to remember:
160
+
161
+ ```typescript
162
+ import { generateKeyFile, downloadKeyFile, parseKeyFile } from '@time-file/browser-file-crypto';
163
+
164
+ const keyFile = generateKeyFile();
165
+ downloadKeyFile(keyFile.key, 'my-secret-key'); // saves .key file (default)
166
+
167
+ // custom extension
168
+ downloadKeyFile(keyFile.key, 'my-secret-key', 'tfkey'); // saves .tfkey file
169
+
170
+ const encrypted = await encryptFile(file, { keyData: keyFile.key });
171
+
172
+ // Later, load and use
173
+ const content = await uploadedFile.text();
174
+ const loaded = parseKeyFile(content);
175
+ if (loaded) {
176
+ const decrypted = await decryptFile(encrypted, { keyData: loaded.key });
177
+ }
178
+ ```
179
+
180
+ ### Utilities
181
+
182
+ ```typescript
183
+ import { getEncryptionType, isEncryptedFile, generateRandomPassword } from '@time-file/browser-file-crypto';
184
+
185
+ await getEncryptionType(blob); // 'password' | 'keyfile' | 'unknown'
186
+ await isEncryptedFile(blob); // true | false
187
+ generateRandomPassword(24); // 'Kx9#mP2$vL5@nQ8!...'
188
+ ```
189
+
190
+ ### Download & Decrypt
191
+
192
+ ```typescript
193
+ import { downloadAndDecrypt } from '@time-file/browser-file-crypto';
194
+
195
+ await downloadAndDecrypt('https://example.com/secret.enc', {
196
+ password: 'secret',
197
+ fileName: 'document.pdf',
198
+ onProgress: ({ phase, progress }) => console.log(`${phase}: ${progress}%`)
199
+ });
200
+ ```
201
+
202
+ ### Error Handling
203
+
204
+ ```typescript
205
+ import { isCryptoError } from '@time-file/browser-file-crypto';
206
+
207
+ try {
208
+ await decryptFile(encrypted, { password: 'wrong' });
209
+ } catch (error) {
210
+ if (isCryptoError(error)) {
211
+ // error.code: 'INVALID_PASSWORD' | 'INVALID_KEYFILE' | 'INVALID_ENCRYPTED_DATA'
212
+ }
213
+ }
214
+ ```
215
+
216
+ ## Security
217
+
218
+ ### Spec
219
+
220
+ | Component | Value |
221
+ |-----------|-------|
222
+ | Algorithm | AES-256-GCM |
223
+ | Key Derivation | PBKDF2 (SHA-256, 100k iterations) |
224
+ | Salt | 16 bytes (random per encryption) |
225
+ | IV | 12 bytes (random per encryption) |
226
+ | Auth Tag | 16 bytes |
227
+
228
+ ### File Format
229
+
230
+ <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%" />
233
+ </p>
234
+
235
+ ```
236
+ Password-encrypted
237
+ => [0x01] + [salt:16] + [iv:12] + [ciphertext + auth_tag:16]
238
+
239
+ Keyfile-encrypted
240
+ => [0x02] + [iv:12] + [ciphertext + auth_tag:16]
241
+ ```
242
+
243
+ ### Notes
244
+
245
+ - Keys are non-extractable (`extractable: false`)
246
+ - Random IV/salt per encryption = no identical ciphertexts
247
+ - AES-GCM = authenticated encryption (tamper detection)
248
+ - 100k PBKDF2 iterations = brute-force resistant
249
+
250
+ ## Browser Support
251
+
252
+ | Browser | Version |
253
+ |---------|---------|
254
+ | Chrome | 80+ |
255
+ | Firefox | 74+ |
256
+ | Safari | 14+ |
257
+ | Edge | 80+ |
258
+
259
+ Also works in Node.js 18+, Deno, Cloudflare Workers.
260
+
261
+ ## TypeScript
262
+
263
+ ```typescript
264
+ import type {
265
+ EncryptOptions,
266
+ DecryptOptions,
267
+ Progress,
268
+ KeyFile,
269
+ EncryptionType,
270
+ CryptoErrorCode
271
+ } from '@time-file/browser-file-crypto';
272
+ ```
273
+
274
+ ## Links
275
+
276
+ - [Changelog](./CHANGELOG.md)
277
+ - [License](./LICENSE)
278
+ - [npm](https://www.npmjs.com/package/@time-file/browser-file-crypto)
279
+ - [GitHub](https://github.com/time-file/browser-file-crypto)
280
+
281
+ ## License
282
+
283
+ [MIT](./LICENSE)
284
+
285
+ ---
286
+
287
+ <p align="center">
288
+ <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" />
291
+ </a>
292
+ </p>
package/dist/index.cjs ADDED
@@ -0,0 +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;
2
+ //# sourceMappingURL=index.cjs.map