Skip to content

Commit f31fa5d

Browse files
authored
Merge pull request #79 from bcgov/feature/multi-bucket
Implement Encrypt/Decrypt component support
2 parents 0820b28 + 387d8cb commit f31fa5d

File tree

6 files changed

+187
-9
lines changed

6 files changed

+187
-9
lines changed

app/config/custom-environment-variables.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"bodyLimit": "SERVER_BODYLIMIT",
3434
"logFile": "SERVER_LOGFILE",
3535
"logLevel": "SERVER_LOGLEVEL",
36+
"passphrase": "SERVER_PASSPHRASE",
3637
"port": "SERVER_PORT"
3738
}
3839
}

app/src/components/crypt.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
const config = require('config');
2+
const crypto = require('crypto');
3+
4+
// GCM mode is good for situations with random access and authenticity requirements
5+
// CBC mode is older, but is sufficiently secure with high performance for short payloads
6+
const algorithm = 'aes-256-cbc';
7+
const encoding = 'base64';
8+
const hashAlgorithm = 'sha256';
9+
10+
const crypt = {
11+
/**
12+
* @function encrypt
13+
* Yields an encrypted string containing the iv and ciphertext, separated by a colon.
14+
* If no key is provided, ciphertext will be the plaintext in base64 encoding.
15+
* @param {string} plaintext The input string contents
16+
* @returns {string} The encrypted base64 formatted string in the format `iv:ciphertext`.
17+
*/
18+
encrypt(plaintext) {
19+
const passphrase = config.has('server.passphrase') ? config.get('server.passphrase') : undefined;
20+
const iv = crypto.randomBytes(16);
21+
let content = Buffer.from(plaintext);
22+
23+
if (passphrase && passphrase.length) {
24+
const hash = crypto.createHash(hashAlgorithm);
25+
// AES-256 key length must be exactly 32 bytes
26+
const key = hash.update(passphrase).digest().subarray(0, 32);
27+
const cipher = crypto.createCipheriv(algorithm, key, iv);
28+
content = Buffer.concat([cipher.update(plaintext), cipher.final()]);
29+
}
30+
return `${iv.toString(encoding)}:${content.toString(encoding)}`;
31+
},
32+
33+
/**
34+
* @function decrypt
35+
* Yields the plaintext by accepting an encrypted string containing the iv and
36+
* ciphertext, separated by a colon. If no key is provided, the plaintext will be
37+
* the ciphertext.
38+
* @param {string} ciphertext The input encrypted string contents
39+
* @returns {string} The decrypted plaintext string, usually in utf-8
40+
*/
41+
decrypt(ciphertext) {
42+
const passphrase = config.has('server.passphrase') ? config.get('server.passphrase') : undefined;
43+
const [iv, encrypted] = ciphertext.split(':').map(p => Buffer.from(p, encoding));
44+
let content = encrypted;
45+
46+
if (passphrase && passphrase.length) {
47+
const hash = crypto.createHash(hashAlgorithm);
48+
// AES-256 key length must be exactly 32 bytes
49+
const key = hash.update(passphrase).digest().subarray(0, 32);
50+
const decipher = crypto.createDecipheriv(algorithm, key, iv);
51+
content = Buffer.concat([decipher.update(encrypted), decipher.final()]);
52+
}
53+
return Buffer.from(content, encoding).toString();
54+
}
55+
};
56+
57+
module.exports = crypt;
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
const config = require('config');
2+
const crypt = require('../../../src/components/crypt');
3+
4+
// Mock config library - @see {@link https://stackoverflow.com/a/64819698}
5+
jest.mock('config');
6+
7+
beforeEach(() => {
8+
jest.resetAllMocks();
9+
});
10+
11+
afterAll(() => {
12+
jest.restoreAllMocks();
13+
});
14+
15+
// General testing constants
16+
const passphrase = 'passphrase';
17+
18+
describe('encrypt', () => {
19+
describe('without a passphrase', () => {
20+
beforeEach(() => {
21+
config.has.mockReturnValueOnce(false); // server.passphrase
22+
});
23+
24+
it.each([
25+
[''],
26+
['foobar'],
27+
['1bazbam']
28+
])('should return a string given %j', (plaintext) => {
29+
const result = crypt.encrypt(plaintext);
30+
expect(result).toBeTruthy();
31+
expect(typeof result).toEqual('string');
32+
expect(result).toContain(':');
33+
34+
const [iv, encrypted] = result.split(':');
35+
expect(iv).toEqual(expect.any(String));
36+
expect(encrypted).toEqual(expect.any(String));
37+
});
38+
});
39+
40+
describe('with a passphrase', () => {
41+
beforeEach(() => {
42+
config.has.mockReturnValueOnce(true); // server.passphrase
43+
config.get.mockReturnValueOnce(passphrase); // server.passphrase
44+
});
45+
46+
it.each([
47+
[''],
48+
['foobar'],
49+
['1bazbam']
50+
])('should return a string given %j', (plaintext) => {
51+
const result = crypt.encrypt(plaintext);
52+
expect(result).toBeTruthy();
53+
expect(typeof result).toEqual('string');
54+
expect(result).toContain(':');
55+
56+
const [iv, encrypted] = result.split(':');
57+
expect(iv).toEqual(expect.any(String));
58+
expect(encrypted).toEqual(expect.any(String));
59+
});
60+
});
61+
});
62+
63+
describe('decrypt', () => {
64+
describe('without a passphrase', () => {
65+
beforeEach(() => {
66+
config.has.mockReturnValueOnce(false); // server.passphrase
67+
});
68+
69+
it.each([
70+
['qYSUumi8L5ypY920HES5sQ==:', ''],
71+
['JWzCHAxVecObsBLSMG/Cdg==:Zm9vYmFy', 'foobar'],
72+
['xCEUr5OoZOnOj87XGRU74Q==:MWJhemJhbQ==', '1bazbam']
73+
])('should return a string given %j', (ciphertext, plaintext) => {
74+
const result = crypt.decrypt(ciphertext);
75+
expect(typeof result).toEqual('string');
76+
expect(result).toMatch(plaintext);
77+
});
78+
});
79+
80+
describe('with a passphrase', () => {
81+
beforeEach(() => {
82+
config.has.mockReturnValueOnce(true); // server.passphrase
83+
config.get.mockReturnValueOnce(passphrase); // server.passphrase
84+
});
85+
86+
it.each([
87+
['ZOWviyZrzhIhjSo38rtpag==:4Lq5+MmjDjK5JE7J1osSJA==', ''],
88+
['V8nPQy/0b+Rj2NefzaY+rg==:9p1TC9i4jFZX7OXcUgUSrA==', 'foobar'],
89+
['WnF471CEo8U4BVcHXAe2bA==:2iER6Is+gtPPCVCoAYzkOw==', '1bazbam']
90+
])('should return a string given %j', (ciphertext, plaintext) => {
91+
const result = crypt.decrypt(ciphertext);
92+
expect(typeof result).toEqual('string');
93+
expect(result).toMatch(plaintext);
94+
});
95+
});
96+
});

charts/coms/Chart.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name: common-object-management-service
33
# This is the chart version. This version number should be incremented each time you make changes
44
# to the chart and its templates, including the app version.
55
# Versions are expected to follow Semantic Versioning (https://semver.org/)
6-
version: 0.0.5
6+
version: 0.0.6
77
kubeVersion: ">= 1.13.0"
88
description: A microservice for managing access control to S3 Objects
99
# A chart can be either an 'application' or a 'library' chart.

charts/coms/templates/deploymentconfig.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,11 @@ spec:
151151
secretKeyRef:
152152
key: password
153153
name: {{ include "coms.configname" . }}-objectstorage
154+
- name: SERVER_PASSPHRASE
155+
valueFrom:
156+
secretKeyRef:
157+
key: password
158+
name: {{ include "coms.fullname" . }}-passphrase
154159
envFrom:
155160
- configMapRef:
156161
name: {{ include "coms.configname" . }}-config

charts/coms/templates/secret.yaml

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,38 @@
1-
{{- $password := (randAlphaNum 32) | b64enc }}
2-
{{- $username := (randAlphaNum 32) | b64enc }}
1+
{{- $bPassword := (randAlphaNum 32) | b64enc }}
2+
{{- $bUsername := (randAlphaNum 32) | b64enc }}
3+
{{- $pPassword := (randAlphaNum 32) | b64enc }}
4+
{{- $pUsername := (randAlphaNum 32) | b64enc }}
35

4-
{{- $secretName := printf "%s-%s" (include "coms.fullname" .) "basicauth" }}
5-
{{- $secret := (lookup "v1" "Secret" .Release.Namespace $secretName ) }}
6-
{{- if not $secret }}
6+
{{- $bSecretName := printf "%s-%s" (include "coms.fullname" .) "basicauth" }}
7+
{{- $bSecret := (lookup "v1" "Secret" .Release.Namespace $bSecretName ) }}
8+
{{- $pSecretName := printf "%s-%s" (include "coms.fullname" .) "passphrase" }}
9+
{{- $pSecret := (lookup "v1" "Secret" .Release.Namespace $pSecretName ) }}
10+
11+
{{- if not $bSecret }}
12+
---
13+
apiVersion: v1
14+
kind: Secret
15+
metadata:
16+
annotations:
17+
"helm.sh/resource-policy": keep
18+
name: {{ $bSecretName }}
19+
labels: {{ include "coms.labels" . | nindent 4 }}
20+
type: kubernetes.io/basic-auth
21+
data:
22+
password: {{ $bPassword }}
23+
username: {{ $bUsername }}
24+
{{- end }}
25+
{{- if not $pSecret }}
726
---
827
apiVersion: v1
928
kind: Secret
1029
metadata:
1130
annotations:
1231
"helm.sh/resource-policy": keep
13-
name: {{ $secretName }}
32+
name: {{ $pSecretName }}
1433
labels: {{ include "coms.labels" . | nindent 4 }}
1534
type: kubernetes.io/basic-auth
1635
data:
17-
password: {{ $password }}
18-
username: {{ $username }}
36+
password: {{ $pPassword }}
37+
username: {{ $pUsername }}
1938
{{- end }}

0 commit comments

Comments
 (0)