@alessiofrittoli/crypto-cipher 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/README.md +995 -0
- package/dist/index.d.mts +206 -0
- package/dist/index.d.ts +206 -0
- package/dist/index.js +1 -0
- package/dist/index.mjs +1 -0
- package/dist/types.d.mts +89 -0
- package/dist/types.d.ts +89 -0
- package/dist/types.js +1 -0
- package/dist/types.mjs +0 -0
- package/license.md +21 -0
- package/package.json +91 -0
package/README.md
ADDED
|
@@ -0,0 +1,995 @@
|
|
|
1
|
+
# Crypto Cipher 🔐
|
|
2
|
+
|
|
3
|
+
[![NPM Latest Version][version-badge]][npm-url] [![Coverage Status][coverage-badge]][coverage-url] [![NPM Monthly Downloads][downloads-badge]][npm-url] [![Dependencies][deps-badge]][deps-url]
|
|
4
|
+
|
|
5
|
+
[version-badge]: https://img.shields.io/npm/v/%40alessiofrittoli%2Fcrypto-cipher
|
|
6
|
+
[npm-url]: https://npmjs.org/package/%40alessiofrittoli%2Fcrypto-cipher
|
|
7
|
+
[coverage-badge]: https://coveralls.io/repos/github/alessiofrittoli/crypto-cipher/badge.svg
|
|
8
|
+
[coverage-url]: https://coveralls.io/github/alessiofrittoli/crypto-cipher
|
|
9
|
+
[downloads-badge]: https://img.shields.io/npm/dm/%40alessiofrittoli%2Fcrypto-cipher.svg
|
|
10
|
+
[deps-badge]: https://img.shields.io/librariesio/release/npm/%40alessiofrittoli%2Fcrypto-cipher
|
|
11
|
+
[deps-url]: https://libraries.io/npm/%40alessiofrittoli%2Fcrypto-cipher
|
|
12
|
+
|
|
13
|
+
## Node.js Cipher cryptograph utility library
|
|
14
|
+
|
|
15
|
+
### Table of Contents
|
|
16
|
+
|
|
17
|
+
- [Getting started](#getting-started)
|
|
18
|
+
- [Key features](#key-features)
|
|
19
|
+
- [API Reference](#api-reference)
|
|
20
|
+
- [Importing the library](#importing-the-library)
|
|
21
|
+
- [Constants](#constants)
|
|
22
|
+
- [Methods](#methods)
|
|
23
|
+
- [`Cipher.encrypt()`](#cipherencrypt)
|
|
24
|
+
- [`Cipher.decrypt()`](#cipherdecrypt)
|
|
25
|
+
- [`Cipher.streamEncrypt()`](#cipherstreamencrypt)
|
|
26
|
+
- [`Cipher.streamDecrypt()`](#cipherstreamencrypt)
|
|
27
|
+
- [`Cipher.hybridEncrypt()`](#cipherhybridencrypt)
|
|
28
|
+
- [`Cipher.hybridDecrypt()`](#cipherhybriddecrypt)
|
|
29
|
+
- [Types](#types)
|
|
30
|
+
- [Examples](#examples)
|
|
31
|
+
- [In-memory data buffer encryption/decryption](#in-memory-data-buffer-encryptiondecryption)
|
|
32
|
+
- [In-memory data stream encryption/decryption](#in-memory-data-stream-encryptiondecryption)
|
|
33
|
+
- [In-memory data stream with hybrid encryption/decryption](#in-memory-data-stream-with-hybrid-encryptiondecryption)
|
|
34
|
+
- [File based data stream encryption/decryption](#file-based-data-stream-encryptiondecryption)
|
|
35
|
+
- [File based data stream with hybrid encryption/decryption](#file-based-data-stream-with-hybrid-encryptiondecryption)
|
|
36
|
+
- [Development](#development)
|
|
37
|
+
- [ESLint](#eslint)
|
|
38
|
+
- [Jest](#jest)
|
|
39
|
+
- [Contributing](#contributing)
|
|
40
|
+
- [Security](#security)
|
|
41
|
+
- [Credits](#made-with-)
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
### Getting started
|
|
46
|
+
|
|
47
|
+
The `crypto-cipher` library provides AES encryption and decryption functionality, supporting both in-memory buffers and streams. It adheres to the [NIST SP 800-38D](https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf) standard recommendations.
|
|
48
|
+
|
|
49
|
+
⚠️ Note that every performed operation cannot be accomplished client-side and must be executed on a back-end server.
|
|
50
|
+
|
|
51
|
+
It is part of the [`crypto`](https://npmjs.com/search?q=%40alessiofrittoli%2Fcrypto) utility libraries and can be installed by running the following command:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm i @alessiofrittoli/crypto-cipher
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
or using `pnpm`
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pnpm i @alessiofrittoli/crypto-cipher
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
### Key features
|
|
66
|
+
|
|
67
|
+
- Supports multiple AES algorithms (`CCM`, `GCM`, `OCB`, `CBC`) and even `chacha20-poly1305`.
|
|
68
|
+
- In-memory buffer encryption and decrpytion.
|
|
69
|
+
- Robust support for encrypting and decrypting streams (in-memory and file based), with seamless handling of key/IV extraction.
|
|
70
|
+
- Hybrid encryption methods for combining symmetric and asymmetric cryptography.
|
|
71
|
+
|
|
72
|
+
#### Options Management
|
|
73
|
+
|
|
74
|
+
- A solid options resolver mechanism ensures consistent handling of defaults and constraints.
|
|
75
|
+
|
|
76
|
+
#### Security Considerations
|
|
77
|
+
|
|
78
|
+
- Random `salt` and `IV` generation.
|
|
79
|
+
- Authenticated encryption modes with proper `authTag` and `Additional Authenticated Data` handling.
|
|
80
|
+
|
|
81
|
+
#### Readable and Modular
|
|
82
|
+
|
|
83
|
+
- Separation of concerns with clear method responsibilities.
|
|
84
|
+
- Comprehensive JSDoc comments enhance maintainability and readability.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
### API Reference
|
|
89
|
+
|
|
90
|
+
#### Constants
|
|
91
|
+
|
|
92
|
+
##### `SALT_LENGTH`
|
|
93
|
+
|
|
94
|
+
Defines the minimum, maximum, and default lengths for salt.
|
|
95
|
+
|
|
96
|
+
<details>
|
|
97
|
+
<summary>Properties</summary>
|
|
98
|
+
|
|
99
|
+
| Property | Value |
|
|
100
|
+
|------------|-------|
|
|
101
|
+
| `min` | 16 |
|
|
102
|
+
| `max` | 64 |
|
|
103
|
+
| `default` | 32 |
|
|
104
|
+
|
|
105
|
+
</details>
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
##### `IV_LENGTH`
|
|
110
|
+
|
|
111
|
+
Defines the minimum, maximum, and default lengths for initialization vectors (IV).
|
|
112
|
+
|
|
113
|
+
<details>
|
|
114
|
+
<summary>Properties</summary>
|
|
115
|
+
|
|
116
|
+
| Property | Value |
|
|
117
|
+
|------------|-------|
|
|
118
|
+
| `min` | 8 |
|
|
119
|
+
| `max` | 32 |
|
|
120
|
+
| `default` | 16 |
|
|
121
|
+
|
|
122
|
+
</details>
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
##### `AUTH_TAG_LENGTH`
|
|
127
|
+
|
|
128
|
+
Defines the minimum, maximum, and default lengths for authentication tags.
|
|
129
|
+
|
|
130
|
+
<details>
|
|
131
|
+
<summary>Properties</summary>
|
|
132
|
+
|
|
133
|
+
| Property | Value |
|
|
134
|
+
|------------|-------|
|
|
135
|
+
| `min` | 4 |
|
|
136
|
+
| `max` | 16 |
|
|
137
|
+
| `default` | 16 |
|
|
138
|
+
|
|
139
|
+
</details>
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
##### `AAD_LENGTH`
|
|
144
|
+
|
|
145
|
+
Defines the minimum, maximum, and default lengths for additional authenticated data (AAD).
|
|
146
|
+
|
|
147
|
+
<details>
|
|
148
|
+
<summary>Properties</summary>
|
|
149
|
+
|
|
150
|
+
| Property | Value |
|
|
151
|
+
|------------|-------|
|
|
152
|
+
| `min` | 16 |
|
|
153
|
+
| `max` | 4096 |
|
|
154
|
+
| `default` | 32 |
|
|
155
|
+
|
|
156
|
+
</details>
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
##### `DEFAULT_ALGORITHM`
|
|
161
|
+
|
|
162
|
+
Specifies default AES algorithms for buffer and stream operations.
|
|
163
|
+
|
|
164
|
+
<details>
|
|
165
|
+
<summary>Properties</summary>
|
|
166
|
+
|
|
167
|
+
| Operation | Algorithm |
|
|
168
|
+
|------------|--------------|
|
|
169
|
+
| `buffer` | `aes-256-gcm` |
|
|
170
|
+
| `stream` | `aes-256-cbc` |
|
|
171
|
+
|
|
172
|
+
</details>
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
##### `ALGORITHMS`
|
|
177
|
+
|
|
178
|
+
Supported AES algorithms:
|
|
179
|
+
|
|
180
|
+
<details>
|
|
181
|
+
<summary>Properties</summary>
|
|
182
|
+
|
|
183
|
+
- `aes-128-gcm`
|
|
184
|
+
- `aes-192-gcm`
|
|
185
|
+
- `aes-256-gcm`
|
|
186
|
+
- `aes-128-ccm`
|
|
187
|
+
- `aes-192-ccm`
|
|
188
|
+
- `aes-256-ccm`
|
|
189
|
+
- `aes-128-ocb`
|
|
190
|
+
- `aes-192-ocb`
|
|
191
|
+
- `aes-256-ocb`
|
|
192
|
+
- `aes-128-cbc`
|
|
193
|
+
- `aes-192-cbc`
|
|
194
|
+
- `aes-256-cbc`
|
|
195
|
+
- `chacha20-poly1305`
|
|
196
|
+
|
|
197
|
+
</details>
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
#### Methods
|
|
202
|
+
|
|
203
|
+
##### `Cipher.encrypt()`
|
|
204
|
+
|
|
205
|
+
Encrypts an in-memory data buffer.
|
|
206
|
+
|
|
207
|
+
⚠️ This is not suitable for large data.
|
|
208
|
+
Use [`Cipher.streamEncrypt()`](#cipherstreamencrypt) or [`Cipher.hybridEncrypt()`](#cipherhybridencrypt) methods for large data encryption.
|
|
209
|
+
|
|
210
|
+
<details>
|
|
211
|
+
|
|
212
|
+
<summary>Parameters</summary>
|
|
213
|
+
|
|
214
|
+
| Name | Type | Description |
|
|
215
|
+
|----------|-------------------------|-----------------------------------------|
|
|
216
|
+
| `data` | `CoerceToUint8ArrayInput` | Data to encrypt. |
|
|
217
|
+
| `secret` | `CoerceToUint8ArrayInput` | Secret key for encryption. |
|
|
218
|
+
| `options`| `Cph.Options` | (Optional) Additional encryption options.|
|
|
219
|
+
|
|
220
|
+
</details>
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
<details>
|
|
225
|
+
|
|
226
|
+
<summary>Returns</summary>
|
|
227
|
+
|
|
228
|
+
Type: `Buffer`
|
|
229
|
+
|
|
230
|
+
- The encrypted result buffer.
|
|
231
|
+
|
|
232
|
+
</details>
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
- See [`CoerceToUint8ArrayInput`](#coercetouint8arrayinput) for more informations about supported input data types.
|
|
237
|
+
- See [`Cph.Options`](#cphoptionst) for more informations about additional encryption options.
|
|
238
|
+
- See [In-memory data buffer encryption/decryption](#in-memory-data-buffer-encryptiondecryption) examples.
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
##### `Cipher.decrypt()`
|
|
243
|
+
|
|
244
|
+
Decrypts an in-memory data buffer.
|
|
245
|
+
|
|
246
|
+
⚠️ This is not suitable for large data.
|
|
247
|
+
Use [`Cipher.streamDecrypt()`](#cipherstreamdecrypt) or [`Cipher.hybridDecrypt()`](#cipherhybriddecrypt) methods for large data decryption.
|
|
248
|
+
|
|
249
|
+
<details>
|
|
250
|
+
|
|
251
|
+
<summary>Parameters</summary>
|
|
252
|
+
|
|
253
|
+
| Name | Type | Description |
|
|
254
|
+
|----------|-------------------------|-----------------------------------------|
|
|
255
|
+
| `data` | `CoerceToUint8ArrayInput` | Data to decrypt. |
|
|
256
|
+
| `secret` | `CoerceToUint8ArrayInput` | Secret key for decryption. |
|
|
257
|
+
| `options`| `Cph.Options` | (Optional) Decryption options (must match encryption).|
|
|
258
|
+
|
|
259
|
+
</details>
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
<details>
|
|
264
|
+
|
|
265
|
+
<summary>Returns</summary>
|
|
266
|
+
|
|
267
|
+
Type: `Buffer`
|
|
268
|
+
|
|
269
|
+
- The decrypted result buffer.
|
|
270
|
+
|
|
271
|
+
</details>
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
- See [`CoerceToUint8ArrayInput`](#coercetouint8arrayinput) for more informations about supported input data types.
|
|
276
|
+
- See [`Cph.Options`](#cphoptionst) for more informations about additional decryption options.
|
|
277
|
+
- See [In-memory data buffer encryption/decryption](#in-memory-data-buffer-encryptiondecryption) examples.
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
##### `Cipher.streamEncrypt()`
|
|
282
|
+
|
|
283
|
+
Encrypts a `Readable` stream to a `Writable` stream.
|
|
284
|
+
|
|
285
|
+
<details>
|
|
286
|
+
|
|
287
|
+
<summary>Parameters</summary>
|
|
288
|
+
|
|
289
|
+
| Name | Type | Description |
|
|
290
|
+
|-----------|-----------------------------------|---------------------------------------|
|
|
291
|
+
| `secret` | `CoerceToUint8ArrayInput` | Secret key for encryption. |
|
|
292
|
+
| `options` | `Cph.Stream.Symmetric.EncryptOptions` | Stream encryption options. |
|
|
293
|
+
|
|
294
|
+
</details>
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
<details>
|
|
299
|
+
|
|
300
|
+
<summary>Returns</summary>
|
|
301
|
+
|
|
302
|
+
Type: `Promise<void>`
|
|
303
|
+
|
|
304
|
+
- A new Promise that resolves when the stream encryption is complete.
|
|
305
|
+
|
|
306
|
+
</details>
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
- See [`CoerceToUint8ArrayInput`](#coercetouint8arrayinput) for more informations about supported input data types.
|
|
311
|
+
- See [`Cph.Stream.Symmetric.EncryptOptions`](#cphstreamsymmetricencryptoptions) for more informations about additional encryption options.
|
|
312
|
+
- See [In-memory data stream encryption/decryption](#in-memory-data-stream-encryptiondecryption) examples.
|
|
313
|
+
- See [File based data stream encryption/decryption](#file-based-data-stream-encryptiondecryption) examples.
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
##### `Cipher.streamDecrypt()`
|
|
318
|
+
|
|
319
|
+
Decrypts a `Readable` stream to a `Writable` stream.
|
|
320
|
+
|
|
321
|
+
<details>
|
|
322
|
+
|
|
323
|
+
<summary>Parameters</summary>
|
|
324
|
+
|
|
325
|
+
| Name | Type | Description |
|
|
326
|
+
|-----------|-----------------------------------|---------------------------------------|
|
|
327
|
+
| `secret` | `CoerceToUint8ArrayInput` | Secret key for decryption. |
|
|
328
|
+
| `options` | `Cph.Stream.Symmetric.DecryptOptions` | Stream decryption options. |
|
|
329
|
+
|
|
330
|
+
</details>
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
<details>
|
|
335
|
+
|
|
336
|
+
<summary>Returns</summary>
|
|
337
|
+
|
|
338
|
+
Type: `Promise<void>`
|
|
339
|
+
|
|
340
|
+
- A new Promise that resolves when the stream decryption is complete.
|
|
341
|
+
|
|
342
|
+
</details>
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
- See [`CoerceToUint8ArrayInput`](#coercetouint8arrayinput) for more informations about supported input data types.
|
|
347
|
+
- See [`Cph.Stream.Symmetric.DecryptOptions`](#cphstreamsymmetricdecryptoptions) for more informations about additional decryption options.
|
|
348
|
+
- See [In-memory data stream encryption/decryption](#in-memory-data-stream-encryptiondecryption) examples.
|
|
349
|
+
- See [File based data stream encryption/decryption](#file-based-data-stream-encryptiondecryption) examples.
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
##### `Cipher.hybridEncrypt()`
|
|
354
|
+
|
|
355
|
+
Encrypts a stream using hybrid encryption (symmetric + RSA).
|
|
356
|
+
|
|
357
|
+
<details>
|
|
358
|
+
|
|
359
|
+
<summary>Parameters</summary>
|
|
360
|
+
|
|
361
|
+
| Name | Type | Description |
|
|
362
|
+
|-------------|-----------------------------------|---------------------------------------|
|
|
363
|
+
| `secret` | `CoerceToUint8ArrayInput` | Symmetric secret key. |
|
|
364
|
+
| `publicKey` | `crypto.RsaPublicKey \| crypto.KeyLike` | RSA public key used to encrypt the generated symmetric key. |
|
|
365
|
+
| `options` | `Cph.Stream.Hybrid.EncryptOptions` | Stream encryption options. |
|
|
366
|
+
|
|
367
|
+
</details>
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
<details>
|
|
372
|
+
|
|
373
|
+
<summary>Returns</summary>
|
|
374
|
+
|
|
375
|
+
Type: `Promise<void>`
|
|
376
|
+
|
|
377
|
+
- A new Promise that resolves when hybrid encryption is complete.
|
|
378
|
+
|
|
379
|
+
</details>
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
- See [`CoerceToUint8ArrayInput`](#coercetouint8arrayinput) for more informations about supported input data types.
|
|
384
|
+
- See [`Cph.Stream.Hybrid.EncryptOptions`](#cphstreamhybridencryptoptions) for more informations about additional encryption options.
|
|
385
|
+
- See [In-memory data stream with hybrid encryption/decryption](#in-memory-data-stream-with-hybrid-encryptiondecryption) examples.
|
|
386
|
+
- See [File based data stream with hybrid encryption/decryption](#file-based-data-stream-with-hybrid-encryptiondecryption) examples.
|
|
387
|
+
|
|
388
|
+
---
|
|
389
|
+
|
|
390
|
+
##### `Cipher.hybridDecrypt()`
|
|
391
|
+
|
|
392
|
+
Decrypts a stream using hybrid decryption (symmetric + RSA).
|
|
393
|
+
|
|
394
|
+
<details>
|
|
395
|
+
|
|
396
|
+
<summary>Parameters</summary>
|
|
397
|
+
|
|
398
|
+
| Name | Type | Description |
|
|
399
|
+
|--------------|-----------------------------------|---------------------------------------|
|
|
400
|
+
| `privateKey` | `crypto.RsaPrivateKey \| crypto.KeyLike` | RSA private key for used to decrpyt the encrypted symmetric key. |
|
|
401
|
+
| `options` | `Cph.Stream.Hybrid.DecryptOptions` | Stream decryption options. |
|
|
402
|
+
|
|
403
|
+
</details>
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
<details>
|
|
408
|
+
|
|
409
|
+
<summary>Returns</summary>
|
|
410
|
+
|
|
411
|
+
Type: `Promise<void>`
|
|
412
|
+
|
|
413
|
+
- A new Promise that resolves when hybrid decryption is complete.
|
|
414
|
+
|
|
415
|
+
</details>
|
|
416
|
+
|
|
417
|
+
---
|
|
418
|
+
|
|
419
|
+
- See [`Cph.Stream.Hybrid.DecryptOptions`](#cphstreamhybriddecryptoptions) for more informations about decryption options.
|
|
420
|
+
- See [In-memory data stream with hybrid encryption/decryption](#in-memory-data-stream-with-hybrid-encryptiondecryption) examples.
|
|
421
|
+
- See [File based data stream with hybrid encryption/decryption](#file-based-data-stream-with-hybrid-encryptiondecryption) examples.
|
|
422
|
+
|
|
423
|
+
---
|
|
424
|
+
|
|
425
|
+
#### Types
|
|
426
|
+
|
|
427
|
+
##### `CoerceToUint8ArrayInput`
|
|
428
|
+
|
|
429
|
+
This module supports different input data types and it uses the [`coerceToUint8Array`](https://npmjs.com/package/@alessiofrittoli/crypto-buffer#coercetouint8array) utility function from [`@alessiofrittoli/crypto-buffer`](https://npmjs.com/package/@alessiofrittoli/crypto-buffer) to convert it to a `Uint8Array`.
|
|
430
|
+
|
|
431
|
+
- See [`coerceToUint8Array`](https://npmjs.com/package/@alessiofrittoli/crypto-buffer#coercetouint8array) for more informations about the supported input types.
|
|
432
|
+
|
|
433
|
+
---
|
|
434
|
+
|
|
435
|
+
##### `CBCTypes`
|
|
436
|
+
|
|
437
|
+
Cipher CBC algorithm types.
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
##### `AesAlgorithm`
|
|
442
|
+
|
|
443
|
+
Supported AES algorithm types.
|
|
444
|
+
|
|
445
|
+
---
|
|
446
|
+
|
|
447
|
+
##### `Cph.Options<T>`
|
|
448
|
+
|
|
449
|
+
Common options in encryption/decryption processes.
|
|
450
|
+
|
|
451
|
+
<details>
|
|
452
|
+
|
|
453
|
+
<summary>Type parameters</summary>
|
|
454
|
+
|
|
455
|
+
| Parameter | Default | Description |
|
|
456
|
+
|-------------|----------------|-------------|
|
|
457
|
+
| `T` | `AesAlgorithm` | Accepted algorithm in `Cph.Options`. This is usefull to constraint specifc algorithms. |
|
|
458
|
+
|
|
459
|
+
</details>
|
|
460
|
+
|
|
461
|
+
---
|
|
462
|
+
|
|
463
|
+
<details>
|
|
464
|
+
|
|
465
|
+
<summary>Properties</summary>
|
|
466
|
+
|
|
467
|
+
| Property | Type | Default | Description |
|
|
468
|
+
|-------------|----------|---------------|-------------|
|
|
469
|
+
| `algorithm` | `T` | `aes-256-gcm \| aes-256-cbc` | Accepted algorithms. |
|
|
470
|
+
| `salt` | `number` | `32` | The `salt` length in bytes. Minimum: `16`, Maximum: `64`. |
|
|
471
|
+
| `iv` | `number` | `16` | The Initialization Vector length in bytes. Minimum: `8`, Maximum: `32`. |
|
|
472
|
+
| `authTag` | `number` | `16` | The `authTag` length in bytes. Minimum: `4`, Maximum: `16`. |
|
|
473
|
+
| `aad` | `CoerceToUint8ArrayInput` | - | Custom Additional Authenticated Data. `aadLength` is then automatically resolved. If not provided, a random AAD is generated with a max length of `aadLength`. |
|
|
474
|
+
| `aadLength` | `number` | `32` | The auto generated AAD length in bytes. Minimum: `16`, Maximum: `128`. |
|
|
475
|
+
|
|
476
|
+
</details>
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
##### `Cph.Stream.Symmetric.EncryptOptions`
|
|
481
|
+
|
|
482
|
+
Stream symmetric encryption options.
|
|
483
|
+
|
|
484
|
+
- Extends [`Cph.Options<Cph.CBCTypes>`](#cphoptionst).
|
|
485
|
+
|
|
486
|
+
<details>
|
|
487
|
+
|
|
488
|
+
<summary>Properties</summary>
|
|
489
|
+
|
|
490
|
+
| Property | Type | Description |
|
|
491
|
+
|----------|----------|-------------|
|
|
492
|
+
| `input` | `Readable` | The `Readable` Stream from where raw data to encrypt is read. |
|
|
493
|
+
| `output` | `Writable` | The `Writable` Stream where encrypted data is written. |
|
|
494
|
+
|
|
495
|
+
</details>
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
##### `Cph.Stream.Symmetric.DecryptOptions`
|
|
500
|
+
|
|
501
|
+
Stream symmetric decryption options.
|
|
502
|
+
|
|
503
|
+
- Extends [`Cph.Stream.Symmetric.EncryptOptions`](#cphstreamsymmetricencryptoptions).
|
|
504
|
+
|
|
505
|
+
<details>
|
|
506
|
+
|
|
507
|
+
<summary>Properties</summary>
|
|
508
|
+
|
|
509
|
+
| Property | Type | Description |
|
|
510
|
+
|----------|----------|-------------|
|
|
511
|
+
| `input` | `Readable` | The `Readable` Stream from where encrypted data is read. |
|
|
512
|
+
| `output` | `Writable` | The `Writable` Stream where decrypted data is written. |
|
|
513
|
+
|
|
514
|
+
</details>
|
|
515
|
+
|
|
516
|
+
---
|
|
517
|
+
|
|
518
|
+
##### `Cph.Stream.Hybrid.EncryptOptions`
|
|
519
|
+
|
|
520
|
+
Stream hybrid encryption options.
|
|
521
|
+
|
|
522
|
+
- Alias for [`Cph.Stream.Symmetric.EncryptOptions`](#cphstreamsymmetricencryptoptions)
|
|
523
|
+
|
|
524
|
+
---
|
|
525
|
+
|
|
526
|
+
##### `Cph.Stream.Hybrid.DecryptOptions`
|
|
527
|
+
|
|
528
|
+
Stream hybrid decryption options.
|
|
529
|
+
|
|
530
|
+
- Extends [`Cph.Stream.Symmetric.DecryptOptions`](#cphstreamsymmetricdecryptoptions).
|
|
531
|
+
|
|
532
|
+
<details>
|
|
533
|
+
|
|
534
|
+
<summary>Properties</summary>
|
|
535
|
+
|
|
536
|
+
| Property | Type | Description |
|
|
537
|
+
|----------------|----------|-------------|
|
|
538
|
+
| `rsaKeyLength` | `number` | The RSA key length in bytes used while encrypting data. This is used to properly extract the encrypted Cipher Key and Initialization Vector from the encrypted data. |
|
|
539
|
+
|
|
540
|
+
</details>
|
|
541
|
+
|
|
542
|
+
---
|
|
543
|
+
|
|
544
|
+
### Examples
|
|
545
|
+
|
|
546
|
+
#### Importing the library
|
|
547
|
+
|
|
548
|
+
```ts
|
|
549
|
+
import { Cipher } from '@alessiofrittoli/crypto-cipher'
|
|
550
|
+
import type { Cph as CipherTypes } from '@alessiofrittoli/crypto-cipher/types'
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
#### In-memory data buffer encryption/decryption
|
|
554
|
+
|
|
555
|
+
The simpliest way to encrypt/decrypt in-memory data buffers.
|
|
556
|
+
|
|
557
|
+
```ts
|
|
558
|
+
// encrypt
|
|
559
|
+
const data = 'my top-secret data'
|
|
560
|
+
const password = 'my-very-strong-password'
|
|
561
|
+
|
|
562
|
+
const encrypted = Cipher.encrypt( data, password )
|
|
563
|
+
|
|
564
|
+
// decrypt
|
|
565
|
+
const decrypted = Cipher.decrypt( encrypted, password )
|
|
566
|
+
console.log( decrypted ) // Outputs: my top-secret data
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
---
|
|
570
|
+
|
|
571
|
+
#### In-memory data stream encryption/decryption
|
|
572
|
+
|
|
573
|
+
The in-memory data stream comes pretty handy when, for example, we need to stream encrypted data within a Server Response or to decrypt stream data from a Server Response.
|
|
574
|
+
|
|
575
|
+
##### Streaming
|
|
576
|
+
|
|
577
|
+
```ts
|
|
578
|
+
// /api/stream-encrypt
|
|
579
|
+
|
|
580
|
+
import { Readable, Writable } from 'stream'
|
|
581
|
+
|
|
582
|
+
const routeHandler = () => {
|
|
583
|
+
const data = 'my top-secret data'
|
|
584
|
+
const password = 'my-very-strong-password'
|
|
585
|
+
const stream = new TransformStream()
|
|
586
|
+
const writer = stream.writable.getWriter()
|
|
587
|
+
|
|
588
|
+
// Create a `Readable` Stream with raw data.
|
|
589
|
+
const input = new Readable( {
|
|
590
|
+
read()
|
|
591
|
+
{
|
|
592
|
+
this.push( data )
|
|
593
|
+
this.push( null ) // Signal end of stream
|
|
594
|
+
},
|
|
595
|
+
} )
|
|
596
|
+
|
|
597
|
+
// `Writable` Stream where encrypted data is written
|
|
598
|
+
const output = new Writable( {
|
|
599
|
+
write( chunk, encoding, callback )
|
|
600
|
+
{
|
|
601
|
+
writer.write( chunk )
|
|
602
|
+
callback()
|
|
603
|
+
},
|
|
604
|
+
final( callback )
|
|
605
|
+
{
|
|
606
|
+
writer.close()
|
|
607
|
+
callback()
|
|
608
|
+
}
|
|
609
|
+
} )
|
|
610
|
+
|
|
611
|
+
Cipher.streamEncrypt( password, { input, output } )
|
|
612
|
+
|
|
613
|
+
return (
|
|
614
|
+
// encrypted stream
|
|
615
|
+
new Response( stream.readable )
|
|
616
|
+
)
|
|
617
|
+
}
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
##### Decrypting received stream
|
|
621
|
+
|
|
622
|
+
```ts
|
|
623
|
+
// /api/stream-decrypt
|
|
624
|
+
|
|
625
|
+
import { Transform, Writable } from 'stream'
|
|
626
|
+
import { StreamReader } from '@alessiofrittoli/stream-reader'
|
|
627
|
+
|
|
628
|
+
const password = 'my-very-strong-password'
|
|
629
|
+
|
|
630
|
+
const routeHandler = () => (
|
|
631
|
+
fetch( '/api/stream-encrypt' )
|
|
632
|
+
.then( response => {
|
|
633
|
+
|
|
634
|
+
if ( ! response.body ) {
|
|
635
|
+
return (
|
|
636
|
+
new Respone( null, { status: 400 } )
|
|
637
|
+
)
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const stream = new TransformStream()
|
|
641
|
+
const writer = stream.writable.getWriter()
|
|
642
|
+
const reader = new StreamReader( response.body )
|
|
643
|
+
const input = new Transform()
|
|
644
|
+
|
|
645
|
+
reader.read()
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
reader.on( 'read', chunk => {
|
|
649
|
+
input.push( chunk )
|
|
650
|
+
} )
|
|
651
|
+
reader.on( 'close', () => {
|
|
652
|
+
input.push( null )
|
|
653
|
+
} )
|
|
654
|
+
|
|
655
|
+
const output = new Writable( {
|
|
656
|
+
write( chunk, encoding, callback )
|
|
657
|
+
{
|
|
658
|
+
writer.write( chunk )
|
|
659
|
+
callback()
|
|
660
|
+
},
|
|
661
|
+
final( callback ) {
|
|
662
|
+
writer.close()
|
|
663
|
+
callback()
|
|
664
|
+
},
|
|
665
|
+
} )
|
|
666
|
+
|
|
667
|
+
Cipher.streamDecrypt( password, { input, output } )
|
|
668
|
+
|
|
669
|
+
return (
|
|
670
|
+
// decrypted stream
|
|
671
|
+
new Response( stream.readable )
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
} )
|
|
675
|
+
)
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
---
|
|
679
|
+
|
|
680
|
+
#### In-memory data stream with hybrid encryption/decryption
|
|
681
|
+
|
|
682
|
+
Hybrid encryption offers an higher level of security by encrypting the generated symmetric key with asymmetric RSA keys.
|
|
683
|
+
|
|
684
|
+
##### Keypair
|
|
685
|
+
|
|
686
|
+
```ts
|
|
687
|
+
const password = 'my-very-strong-password'
|
|
688
|
+
|
|
689
|
+
/** RSA modulus length is required for proper key extraction during decryption process. */
|
|
690
|
+
const rsaKeyLength = 512 // bytes
|
|
691
|
+
const keyPair = crypto.generateKeyPairSync( 'rsa', {
|
|
692
|
+
modulusLength : rsaKeyLength * 8, // 4096 bits
|
|
693
|
+
publicKeyEncoding : { type: 'spki', format: 'pem' },
|
|
694
|
+
privateKeyEncoding : { type: 'pkcs1', format: 'pem' },
|
|
695
|
+
} )
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
// or you can optionally set a custom passphrase
|
|
699
|
+
const keyPair = crypto.generateKeyPairSync( 'rsa', {
|
|
700
|
+
modulusLength : rsaKeyLength * 8, // 4096 bits
|
|
701
|
+
publicKeyEncoding : { type: 'spki', format: 'pem' },
|
|
702
|
+
privateKeyEncoding : { type: 'pkcs1', format: 'pem', passphrase: password, cipher: 'aes-256-cbc' },
|
|
703
|
+
} )
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
##### Encrypt
|
|
707
|
+
|
|
708
|
+
```ts
|
|
709
|
+
const data = 'my top-secret data'
|
|
710
|
+
/** Store encrypted chunks for next example. */
|
|
711
|
+
const encryptedChunks: Buffer[] = []
|
|
712
|
+
|
|
713
|
+
// Create a `Readable` Stream with raw data.
|
|
714
|
+
const input = new Readable( {
|
|
715
|
+
read()
|
|
716
|
+
{
|
|
717
|
+
this.push( data ) // Push data to encrypt
|
|
718
|
+
this.push( null ) // Signal end of stream
|
|
719
|
+
},
|
|
720
|
+
} )
|
|
721
|
+
|
|
722
|
+
// Create a `Writable` Stream where encrypted data is written
|
|
723
|
+
const output = new Writable( {
|
|
724
|
+
write( chunk, encoding, callback )
|
|
725
|
+
{
|
|
726
|
+
// push written chunk to `encryptedChunks` for further usage.
|
|
727
|
+
encryptedChunks.push( chunk )
|
|
728
|
+
callback()
|
|
729
|
+
}
|
|
730
|
+
} )
|
|
731
|
+
|
|
732
|
+
await Cipher.hybridEncrypt( password, {
|
|
733
|
+
key : keyPair.publicKey,
|
|
734
|
+
padding : crypto.constants.RSA_PKCS1_OAEP_PADDING,
|
|
735
|
+
oaepHash : 'SHA-256',
|
|
736
|
+
}, { input, output } )
|
|
737
|
+
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
---
|
|
741
|
+
|
|
742
|
+
##### Decrypt
|
|
743
|
+
|
|
744
|
+
```ts
|
|
745
|
+
/** Store decrypted chunks. */
|
|
746
|
+
const chunks: Buffer[] = []
|
|
747
|
+
// Create a `Readable` Stream with encrypted data.
|
|
748
|
+
const input = new Readable( {
|
|
749
|
+
read()
|
|
750
|
+
{
|
|
751
|
+
this.push( Buffer.concat( encryptedChunks ) ) // Push data to decrypt
|
|
752
|
+
this.push( null ) // Signal end of stream
|
|
753
|
+
},
|
|
754
|
+
} )
|
|
755
|
+
|
|
756
|
+
// Create a `Writable` Stream where decrypted data is written
|
|
757
|
+
const output = new Writable( {
|
|
758
|
+
write( chunk, encoding, callback )
|
|
759
|
+
{
|
|
760
|
+
chunks.push( chunk )
|
|
761
|
+
callback()
|
|
762
|
+
},
|
|
763
|
+
} )
|
|
764
|
+
|
|
765
|
+
await Cipher.hybridDecrypt(
|
|
766
|
+
{
|
|
767
|
+
key : keyPair.privateKey,
|
|
768
|
+
passphrase: password, // optional passhrase (required if set while generating keypair).
|
|
769
|
+
padding : crypto.constants.RSA_PKCS1_OAEP_PADDING,
|
|
770
|
+
oaepHash : 'SHA-256',
|
|
771
|
+
}, { input, output, rsaKeyLength }
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
console.log( Buffer.concat( chunks ).toString() ) // Outputs: 'my top-secret data'
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
---
|
|
778
|
+
|
|
779
|
+
#### File based data stream encryption/decryption
|
|
780
|
+
|
|
781
|
+
Nothig differs from the [In-memory data stream encryption/decryption](#in-memory-data-stream-encryptiondecryption) example, except for `input` and `output` streams which now comes directly from files reading/writing.
|
|
782
|
+
|
|
783
|
+
##### Encrypt a file
|
|
784
|
+
|
|
785
|
+
```ts
|
|
786
|
+
import fs from 'fs'
|
|
787
|
+
|
|
788
|
+
const password = 'my-very-strong-password'
|
|
789
|
+
|
|
790
|
+
// input where raw data to encrypt is read
|
|
791
|
+
const input = fs.createReadStream( 'my-very-large-top-secret-file.pdf' )
|
|
792
|
+
// output where encrypted data is written
|
|
793
|
+
const output = fs.createWriteStream( 'my-very-large-top-secret-file.encrypted' )
|
|
794
|
+
// encrypt
|
|
795
|
+
await Cipher.streamEncrypt( password, { input, output } )
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
---
|
|
799
|
+
|
|
800
|
+
##### Decrypt a file
|
|
801
|
+
|
|
802
|
+
```ts
|
|
803
|
+
import fs from 'fs'
|
|
804
|
+
|
|
805
|
+
const password = 'my-very-strong-password'
|
|
806
|
+
|
|
807
|
+
// input where encrypted data is read
|
|
808
|
+
const input = fs.createReadStream( 'my-very-large-top-secret-file.encrypted' )
|
|
809
|
+
// output where decrypted data is written
|
|
810
|
+
const output = fs.createWriteStream( 'my-very-large-top-secret-file-decrypted.pdf' )
|
|
811
|
+
// decrypt
|
|
812
|
+
await Cipher.streamDecrypt( password, { input, output } )
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
---
|
|
816
|
+
|
|
817
|
+
#### File based data stream with hybrid encryption/decryption
|
|
818
|
+
|
|
819
|
+
Nothig differs from the [In-memory data stream with hybrid encryption/decryption](#in-memory-data-stream-with-hybrid-encryptiondecryption) example, except for `input` and `output` streams which now comes directly from files reading/writing.
|
|
820
|
+
|
|
821
|
+
##### Keypair
|
|
822
|
+
|
|
823
|
+
```ts
|
|
824
|
+
const password = 'my-very-strong-password'
|
|
825
|
+
|
|
826
|
+
/** RSA modulus length is required for proper key extraction during decryption process. */
|
|
827
|
+
const rsaKeyLength = 512 // bytes
|
|
828
|
+
const keyPair = crypto.generateKeyPairSync( 'rsa', {
|
|
829
|
+
modulusLength : rsaKeyLength * 8, // 4096 bits
|
|
830
|
+
publicKeyEncoding : { type: 'spki', format: 'pem' },
|
|
831
|
+
privateKeyEncoding : { type: 'pkcs1', format: 'pem' },
|
|
832
|
+
} )
|
|
833
|
+
|
|
834
|
+
|
|
835
|
+
// or you can optionally set a custom passphrase
|
|
836
|
+
const keyPair = crypto.generateKeyPairSync( 'rsa', {
|
|
837
|
+
modulusLength : rsaKeyLength * 8, // 4096 bits
|
|
838
|
+
publicKeyEncoding : { type: 'spki', format: 'pem' },
|
|
839
|
+
privateKeyEncoding : { type: 'pkcs1', format: 'pem', passphrase: password, cipher: 'aes-256-cbc' },
|
|
840
|
+
} )
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
##### Encrypt a file
|
|
844
|
+
|
|
845
|
+
```ts
|
|
846
|
+
import fs from 'fs'
|
|
847
|
+
|
|
848
|
+
const password = 'my-very-strong-password'
|
|
849
|
+
|
|
850
|
+
// input where raw data to encrypt is read
|
|
851
|
+
const input = fs.createReadStream( 'my-very-large-top-secret-file.pdf' )
|
|
852
|
+
// output where encrypted data is written
|
|
853
|
+
const output = fs.createWriteStream( 'my-very-large-top-secret-file.encrypted' )
|
|
854
|
+
// encrypt
|
|
855
|
+
await Cipher.hybridEncrypt( password, {
|
|
856
|
+
key : keyPair.publicKey,
|
|
857
|
+
padding : crypto.constants.RSA_PKCS1_OAEP_PADDING,
|
|
858
|
+
oaepHash : 'SHA-256',
|
|
859
|
+
}, { input, output } )
|
|
860
|
+
```
|
|
861
|
+
|
|
862
|
+
---
|
|
863
|
+
|
|
864
|
+
##### Decrypt a file
|
|
865
|
+
|
|
866
|
+
```ts
|
|
867
|
+
import fs from 'fs'
|
|
868
|
+
|
|
869
|
+
const password = 'my-very-strong-password'
|
|
870
|
+
|
|
871
|
+
// input where encrypted data is read
|
|
872
|
+
const input = fs.createReadStream( 'my-very-large-top-secret-file.encrypted' )
|
|
873
|
+
// output where decrypted data is written
|
|
874
|
+
const output = fs.createWriteStream( 'my-very-large-top-secret-file-decrypted.pdf' )
|
|
875
|
+
// decrypt
|
|
876
|
+
await Cipher.hybridDecrypt(
|
|
877
|
+
{
|
|
878
|
+
key : keyPair.privateKey,
|
|
879
|
+
passphrase: password, // optional passhrase (required if set while generating keypair).
|
|
880
|
+
padding : crypto.constants.RSA_PKCS1_OAEP_PADDING,
|
|
881
|
+
oaepHash : 'SHA-256',
|
|
882
|
+
}, { input, output, rsaKeyLength }
|
|
883
|
+
)
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
---
|
|
887
|
+
|
|
888
|
+
### Development
|
|
889
|
+
|
|
890
|
+
#### Install depenendencies
|
|
891
|
+
|
|
892
|
+
```bash
|
|
893
|
+
npm install
|
|
894
|
+
```
|
|
895
|
+
|
|
896
|
+
or using `pnpm`
|
|
897
|
+
|
|
898
|
+
```bash
|
|
899
|
+
pnpm i
|
|
900
|
+
```
|
|
901
|
+
|
|
902
|
+
#### Build the source code
|
|
903
|
+
|
|
904
|
+
Run the following command to test and build code for distribution.
|
|
905
|
+
|
|
906
|
+
```bash
|
|
907
|
+
pnpm build
|
|
908
|
+
```
|
|
909
|
+
|
|
910
|
+
#### [ESLint](https://www.npmjs.com/package/eslint)
|
|
911
|
+
|
|
912
|
+
warnings / errors check.
|
|
913
|
+
|
|
914
|
+
```bash
|
|
915
|
+
pnpm lint
|
|
916
|
+
```
|
|
917
|
+
|
|
918
|
+
#### [Jest](https://npmjs.com/package/jest)
|
|
919
|
+
|
|
920
|
+
Run all the defined test suites by running the following:
|
|
921
|
+
|
|
922
|
+
```bash
|
|
923
|
+
# Run tests and watch file changes.
|
|
924
|
+
pnpm test:watch
|
|
925
|
+
|
|
926
|
+
# Run tests and watch file changes with jest-environment-jsdom.
|
|
927
|
+
pnpm test:jsdom
|
|
928
|
+
|
|
929
|
+
# Run tests in a CI environment.
|
|
930
|
+
pnpm test:ci
|
|
931
|
+
|
|
932
|
+
# Run tests in a CI environment with jest-environment-jsdom.
|
|
933
|
+
pnpm test:ci:jsdom
|
|
934
|
+
```
|
|
935
|
+
|
|
936
|
+
You can eventually run specific suits like so:
|
|
937
|
+
|
|
938
|
+
```bash
|
|
939
|
+
pnpm test:jest
|
|
940
|
+
pnpm test:jest:jsdom
|
|
941
|
+
```
|
|
942
|
+
|
|
943
|
+
Run tests with coverage.
|
|
944
|
+
|
|
945
|
+
An HTTP server is then started to serve coverage files from `./coverage` folder.
|
|
946
|
+
|
|
947
|
+
⚠️ You may see a blank page the first time you run this command. Simply refresh the browser to see the updates.
|
|
948
|
+
|
|
949
|
+
```bash
|
|
950
|
+
test:coverage:serve
|
|
951
|
+
```
|
|
952
|
+
|
|
953
|
+
---
|
|
954
|
+
|
|
955
|
+
### Contributing
|
|
956
|
+
|
|
957
|
+
Contributions are truly welcome!\
|
|
958
|
+
Please refer to the [Contributing Doc](./CONTRIBUTING.md) for more information on how to start contributing to this project.
|
|
959
|
+
|
|
960
|
+
---
|
|
961
|
+
|
|
962
|
+
### Security
|
|
963
|
+
|
|
964
|
+
If you believe you have found a security vulnerability, we encourage you to **_responsibly disclose this and NOT open a public issue_**. We will investigate all legitimate reports. Email `security@alessiofrittoli.it` to disclose any security vulnerabilities.
|
|
965
|
+
|
|
966
|
+
### Made with ☕
|
|
967
|
+
|
|
968
|
+
<table style='display:flex;gap:20px;'>
|
|
969
|
+
<tbody>
|
|
970
|
+
<tr>
|
|
971
|
+
<td>
|
|
972
|
+
<img alt="avatar" src='https://avatars.githubusercontent.com/u/35973186' style='width:60px;border-radius:50%;object-fit:contain;'>
|
|
973
|
+
</td>
|
|
974
|
+
<td>
|
|
975
|
+
<table style='display:flex;gap:2px;flex-direction:column;'>
|
|
976
|
+
<tbody>
|
|
977
|
+
<tr>
|
|
978
|
+
<td>
|
|
979
|
+
<a href='https://github.com/alessiofrittoli' target='_blank' rel='noopener'>Alessio Frittoli</a>
|
|
980
|
+
</td>
|
|
981
|
+
</tr>
|
|
982
|
+
<tr>
|
|
983
|
+
<td>
|
|
984
|
+
<small>
|
|
985
|
+
<a href='https://alessiofrittoli.it' target='_blank' rel='noopener'>https://alessiofrittoli.it</a> |
|
|
986
|
+
<a href='mailto:info@alessiofrittoli.it' target='_blank' rel='noopener'>info@alessiofrittoli.it</a>
|
|
987
|
+
</small>
|
|
988
|
+
</td>
|
|
989
|
+
</tr>
|
|
990
|
+
</tbody>
|
|
991
|
+
</table>
|
|
992
|
+
</td>
|
|
993
|
+
</tr>
|
|
994
|
+
</tbody>
|
|
995
|
+
</table>
|