@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 +21 -0
- package/README.ko.md +287 -0
- package/README.md +292 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +726 -0
- package/dist/index.mjs +334 -0
- package/dist/index.mjs.map +1 -0
- package/dist/index.umd.js +2 -0
- package/dist/index.umd.js.map +1 -0
- package/package.json +68 -0
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
|