@things-factory/shell 10.1.31 → 10.1.33
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/dist-server/index.d.ts +1 -0
- package/dist-server/index.js +1 -0
- package/dist-server/index.js.map +1 -1
- package/dist-server/reversibility/index.d.ts +1 -0
- package/dist-server/reversibility/index.js +5 -0
- package/dist-server/reversibility/index.js.map +1 -0
- package/dist-server/reversibility/reversibility.d.ts +25 -0
- package/dist-server/reversibility/reversibility.js +28 -0
- package/dist-server/reversibility/reversibility.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/dist-server/typeorm/encrypt-transform.d.ts +22 -3
- package/dist-server/typeorm/encrypt-transform.js +51 -35
- package/dist-server/typeorm/encrypt-transform.js.map +1 -1
- package/package.json +2 -2
- package/tests/encrypt-transform.test.ts +130 -0
|
@@ -1,7 +1,26 @@
|
|
|
1
1
|
import { ValueTransformer } from 'typeorm';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* Encrypting a database column, and reading one that may not be encrypted yet.
|
|
4
|
+
*
|
|
5
|
+
* ── Reading tells two failures apart ────────────────────────────────────────
|
|
6
|
+
* This used to answer `null` whenever decryption threw, which folded two very different
|
|
7
|
+
* things into one:
|
|
8
|
+
*
|
|
9
|
+
* the value was written before this column was encrypted → it is plaintext
|
|
10
|
+
* the value is ciphertext and the key cannot open it → something is wrong
|
|
11
|
+
*
|
|
12
|
+
* Both came back as "there is no value". A deployment with the wrong `dataEncryptionKey`
|
|
13
|
+
* therefore looked like a system holding no credentials at all — the rows were intact, one
|
|
14
|
+
* line went to the console, and nothing said which row or how many. Devices kept
|
|
15
|
+
* authenticating, because a signed token never consults these columns, so only the screens
|
|
16
|
+
* lost the values.
|
|
17
|
+
*
|
|
18
|
+
* So: a value that is not ciphertext-shaped is returned as it is, which is what lets a column
|
|
19
|
+
* be encrypted without a flag day. A value that IS ciphertext-shaped and will not open throws,
|
|
20
|
+
* because that is a broken key and not an absent value.
|
|
6
21
|
*/
|
|
7
22
|
export declare const encryptTransformer: ValueTransformer;
|
|
23
|
+
/** Raised when a value is ciphertext and the configured key does not open it. */
|
|
24
|
+
export declare class DecryptionFailed extends Error {
|
|
25
|
+
constructor(cause: unknown);
|
|
26
|
+
}
|
|
@@ -1,36 +1,52 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.encryptTransformer = void 0;
|
|
3
|
+
exports.DecryptionFailed = exports.encryptTransformer = void 0;
|
|
4
4
|
const crypto_1 = require("crypto");
|
|
5
5
|
const get_data_encryption_key_js_1 = require("./get-data-encryption-key.js");
|
|
6
6
|
const ALGORITHM = 'aes-256-cbc';
|
|
7
|
-
const KEY_LENGTH = 32;
|
|
8
|
-
const SALT_LENGTH = 16;
|
|
9
7
|
const IV_LENGTH = 16;
|
|
10
8
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* What this writes: `<iv as hex>:<ciphertext as hex>`. The IV is 16 bytes, so the prefix is
|
|
10
|
+
* exactly 32 hex characters, and the body is hex of even length.
|
|
11
|
+
*
|
|
12
|
+
* Reading matches on that shape rather than trying and seeing what happens, because the two
|
|
13
|
+
* failures below have to stay apart.
|
|
14
14
|
*/
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Encrypts the entity field value before storing it in the database.
|
|
18
|
-
* @param {string} entityValue - Unencrypted entity field value
|
|
19
|
-
* @returns {string} - Encrypted string
|
|
20
|
-
*/
|
|
21
|
-
to: (entityValue) => encrypt(entityValue), // DB에 저장하기 전에 암호화
|
|
22
|
-
/**
|
|
23
|
-
* Decrypts the encrypted database value when retrieving it from the database.
|
|
24
|
-
* @param {string} databaseValue - Encrypted database field value
|
|
25
|
-
* @returns {string} - Decrypted string
|
|
26
|
-
*/
|
|
27
|
-
from: (databaseValue) => decrypt(databaseValue) // DB에서 값을 가져올 때 복호화
|
|
28
|
-
};
|
|
15
|
+
const CIPHERTEXT = /^[0-9a-f]{32}:(?:[0-9a-f]{2})+$/;
|
|
29
16
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
17
|
+
* Encrypting a database column, and reading one that may not be encrypted yet.
|
|
18
|
+
*
|
|
19
|
+
* ── Reading tells two failures apart ────────────────────────────────────────
|
|
20
|
+
* This used to answer `null` whenever decryption threw, which folded two very different
|
|
21
|
+
* things into one:
|
|
22
|
+
*
|
|
23
|
+
* the value was written before this column was encrypted → it is plaintext
|
|
24
|
+
* the value is ciphertext and the key cannot open it → something is wrong
|
|
25
|
+
*
|
|
26
|
+
* Both came back as "there is no value". A deployment with the wrong `dataEncryptionKey`
|
|
27
|
+
* therefore looked like a system holding no credentials at all — the rows were intact, one
|
|
28
|
+
* line went to the console, and nothing said which row or how many. Devices kept
|
|
29
|
+
* authenticating, because a signed token never consults these columns, so only the screens
|
|
30
|
+
* lost the values.
|
|
31
|
+
*
|
|
32
|
+
* So: a value that is not ciphertext-shaped is returned as it is, which is what lets a column
|
|
33
|
+
* be encrypted without a flag day. A value that IS ciphertext-shaped and will not open throws,
|
|
34
|
+
* because that is a broken key and not an absent value.
|
|
33
35
|
*/
|
|
36
|
+
exports.encryptTransformer = {
|
|
37
|
+
to: (entityValue) => encrypt(entityValue),
|
|
38
|
+
from: (databaseValue) => decrypt(databaseValue)
|
|
39
|
+
};
|
|
40
|
+
/** Raised when a value is ciphertext and the configured key does not open it. */
|
|
41
|
+
class DecryptionFailed extends Error {
|
|
42
|
+
constructor(cause) {
|
|
43
|
+
super('an encrypted column could not be decrypted with the configured dataEncryptionKey. ' +
|
|
44
|
+
'The value is still in the database — this is a key problem, not a missing value.');
|
|
45
|
+
this.name = 'DecryptionFailed';
|
|
46
|
+
this.cause = cause;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
exports.DecryptionFailed = DecryptionFailed;
|
|
34
50
|
function encrypt(text) {
|
|
35
51
|
if (!text) {
|
|
36
52
|
return null;
|
|
@@ -40,25 +56,25 @@ function encrypt(text) {
|
|
|
40
56
|
const encrypted = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]);
|
|
41
57
|
return `${iv.toString('hex')}:${encrypted.toString('hex')}`;
|
|
42
58
|
}
|
|
43
|
-
/**
|
|
44
|
-
* Decrypts the given encrypted text.
|
|
45
|
-
* @param {string} text - Encrypted text to decrypt
|
|
46
|
-
* @returns {string} - Decrypted string
|
|
47
|
-
*/
|
|
48
59
|
function decrypt(text) {
|
|
49
60
|
if (!text) {
|
|
50
61
|
return null;
|
|
51
62
|
}
|
|
63
|
+
/*
|
|
64
|
+
* Written before this column was encrypted. Handing it back unchanged is the whole of the
|
|
65
|
+
* backward compatibility: a column can start encrypting while the rows already in it stay
|
|
66
|
+
* readable, and each row converts the next time something writes it.
|
|
67
|
+
*/
|
|
68
|
+
if (!CIPHERTEXT.test(text)) {
|
|
69
|
+
return text;
|
|
70
|
+
}
|
|
52
71
|
try {
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
const decipher = (0, crypto_1.createDecipheriv)(ALGORITHM, get_data_encryption_key_js_1.dataEncryptionKey, iv);
|
|
57
|
-
return Buffer.concat([decipher.update(encryptedText), decipher.final()]).toString('utf8');
|
|
72
|
+
const [ivHex, bodyHex] = text.split(':');
|
|
73
|
+
const decipher = (0, crypto_1.createDecipheriv)(ALGORITHM, get_data_encryption_key_js_1.dataEncryptionKey, Buffer.from(ivHex, 'hex'));
|
|
74
|
+
return Buffer.concat([decipher.update(Buffer.from(bodyHex, 'hex')), decipher.final()]).toString('utf8');
|
|
58
75
|
}
|
|
59
76
|
catch (err) {
|
|
60
|
-
|
|
61
|
-
return null;
|
|
77
|
+
throw new DecryptionFailed(err);
|
|
62
78
|
}
|
|
63
79
|
}
|
|
64
80
|
//# sourceMappingURL=encrypt-transform.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"encrypt-transform.js","sourceRoot":"","sources":["../../server/typeorm/encrypt-transform.ts"],"names":[],"mappings":";;;AACA,
|
|
1
|
+
{"version":3,"file":"encrypt-transform.js","sourceRoot":"","sources":["../../server/typeorm/encrypt-transform.ts"],"names":[],"mappings":";;;AACA,mCAAsE;AAEtE,6EAAgE;AAEhE,MAAM,SAAS,GAAG,aAAa,CAAA;AAC/B,MAAM,SAAS,GAAG,EAAE,CAAA;AAEpB;;;;;;GAMG;AACH,MAAM,UAAU,GAAG,iCAAiC,CAAA;AAEpD;;;;;;;;;;;;;;;;;;;GAmBG;AACU,QAAA,kBAAkB,GAAqB;IAClD,EAAE,EAAE,CAAC,WAAmB,EAAE,EAAE,CAAC,OAAO,CAAC,WAAW,CAAC;IACjD,IAAI,EAAE,CAAC,aAAqB,EAAE,EAAE,CAAC,OAAO,CAAC,aAAa,CAAC;CACxD,CAAA;AAED,iFAAiF;AACjF,MAAa,gBAAiB,SAAQ,KAAK;IACzC,YAAY,KAAc;QACxB,KAAK,CACH,oFAAoF;YAClF,kFAAkF,CACrF,CAAA;QAED,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAC7B;QAAC,IAAY,CAAC,KAAK,GAAG,KAAK,CAAA;IAC9B,CAAC;CACF;AAVD,4CAUC;AAED,SAAS,OAAO,CAAC,IAAY;IAC3B,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,IAAI,CAAA;IACb,CAAC;IAED,MAAM,EAAE,GAAG,IAAA,oBAAW,EAAC,SAAS,CAAC,CAAA;IACjC,MAAM,MAAM,GAAG,IAAA,uBAAc,EAAC,SAAS,EAAE,8CAAiB,EAAE,EAAE,CAAC,CAAA;IAC/D,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAA;IAE9E,OAAO,GAAG,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAA;AAC7D,CAAC;AAED,SAAS,OAAO,CAAC,IAAY;IAC3B,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,IAAI,CAAA;IACb,CAAC;IAED;;;;OAIG;IACH,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAC3B,OAAO,IAAI,CAAA;IACb,CAAC;IAED,IAAI,CAAC;QACH,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;QACxC,MAAM,QAAQ,GAAG,IAAA,yBAAgB,EAAC,SAAS,EAAE,8CAAiB,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAA;QAE1F,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAA;IACzG,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,gBAAgB,CAAC,GAAG,CAAC,CAAA;IACjC,CAAC;AACH,CAAC","sourcesContent":["import { ValueTransformer } from 'typeorm'\nimport { createCipheriv, createDecipheriv, randomBytes } from 'crypto'\n\nimport { dataEncryptionKey } from './get-data-encryption-key.js'\n\nconst ALGORITHM = 'aes-256-cbc'\nconst IV_LENGTH = 16\n\n/**\n * What this writes: `<iv as hex>:<ciphertext as hex>`. The IV is 16 bytes, so the prefix is\n * exactly 32 hex characters, and the body is hex of even length.\n *\n * Reading matches on that shape rather than trying and seeing what happens, because the two\n * failures below have to stay apart.\n */\nconst CIPHERTEXT = /^[0-9a-f]{32}:(?:[0-9a-f]{2})+$/\n\n/**\n * Encrypting a database column, and reading one that may not be encrypted yet.\n *\n * ── Reading tells two failures apart ────────────────────────────────────────\n * This used to answer `null` whenever decryption threw, which folded two very different\n * things into one:\n *\n * the value was written before this column was encrypted → it is plaintext\n * the value is ciphertext and the key cannot open it → something is wrong\n *\n * Both came back as \"there is no value\". A deployment with the wrong `dataEncryptionKey`\n * therefore looked like a system holding no credentials at all — the rows were intact, one\n * line went to the console, and nothing said which row or how many. Devices kept\n * authenticating, because a signed token never consults these columns, so only the screens\n * lost the values.\n *\n * So: a value that is not ciphertext-shaped is returned as it is, which is what lets a column\n * be encrypted without a flag day. A value that IS ciphertext-shaped and will not open throws,\n * because that is a broken key and not an absent value.\n */\nexport const encryptTransformer: ValueTransformer = {\n to: (entityValue: string) => encrypt(entityValue),\n from: (databaseValue: string) => decrypt(databaseValue)\n}\n\n/** Raised when a value is ciphertext and the configured key does not open it. */\nexport class DecryptionFailed extends Error {\n constructor(cause: unknown) {\n super(\n 'an encrypted column could not be decrypted with the configured dataEncryptionKey. ' +\n 'The value is still in the database — this is a key problem, not a missing value.'\n )\n\n this.name = 'DecryptionFailed'\n ;(this as any).cause = cause\n }\n}\n\nfunction encrypt(text: string): string {\n if (!text) {\n return null\n }\n\n const iv = randomBytes(IV_LENGTH)\n const cipher = createCipheriv(ALGORITHM, dataEncryptionKey, iv)\n const encrypted = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()])\n\n return `${iv.toString('hex')}:${encrypted.toString('hex')}`\n}\n\nfunction decrypt(text: string): string {\n if (!text) {\n return null\n }\n\n /*\n * Written before this column was encrypted. Handing it back unchanged is the whole of the\n * backward compatibility: a column can start encrypting while the rows already in it stay\n * readable, and each row converts the next time something writes it.\n */\n if (!CIPHERTEXT.test(text)) {\n return text\n }\n\n try {\n const [ivHex, bodyHex] = text.split(':')\n const decipher = createDecipheriv(ALGORITHM, dataEncryptionKey, Buffer.from(ivHex, 'hex'))\n\n return Buffer.concat([decipher.update(Buffer.from(bodyHex, 'hex')), decipher.final()]).toString('utf8')\n } catch (err) {\n throw new DecryptionFailed(err)\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/shell",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.33",
|
|
4
4
|
"description": "Core module for framework",
|
|
5
5
|
"bin": {
|
|
6
6
|
"things-factory": "bin/things-factory",
|
|
@@ -126,5 +126,5 @@
|
|
|
126
126
|
"pg": "^8.7.3",
|
|
127
127
|
"sqlite3": "^5.0.8"
|
|
128
128
|
},
|
|
129
|
-
"gitHead": "
|
|
129
|
+
"gitHead": "7fb7b6587846b5ff6ee7a4983652d9561b4d6b2c"
|
|
130
130
|
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { encryptTransformer, DecryptionFailed } from '../server/typeorm/encrypt-transform'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reading an encrypted column has to tell two failures apart.
|
|
5
|
+
*
|
|
6
|
+
* The transformer used to answer `null` whenever decryption threw. That folded together a value
|
|
7
|
+
* written before the column was encrypted (plaintext, perfectly fine) and a value that is
|
|
8
|
+
* ciphertext the key will not open (something is wrong). Both read as "there is no value", so a
|
|
9
|
+
* deployment with the wrong `dataEncryptionKey` looked like a system holding no credentials —
|
|
10
|
+
* rows intact, one line on the console, no row named and no count.
|
|
11
|
+
*
|
|
12
|
+
* It also blocked turning encryption on at all: every row written before the change would have
|
|
13
|
+
* gone blank on the next read.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** The shape `to` writes: 32 hex characters of IV, a colon, then hex of even length. */
|
|
17
|
+
const CIPHERTEXT = /^[0-9a-f]{32}:(?:[0-9a-f]{2})+$/
|
|
18
|
+
|
|
19
|
+
describe('writing an encrypted column', () => {
|
|
20
|
+
it('produces the iv:body shape', () => {
|
|
21
|
+
const stored = encryptTransformer.to('client-secret-value')
|
|
22
|
+
|
|
23
|
+
expect(stored).toMatch(CIPHERTEXT)
|
|
24
|
+
expect(stored).not.toContain('client-secret-value')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('keeps nothing for an empty value', () => {
|
|
28
|
+
expect(encryptTransformer.to('')).toBeNull()
|
|
29
|
+
expect(encryptTransformer.to(null as any)).toBeNull()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('writes a different string each time, so equal values do not look equal at rest', () => {
|
|
33
|
+
/* The IV is random per write. Without that, two rows holding the same secret would match. */
|
|
34
|
+
expect(encryptTransformer.to('same')).not.toEqual(encryptTransformer.to('same'))
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe('reading an encrypted column', () => {
|
|
39
|
+
it('round-trips what it wrote', () => {
|
|
40
|
+
expect(encryptTransformer.from(encryptTransformer.to('client-secret-value'))).toEqual('client-secret-value')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('hands back a value written before the column was encrypted', () => {
|
|
44
|
+
/*
|
|
45
|
+
* ⚠ The case that makes turning encryption on possible at all. Every row already in the
|
|
46
|
+
* table is plaintext; if reading them answered null they would go blank on deploy.
|
|
47
|
+
*/
|
|
48
|
+
expect(encryptTransformer.from('plain-client-secret')).toEqual('plain-client-secret')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('hands back a JWT, which is what these columns actually hold', () => {
|
|
52
|
+
/*
|
|
53
|
+
* `Appliance.accessToken` and the shadow user's password hold signed JWTs. A JWT is dot
|
|
54
|
+
* separated and carries no colon, so it can never be mistaken for the ciphertext shape.
|
|
55
|
+
*/
|
|
56
|
+
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEyMyIsInVzZXJUeXBlIjoiYXBwbGlhbmNlIn0.c2lnbmF0dXJl'
|
|
57
|
+
|
|
58
|
+
expect(encryptTransformer.from(token)).toEqual(token)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('throws when the value is ciphertext and the key does not open it', () => {
|
|
62
|
+
/*
|
|
63
|
+
* ⚠ The case this file exists for. Answering null here is what made a wrong key look like an
|
|
64
|
+
* empty system. The row is still there; what is broken is the key.
|
|
65
|
+
*/
|
|
66
|
+
const iv = 'a'.repeat(32)
|
|
67
|
+
const body = 'b'.repeat(64)
|
|
68
|
+
|
|
69
|
+
expect(() => encryptTransformer.from(`${iv}:${body}`)).toThrow(DecryptionFailed)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('says in the message that the value still exists', () => {
|
|
73
|
+
/* Whoever reads the log has to know not to go re-issue credentials. */
|
|
74
|
+
const iv = 'a'.repeat(32)
|
|
75
|
+
|
|
76
|
+
expect(() => encryptTransformer.from(`${iv}:${'b'.repeat(64)}`)).toThrow(/still in the database/)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('keeps nothing for an empty value', () => {
|
|
80
|
+
expect(encryptTransformer.from('')).toBeNull()
|
|
81
|
+
expect(encryptTransformer.from(null as any)).toBeNull()
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
describe('telling the two shapes apart', () => {
|
|
86
|
+
/*
|
|
87
|
+
* The split has to be exact in both directions: a plaintext value read as ciphertext throws on
|
|
88
|
+
* a perfectly good row, and a ciphertext value read as plaintext hands out the stored bytes.
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
it('does not take a colon alone for ciphertext', () => {
|
|
92
|
+
expect(encryptTransformer.from('host:5432')).toEqual('host:5432')
|
|
93
|
+
expect(encryptTransformer.from('Basic dXNlcjpwYXNz')).toEqual('Basic dXNlcjpwYXNz')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('does not take hex of the wrong length for ciphertext', () => {
|
|
97
|
+
/* The IV is 16 bytes, so 32 hex characters and no other length. */
|
|
98
|
+
expect(encryptTransformer.from(`${'a'.repeat(31)}:${'b'.repeat(64)}`)).toEqual(
|
|
99
|
+
`${'a'.repeat(31)}:${'b'.repeat(64)}`
|
|
100
|
+
)
|
|
101
|
+
expect(encryptTransformer.from(`${'a'.repeat(33)}:${'b'.repeat(64)}`)).toEqual(
|
|
102
|
+
`${'a'.repeat(33)}:${'b'.repeat(64)}`
|
|
103
|
+
)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('does not take an odd-length body for ciphertext', () => {
|
|
107
|
+
const odd = `${'a'.repeat(32)}:${'b'.repeat(63)}`
|
|
108
|
+
|
|
109
|
+
expect(encryptTransformer.from(odd)).toEqual(odd)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('does not take uppercase hex for ciphertext', () => {
|
|
113
|
+
/* `to` writes lowercase. Something else wrote this, so it is not ours to decrypt. */
|
|
114
|
+
const upper = `${'A'.repeat(32)}:${'B'.repeat(64)}`
|
|
115
|
+
|
|
116
|
+
expect(encryptTransformer.from(upper)).toEqual(upper)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('recognises its own output as ciphertext', () => {
|
|
120
|
+
/*
|
|
121
|
+
* The other direction of the same split, and the one that would silently hand out stored
|
|
122
|
+
* bytes if the pattern ever stopped matching what `to` writes.
|
|
123
|
+
*/
|
|
124
|
+
const stored = encryptTransformer.to('secret')
|
|
125
|
+
|
|
126
|
+
expect(stored).toMatch(CIPHERTEXT)
|
|
127
|
+
expect(encryptTransformer.from(stored)).toEqual('secret')
|
|
128
|
+
expect(encryptTransformer.from(stored)).not.toEqual(stored)
|
|
129
|
+
})
|
|
130
|
+
})
|