- Getting started
- Key features
- What's changed
- Migration guide
- API Reference
- Examples
- Development
- Contributing
- Security
- Credits
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 standard recommendations.
It is part of the crypto utility libraries and can be installed by running the following command:
npm i @alessiofrittoli/crypto-cipheror using pnpm
pnpm i @alessiofrittoli/crypto-cipher- Supports multiple AES algorithms (
CCM,GCM,OCB,CBC) and evenchacha20-poly1305. - In-memory buffer encryption and decrpytion.
- Robust support for encrypting and decrypting streams (in-memory and file based).
- Hybrid encryption methods for combining symmetric and asymmetric cryptography.
- Random
saltandIVgeneration. - AEAD - Authenticated encryption modes with proper
authTagandAdditional Authenticated Datahandling.
- Separation of concerns with clear method responsibilities.
- Comprehensive JSDoc comments enhance maintainability and readability.
🎉 Core updates in the latest release:
Ciphermethods have been refactored to provide a solid and easy usageCipher.HybridEncrypt()andCipher.HybridDecrypt()have been added. These methods will allow you to encrypt/decrypt in-memory buffer data using hybrid encryption algorithms.Cipher.streamsubgroup has been added to keep method names consistent across the library.- hybrid encryption doesn't require a password anymore which was redundant. an RSA key pair is all what you need.
- providing RSA key length during hybrid decryption is no longer needed.
Cipher.encrypt()
Is now renamed to Cipher.Encrypt(). It's API implementation remain the same.
Cipher.decrypt()
Is now renamed to Cipher.Decrypt(). It's API implementation remain the same.
Cipher.streamEncrypt()
Before
await Cipher.streamEncrypt(password, { input, output }).encrypt();Now
await Cipher.stream.Encrypt(password, { input, output });Cipher.streamDecrypt()
Before
const { decrypt } = await Cipher.streamDecrypt(password, { input, output });
await decrypt();Now
await Cipher.stream.Decrypt(password, { input, output });Cipher.hybridEncrypt()
Before
const { encrypt } = Cipher.hybridEncrypt(
password,
{
key: keyPair.publicKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: "SHA-256",
},
{ input, output }
);
await encrypt();Now
await Cipher.stream.HybridEncrypt(keyPair.publicKey, { input, output });Cipher.hybridDecrypt()
Before
const { decrypt } = await Cipher.hybridDecrypt(
{
key: keyPair.privateKey,
passphrase: passphrase,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: "SHA-256",
},
{ input, output, rsaKeyLength }
);
await decrypt();Now
await Cipher.stream.HybridDecrypt(
{ key: keyPair.privateKey, passphrase },
{ input, output }
);Or even
await Cipher.stream.HybridDecrypt(keyPair.privateKey, { input, output });Defines the minimum, maximum, and default lengths for salt.
Properties
| Property | Value |
|---|---|
min |
16 |
max |
64 |
default |
32 |
Defines the minimum, maximum, and default lengths for initialization vectors (IV).
Properties
| Property | Value |
|---|---|
min |
8 |
max |
32 |
default |
16 |
Defines the minimum, maximum, and default lengths for authentication tags.
Properties
| Property | Value |
|---|---|
min |
4 |
max |
16 |
default |
16 |
Defines the minimum, maximum, and default lengths for additional authenticated data (AAD).
Properties
| Property | Value |
|---|---|
min |
16 |
max |
4096 |
default |
32 |
Specifies default AES algorithms for buffer and stream operations.
Properties
| Operation | Algorithm | Description |
|---|---|---|
buffer |
aes-256-gcm |
Default algorithm used for buffer data encryption/decryption |
stream |
aes-256-cbc |
Default algorithm used for stream encryption/decryption (Cipher Block Chaining mode). |
An object defining algorithm names.
Properties
| Property | Value |
|---|---|
AES_128_CBC |
'aes-128-cbc' |
AES_192_CBC |
'aes-192-cbc' |
AES_256_CBC |
'aes-256-cbc' |
AES_128_CCM |
'aes-128-ccm' |
AES_192_CCM |
'aes-192-ccm' |
AES_256_CCM |
'aes-256-ccm' |
AES_128_GCM |
'aes-128-gcm' |
AES_192_GCM |
'aes-192-gcm' |
AES_256_GCM |
'aes-256-gcm' |
AES_128_OCB |
'aes-128-ocb' |
AES_192_OCB |
'aes-192-ocb' |
AES_256_OCB |
'aes-256-ocb' |
CHACHA_20_POLY |
'chacha20-poly1305' |
An array of supported AES algorithms. This array includes all values in Cipher.ALGORITHM constant.
Encrypts an in-memory data buffer.
Warning
This is not suitable for large data encryption. Use Cipher.stream.Encrypt() or Cipher.stream.HybridEncrypt() methods for large data encryption.
Parameters
| Name | Type | Description |
|---|---|---|
data |
CoerceToUint8ArrayInput |
Data to encrypt. |
secret |
CoerceToUint8ArrayInput |
Secret key for encryption. |
options |
Cph.Options |
(Optional) Additional encryption options. |
Returns
Type: Buffer
The encrypted result buffer.
- See
CoerceToUint8ArrayInputfor more informations about supported input data types. - See
Cph.Optionsfor more informations about additional encryption options. - See In-memory data buffer encryption/decryption examples.
Decrypts an in-memory data buffer.
Warning
This is not suitable for large data decryption. Use Cipher.stream.Decrypt() or Cipher.stream.HybridDecrypt() methods for large data decryption.
Parameters
| Name | Type | Description |
|---|---|---|
data |
CoerceToUint8ArrayInput |
Data to decrypt. |
secret |
CoerceToUint8ArrayInput |
Secret key for decryption. |
options |
Cph.Options |
(Optional) Decryption options (must match encryption). |
Returns
Type: Buffer
The decrypted result buffer.
- See
CoerceToUint8ArrayInputfor more informations about supported input data types. - See
Cph.Optionsfor more informations about additional decryption options. - See In-memory data buffer encryption/decryption examples.
Encrypts in-memory data using hybrid encryption.
Warning
This is not suitable for large data encryption. Use Cipher.stream.HybridEncrypt() method for large data encryption.
Warning
Please, note that when using hybrid encryption/decryption algorithms:
- an RSA keypair is required.
- if a passphrase is set for the Private Key, please make sure to use one of the Cipher Block Chaining algorithm or
chacha20-poly1305algorithm:- aes-128-cbc (
typecan bepkcs1orpkcs8) - aes-192-cbc (
typecan bepkcs1orpkcs8) - aes-256-cbc (
typecan bepkcs1orpkcs8) - chacha20-poly1305 (
typecan only bepkcs1)
- aes-128-cbc (
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
data |
CoerceToUint8ArrayInput |
- | The data to encrypt. |
key |
crypto.KeyLike |
- | The RSA Public Key. |
options |
Cph.Options |
- | (Optional) Additional options. |
Returns
Type: Buffer
The encrypted result buffer.
- See
CoerceToUint8ArrayInputfor more informations about supported input data types. - See
Cph.Optionsfor more informations about additional decryption options. - See In-memory data buffer hybrid encryption/decryption example.
Decrypts in-memory data using hybrid encryption.
Warning
This is not suitable for large data decryption. Use Cipher.stream.HybridDecrypt() method for large data decryption.
Warning
Please, note that when using hybrid encryption/decryption algorithms:
- an RSA keypair is required.
- if a passphrase is set for the Private Key, please make sure to use one of the Cipher Block Chaining algorithm or
chacha20-poly1305algorithm:- aes-128-cbc (
typecan bepkcs1orpkcs8) - aes-192-cbc (
typecan bepkcs1orpkcs8) - aes-256-cbc (
typecan bepkcs1orpkcs8) - chacha20-poly1305 (
typecan only bepkcs1)
- aes-128-cbc (
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
data |
CoerceToUint8ArrayInput |
- | The data to encrypt. |
key |
Cph.PrivateKey |
- | The RSA Private Key. |
options |
Cph.Options |
- | (Optional) Additional options. |
Returns
Type: Buffer.
The encrypted result Buffer.
- See
CoerceToUint8ArrayInputfor more informations about supported input data types. - See
Cph.PrivateKeyfor accepted formats. - See
Cph.Optionsfor more informations about additional decryption options. - See In-memory data buffer hybrid encryption/decryption example.
Encrypt stream data.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
secret |
CoerceToUint8ArrayInput |
- | The secret key used to encrypt the data. |
options |
Cph.Stream.EncryptOptions |
- | An object defining required options. |
options.input |
Readable |
- | The Readable Stream where raw data to encrypt is read. |
options.output |
Writable |
- | The Writable Stream where encrypted data is written. |
options.algorithm |
Cph.CBCTypes |
aes-256-cbc |
One of the Cipher Block Chaining algorithm. |
Returns
Type: Promise<void>
A new Promise that resolves void once stream encryption is completed.
- See In-memory data stream encryption/decryption example.
Decrypt stream data.
Parameters
| Name | Type | Default | Description |
|---|---|---|---|
secret |
CoerceToUint8ArrayInput |
- | The secret key used to decrypt the data. |
options |
Cph.Stream.DecryptOptions |
- | An object defining required options. |
options.input |
Readable |
- | The Readable Stream where encrypted data is read. |
options.output |
Writable |
- | The Writable Stream where decrypted data is written. |
options.algorithm |
Cph.CBCTypes |
aes-256-cbc |
One of the Cipher Block Chaining algorithm. |
Returns
Type: Promise<void>
A new Promise that resolves void once stream decryption is completed.
- See In-memory data stream encryption/decryption example.
Encrypt stream data using hybrid encryption.
Warning
Please, note that when using hybrid encryption/decryption algorithms:
- an RSA keypair is required.
- if a passphrase is set for the Private Key, please make sure to use one of the Cipher Block Chaining algorithm or
chacha20-poly1305algorithm:- aes-128-cbc (
typecan bepkcs1orpkcs8) - aes-192-cbc (
typecan bepkcs1orpkcs8) - aes-256-cbc (
typecan bepkcs1orpkcs8) - chacha20-poly1305 (
typecan only bepkcs1)
- aes-128-cbc (
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
key |
crypto.KeyLike |
- | The RSA Public Key. |
options |
Cph.Stream.EncryptOptions |
- | An object defining required options. |
options.input |
Readable |
- | The Readable Stream where raw data to encrypt is read. |
options.output |
Writable |
- | The Writable Stream where encrypted data is written. |
options.algorithm |
Cph.CBCTypes |
aes-256-cbc |
One of the Cipher Block Chaining algorithm. |
Returns
Type: Promise<void>
A new Promise that resolves void once stream encryption is completed.
Decrypt stream data using hybrid decryption.
Warning
Please, note that when using hybrid encryption/decryption algorithms:
- an RSA keypair is required.
- if a passphrase is set for the Private Key, please make sure to use one of the Cipher Block Chaining algorithm or
chacha20-poly1305algorithm:- aes-128-cbc (
typecan bepkcs1orpkcs8) - aes-192-cbc (
typecan bepkcs1orpkcs8) - aes-256-cbc (
typecan bepkcs1orpkcs8) - chacha20-poly1305 (
typecan only bepkcs1)
- aes-128-cbc (
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
key |
Cph.PrivateKey |
- | The RSA Private Key. |
options |
Cph.Stream.DecryptOptions |
- | An object defining required options. |
options.input |
Readable |
- | The Readable Stream where encrypted data is read. |
options.output |
Writable |
- | The Writable Stream where decrypted data is written. |
options.algorithm |
Cph.CBCTypes |
aes-256-cbc |
One of the Cipher Block Chaining algorithm. |
Returns
Type: Promise<void>
A new Promise that resolves void once stream encryption is completed.
- See
CoerceToUint8ArrayInputfor more informations about supported input data types. - See
Cph.PrivateKeyfor accepted formats. - See In-memory data stream with hybrid encryption/decryption example.
This module supports different input data types and it uses the coerceToUint8Array utility function from @alessiofrittoli/crypto-buffer to convert it to a Uint8Array.
- See
coerceToUint8Arrayfor more informations about the supported input types.
AES Cipher Block Chaining algorithms.
All supported AES algorithms.
Common options in encryption/decryption processes.
Type parameters
| Parameter | Default | Description |
|---|---|---|
T |
Cph.AesAlgorithm |
Accepted algorithm in Cph.Options. This is usefull to constraint specifc algorithms. |
Properties
| Property | Type | Default | Description |
|---|---|---|---|
algorithm |
T |
aes-256-gcm | aes-256-cbc |
Accepted algorithms. |
salt |
number |
32 |
The salt length in bytes. Minimum: 16, Maximum: 64. |
iv |
number |
16 |
The Initialization Vector length in bytes. Minimum: 8, Maximum: 32. |
authTag |
number |
16 |
The authTag length in bytes. Minimum: 4, Maximum: 16. |
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. |
aadLength |
number |
32 |
The auto generated AAD length in bytes. Minimum: 16, Maximum: 128. |
Stream symmetric encryption options.
Properties
| Property | Type | Description |
|---|---|---|
input |
Readable |
The Readable Stream from where raw data to encrypt is read. |
output |
Writable |
The Writable Stream where encrypted data is written. |
algorithm |
Cph.CBCTypes |
One of the Cipher Block Chaining algorithm. |
Stream symmetric decryption options.
- Alias of
Cph.Stream.EncryptOptions.
Properties
| Property | Type | Description |
|---|---|---|
input |
Readable |
The Readable Stream from where encrypted data is read. |
output |
Writable |
The Writable Stream where decrypted data is written. |
algorithm |
Cph.CBCTypes |
One of the Cipher Block Chaining algorithm. |
The RSA Private Key.
It could be:
- a
crypto.KeyLike - an object defining
keyandpassphrasewherekeyis acrypto.KeyLike
The simpliest way to encrypt/decrypt in-memory data buffers.
// encrypt
const data = "my top-secret data";
const password = "my-very-strong-password";
const encrypted = Cipher.Encrypt(data, password);
// decrypt
const decrypted = Cipher.Decrypt(encrypted, password);
console.log(decrypted); // Outputs: my top-secret dataHybrid encryption offers an higher level of security since only the RSA Private Key owner will be able to decrypt the data.
Warning
Please, note that when using hybrid encryption/decryption algorithms:
- an RSA keypair is required.
- if a passphrase is set for the Private Key, please make sure to use one of the Cipher Block Chaining algorithm or
chacha20-poly1305algorithm:- aes-128-cbc (
typecan bepkcs1orpkcs8) - aes-192-cbc (
typecan bepkcs1orpkcs8) - aes-256-cbc (
typecan bepkcs1orpkcs8) - chacha20-poly1305 (
typecan only bepkcs1)
- aes-128-cbc (
import { Cipher } from "@alessiofrittoli/crypto-cipher";
const keyPair = crypto.generateKeyPairSync("rsa", {
modulusLength: 512 * 8, // 4096 bits
publicKeyEncoding: { type: "spki", format: "pem" },
privateKeyEncoding: { type: "pkcs1", format: "pem" },
});
// or you can optionally set a custom passphrase
const passphrase = "custompassphrase";
const keyPair = crypto.generateKeyPairSync("rsa", {
modulusLength: 512 * 8, // 4096 bits
publicKeyEncoding: { type: "spki", format: "pem" },
privateKeyEncoding: {
type: "pkcs1",
format: "pem",
passphrase,
cipher: Cipher.ALGORITHM.CHACHA_20_POLY,
},
});import { Cipher } from "@alessiofrittoli/crypto-cipher";
// encrypt
const data = "my top-secret data";
const encrypted = Cipher.HybridEncrypt(data, keypair.publicKey);
// decrypt
const decrypted = Cipher.HybridDecrypt(encrypted, {
key: keypair.privateKey,
passphrase,
});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.
// /api/stream-encrypt
import { Readable, Writable } from "stream";
import { Stream } from "@alessiofrittoli/stream-writer";
const routeHandler = () => {
const password = "my-very-strong-password";
const stream = new Stream();
const headers = new Headers(stream.headers);
const input = Readable.from([
Buffer.from("Chunk n.1"),
Buffer.from("Chunk n.2"),
Buffer.from("Chunk n.3"),
Buffer.from("Chunk n.4"),
]);
// `Writable` Stream where encrypted data is written
const output = new Writable({
async write(chunk, encoding, callback) {
await new Promise((resolve) => setTimeout(resolve, 2000));
await stream.write(chunk);
callback();
},
async final(callback) {
await stream.close();
callback();
},
});
Cipher.stream.Encrypt(password, { input, output }).catch(async () => {
await stream.close();
});
return (
// encrypted stream
new Response(stream.readable, { headers })
);
};// /api/stream-decrypt
import { Transform, Writable } from "stream";
import { Cipher } from "@alessiofrittoli/crypto-cipher";
import { StreamReader } from "@alessiofrittoli/stream-reader";
const password = "my-very-strong-password";
const routeHandler = () =>
fetch("/api/stream-encrypt").then((response) => {
if (!response.body) {
return new Respone(null, { status: 400 });
}
// web stream where decrypted data is written
const stream = new Stream<Buffer, string>({
transform(chunk, controller) {
controller.enqueue(chunk.toString());
},
});
const headers = new Headers(stream.headers);
headers.set("Content-Type", "text/html");
const reader = new StreamReader<Uint8Array, Buffer, false>(response.body, {
inMemory: false,
transform: Buffer.from,
});
const input = new Transform();
reader.on("data", (chunk) => input.push(chunk));
reader.on("close", () => input.end());
reader.read();
// `Writable` Stream where encrypted data is written
const output = new Writable({
async write(chunk: Buffer, encoding, callback) {
await stream.write(chunk);
callback();
},
final(callback) {
stream.close();
callback();
},
});
Cipher.stream.Decrypt(password, { input, output }).catch(async (error) => {
console.error(error);
await stream.close();
});
return new Response(stream.readable, { headers });
});Hybrid encryption offers an higher level of security since only the RSA Private Key owner will be able to decrypt the data.
Warning
Please, note that when using hybrid encryption/decryption algorithms:
- an RSA keypair is required.
- if a passphrase is set for the Private Key, please make sure to use one of the Cipher Block Chaining algorithm or
chacha20-poly1305algorithm:- aes-128-cbc (
typecan bepkcs1orpkcs8) - aes-192-cbc (
typecan bepkcs1orpkcs8) - aes-256-cbc (
typecan bepkcs1orpkcs8) - chacha20-poly1305 (
typecan only bepkcs1)
- aes-128-cbc (
import { Cipher } from "@alessiofrittoli/crypto-cipher";
const keyPair = crypto.generateKeyPairSync("rsa", {
modulusLength: 512 * 8, // 4096 bits
publicKeyEncoding: { type: "spki", format: "pem" },
privateKeyEncoding: { type: "pkcs1", format: "pem" },
});
// or you can optionally set a custom passphrase
const passphrase = "custompassphrase";
const keyPair = crypto.generateKeyPairSync("rsa", {
modulusLength: 512 * 8, // 4096 bits
publicKeyEncoding: { type: "spki", format: "pem" },
privateKeyEncoding: {
type: "pkcs1",
format: "pem",
passphrase,
cipher: Cipher.ALGORITHM.CHACHA_20_POLY,
},
});const data = "my top-secret data";
/** Store encrypted chunks for next example. */
const encryptedChunks: Buffer[] = [];
// Create a `Readable` Stream with raw data.
const input = new Readable({
read() {
this.push(data); // Push data to encrypt
this.push(null); // Signal end of stream
},
});
// Create a `Writable` Stream where encrypted data is written
const output = new Writable({
write(chunk, encoding, callback) {
// push written chunk to `encryptedChunks` for further usage.
encryptedChunks.push(chunk);
callback();
},
});
await Cipher.stream.HybridEncrypt(keyPair.publicKey, { input, output });/** Store decrypted chunks. */
const chunks: Buffer[] = [];
// Create a `Readable` Stream with encrypted data.
const input = Readable.from(encryptedChunks);
// Create a `Writable` Stream where decrypted data is written
const output = new Writable({
write(chunk, encoding, callback) {
chunks.push(chunk);
callback();
},
});
await Cipher.stream.HybridDecrypt(
{
key: keyPair.privateKey,
passphrase, // optional passhrase (required if set while generating keypair).
},
{ input, output }
);
console.log(Buffer.concat(chunks).toString()); // Outputs: 'my top-secret data'Nothig differs from the In-memory data stream encryption/decryption example, except for input and output streams which now comes directly from files reading/writing.
import fs from "fs";
const password = "my-very-strong-password";
// input where raw data to encrypt is read
const input = fs.createReadStream("my-very-large-top-secret-file.pdf");
// output where encrypted data is written
const output = fs.createWriteStream("my-very-large-top-secret-file.encrypted");
// encrypt
await Cipher.stream.Encrypt(password, { input, output });import fs from "fs";
const password = "my-very-strong-password";
// input where encrypted data is read
const input = fs.createReadStream("my-very-large-top-secret-file.encrypted");
// output where decrypted data is written
const output = fs.createWriteStream(
"my-very-large-top-secret-file-decrypted.pdf"
);
// decrypt
await Cipher.stream.Decrypt(password, { input, output });Nothig differs from the In-memory data stream with hybrid encryption/decryption example, except for input and output streams which now comes directly from files reading/writing.
Warning
Please, note that when using hybrid encryption/decryption algorithms:
- an RSA keypair is required.
- if a passphrase is set for the Private Key, please make sure to use one of the Cipher Block Chaining algorithm or
chacha20-poly1305algorithm:- aes-128-cbc (
typecan bepkcs1orpkcs8) - aes-192-cbc (
typecan bepkcs1orpkcs8) - aes-256-cbc (
typecan bepkcs1orpkcs8) - chacha20-poly1305 (
typecan only bepkcs1)
- aes-128-cbc (
import { Cipher } from "@alessiofrittoli/crypto-cipher";
const keyPair = crypto.generateKeyPairSync("rsa", {
modulusLength: 512 * 8, // 4096 bits
publicKeyEncoding: { type: "spki", format: "pem" },
privateKeyEncoding: { type: "pkcs1", format: "pem" },
});
// or you can optionally set a custom passphrase
const passphrase = "custompassphrase";
const keyPair = crypto.generateKeyPairSync("rsa", {
modulusLength: 512 * 8, // 4096 bits
publicKeyEncoding: { type: "spki", format: "pem" },
privateKeyEncoding: {
type: "pkcs1",
format: "pem",
passphrase,
cipher: Cipher.ALGORITHM.CHACHA_20_POLY,
},
});import fs from "fs";
// input where raw data to encrypt is read
const input = fs.createReadStream("my-very-large-top-secret-file.pdf");
// output where encrypted data is written
const output = fs.createWriteStream("my-very-large-top-secret-file.encrypted");
// encrypt
await Cipher.stream.HybridEncrypt(keyPair.publicKey, { input, output });import fs from "fs";
// input where encrypted data is read
const input = fs.createReadStream("my-very-large-top-secret-file.encrypted");
// output where decrypted data is written
const output = fs.createWriteStream(
"my-very-large-top-secret-file-decrypted.pdf"
);
// decrypt
const { decrypt } = await Cipher.stream.HybridDecrypt(
{
key: keyPair.privateKey,
passphrase, // optional passhrase (required if set while generating keypair).
},
{ input, output }
);npm installor using pnpm
pnpm iRun the following command to test and build code for distribution.
pnpm buildwarnings / errors check.
pnpm lintRun all the defined test suites by running the following:
# Run tests and watch file changes.
pnpm test:watch
# Run tests in a CI environment.
pnpm test:ci- See
package.jsonfile scripts for more info.
Run tests with coverage.
An HTTP server is then started to serve coverage files from ./coverage folder.
test:coverage:serveContributions are truly welcome!
Please refer to the Contributing Doc for more information on how to start contributing to this project.
Help keep this project up to date with GitHub Sponsor.
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.
|
|
|