jstink 1.1.1 → 1.1.2
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 +16 -3
- package/package.json +1 -1
- package/src/aes_gcm.js +6 -2
- package/src/index.js +25 -0
- package/tests/index.test.js +18 -5
package/README.md
CHANGED
|
@@ -10,7 +10,9 @@ Unfortunately for Javascript developers, [there is no production-ready Javascrip
|
|
|
10
10
|
|
|
11
11
|
jstink was built to fill this gap. It provides a very simple API that is hard to misuse, and is compatible with Tink ciphertexts and with [Tinkey](https://developers.google.com/tink/tinkey-overview) encrypted keysets.
|
|
12
12
|
|
|
13
|
-
jstink currently only supports
|
|
13
|
+
jstink currently only supports 256 bit AES encryption in GCM mode. AES (Advanced Encryption Standard) is recommended by NIST for federal use in the encryption of classified and unclassified data. GCM (Galois/Counter Mode) is also recommended by NIST and provides authentication as well as encryption - that is it ensures the integrity and authenticity of encrypted data in addition to ensuring confidentiality. GCM also accepts associated data which is authenticated but not encrypted.
|
|
14
|
+
|
|
15
|
+
jstink currently only supports Tink keysets encrypted with AWS KMS keys.
|
|
14
16
|
|
|
15
17
|
## Getting Started
|
|
16
18
|
|
|
@@ -67,7 +69,6 @@ Once you've deployed this keyset, you can make it the default for encryption:
|
|
|
67
69
|
```
|
|
68
70
|
tinkey promote-key \
|
|
69
71
|
--key-id <new-key-id> \
|
|
70
|
-
--key-template AES256_GCM \
|
|
71
72
|
--in keysetv2.json \
|
|
72
73
|
--out keysetv3.json \
|
|
73
74
|
--master-key-uri aws-kms://${MASTER_KEY_ARN}
|
|
@@ -77,8 +78,20 @@ Decrypt operations will still use the key that was used for encryption (the encr
|
|
|
77
78
|
|
|
78
79
|
```
|
|
79
80
|
tinkey delete-key \
|
|
80
|
-
--key-
|
|
81
|
+
--key-id <key-id> \
|
|
81
82
|
--in keysetv3.json \
|
|
82
83
|
--out keysetv4.json \
|
|
83
84
|
--master-key-uri aws-kms://${MASTER_KEY_ARN}
|
|
84
85
|
```
|
|
86
|
+
|
|
87
|
+
## Using Authenticated Associated Data (AAD)
|
|
88
|
+
|
|
89
|
+
The GCM algorithm used by jstink is an Authenticated Encryption with Associated Data (AEAD) encryption method. This means that it provides for authentication as well as confidentiality, and also allows the message to contain "associated data". This associated data is authenticated but not encrypted. A decryption operation will only work if the same associated data is used for decryption that was used for encryption.
|
|
90
|
+
|
|
91
|
+
One use of AAD is to bind a ciphertext to it's encryption context. For example, we can use a username as the associated data for data that is encrypted for a specific user:
|
|
92
|
+
|
|
93
|
+
```javascript
|
|
94
|
+
const ciphertext = await aead.encrypt(userData, username);
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
This can prevent attacks where encrypted data is 'cut-and-pasted' from one user to another.
|
package/package.json
CHANGED
package/src/aes_gcm.js
CHANGED
|
@@ -6,7 +6,9 @@ function encrypt(plaintext, associatedData, key) {
|
|
|
6
6
|
const iv = crypto.randomBytes(12);
|
|
7
7
|
const cipher = crypto.createCipheriv(algorithm, key, iv);
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
if (associatedData !== undefined) {
|
|
10
|
+
cipher.setAAD(Buffer.from(associatedData));
|
|
11
|
+
}
|
|
10
12
|
const encrypted = cipher.update(plaintext);
|
|
11
13
|
const remaining = cipher.final();
|
|
12
14
|
const tag = cipher.getAuthTag();
|
|
@@ -20,7 +22,9 @@ function encrypt(plaintext, associatedData, key) {
|
|
|
20
22
|
|
|
21
23
|
function decrypt(ciphertext, associatedData, key, iv, tag) {
|
|
22
24
|
const decipher = crypto.createDecipheriv(algorithm, key, iv);
|
|
23
|
-
|
|
25
|
+
if (associatedData !== undefined) {
|
|
26
|
+
decipher.setAAD(Buffer.from(associatedData));
|
|
27
|
+
}
|
|
24
28
|
decipher.setAuthTag(Buffer.from(tag));
|
|
25
29
|
let decrypted = decipher.update(ciphertext);
|
|
26
30
|
decrypted += decipher.final();
|
package/src/index.js
CHANGED
|
@@ -10,8 +10,16 @@ async function decryptKeyset(encryptedKeyset) {
|
|
|
10
10
|
return keysets.create(keysetProtobuf);
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Authenticated Encryption with Associated Data (AEAD).
|
|
15
|
+
*
|
|
16
|
+
*/
|
|
13
17
|
class Aead {
|
|
14
18
|
|
|
19
|
+
/**
|
|
20
|
+
*
|
|
21
|
+
* @param {object} keyset The AWK KMS encrypted Tink keyset
|
|
22
|
+
*/
|
|
15
23
|
constructor(keyset) {
|
|
16
24
|
if (!keyset.encryptedKeyset) {
|
|
17
25
|
throw new Error('keyset must contain an encryptedKeyset property');
|
|
@@ -25,6 +33,15 @@ class Aead {
|
|
|
25
33
|
}
|
|
26
34
|
}
|
|
27
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Encrypts the plaintext with the associated data as associated authenticated data.
|
|
38
|
+
*
|
|
39
|
+
* @param {string|Buffer} plaintext - the plaintext to be encrypted.
|
|
40
|
+
* @param {string|Buffer} associatedData - the associated data to be authenticated but not
|
|
41
|
+
* encrypted. For successful decryption the same
|
|
42
|
+
* associatedData must be provided.
|
|
43
|
+
* @returns {Promise<Buffer>} - the ciphertext.
|
|
44
|
+
*/
|
|
28
45
|
encrypt = async (plaintext, associatedData) => {
|
|
29
46
|
await this.init();
|
|
30
47
|
const key = this.decryptedKeys[this.keyset.keysetInfo.primaryKeyId];
|
|
@@ -37,6 +54,14 @@ class Aead {
|
|
|
37
54
|
});
|
|
38
55
|
}
|
|
39
56
|
|
|
57
|
+
/**
|
|
58
|
+
*
|
|
59
|
+
* @param {Buffer} encrypted - the ciphertext to be decrypted.
|
|
60
|
+
* @param {string|Buffer} associatedData - the associated data to be authenticated.
|
|
61
|
+
* For successful decryption must be the same
|
|
62
|
+
* as used during encryption.
|
|
63
|
+
* @returns {Promise<Buffer>} - the plaintext.
|
|
64
|
+
*/
|
|
40
65
|
decrypt = async (encrypted, associatedData) => {
|
|
41
66
|
await this.init();
|
|
42
67
|
const {keyId, ciphertext, tag, iv} = ciphertexts.parse(encrypted);
|
package/tests/index.test.js
CHANGED
|
@@ -24,18 +24,31 @@ jest.mock('@aws-sdk/client-kms', () => {
|
|
|
24
24
|
});
|
|
25
25
|
|
|
26
26
|
describe('jstink', () => {
|
|
27
|
-
it('should encrypt and decrypt', async () => {
|
|
28
|
-
const keyset = JSON.parse(fs.readFileSync('tests/keyset.json'));
|
|
29
|
-
const aead = new Aead(keyset);
|
|
30
27
|
|
|
28
|
+
const keyset = JSON.parse(fs.readFileSync('tests/keyset.json'));
|
|
29
|
+
const aead = new Aead(keyset);
|
|
30
|
+
|
|
31
|
+
it('should encrypt and decrypt', async () => {
|
|
31
32
|
const ciphertext = await aead.encrypt('Hello World!', 'customer-id1');
|
|
32
33
|
const plaintext = await aead.decrypt(ciphertext, 'customer-id1');
|
|
33
34
|
expect(plaintext).toBe('Hello World!');
|
|
34
35
|
});
|
|
35
36
|
|
|
37
|
+
it('should encrypt and decrypt without associated data', async () => {
|
|
38
|
+
const ciphertext = await aead.encrypt('Hello World!');
|
|
39
|
+
const plaintext = await aead.decrypt(ciphertext);
|
|
40
|
+
|
|
41
|
+
expect(plaintext).toBe('Hello World!');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should authenticate associated data', async () => {
|
|
45
|
+
const ciphertext = await aead.encrypt('Hello World!', 'foo');
|
|
46
|
+
await expect(aead.decrypt(ciphertext, 'bar')).rejects.toThrow(
|
|
47
|
+
'Unsupported state or unable to authenticate data'
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
36
51
|
it('should decrypt', async () => {
|
|
37
|
-
const keyset = JSON.parse(fs.readFileSync('tests/keyset.json'));
|
|
38
|
-
const aead = new Aead(keyset);
|
|
39
52
|
const ciphertext = fs.readFileSync('tests/encrypted.dat');
|
|
40
53
|
const plaintext = await aead.decrypt(ciphertext, 'customer-id1');
|
|
41
54
|
expect(plaintext).toBe('Hello World!');
|