Skip to content

Commit ea548d6

Browse files
author
Artem
authored
Merge pull request #2811 from RedisInsight/be/feature/RI-5072-docker-encryption
Be/feature/ri 5072 docker encryption
2 parents 89fc776 + 45cfdbc commit ea548d6

13 files changed

+317
-12
lines changed

DOCKER_README.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ These commands will build the image and then start the container. Redis Insight
1717

1818
Redis Insight supports several configuration values that can be supplied via container environment variables. The following may be provided:
1919

20-
| Variable | Purpose | Default | Additional Info |
21-
| ---------|---------|-----------------|---------|
22-
| RI_APP_PORT | The port the app listens on | 5000 | See [Express Documentation](https://expressjs.com/en/api.html#app.listen) |
23-
| RI_APP_HOST | The host the app listens on | 0.0.0.0 | See [Express Documentation](https://expressjs.com/en/api.html#app.listen) |
24-
| RI_SERVER_TLS_KEY | Private key for HTTPS | | Private key in [PEM format](https://www.ssl.com/guide/pem-der-crt-and-cer-x-509-encodings-and-conversions/#ftoc-heading-3). May be a path to a file or a string in PEM format. |
25-
| RI_SERVER_TLS_CERT | Certificate for supplied private key | | Public certificate in [PEM format](https://www.ssl.com/guide/pem-der-crt-and-cer-x-509-encodings-and-conversions/#ftoc-heading-3) |
20+
| Variable | Purpose | Default | Additional Info |
21+
| ---------|---------|-----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
22+
| RI_APP_PORT | The port the app listens on | 5000 | See [Express Documentation](https://expressjs.com/en/api.html#app.listen) |
23+
| RI_APP_HOST | The host the app listens on | 0.0.0.0 | See [Express Documentation](https://expressjs.com/en/api.html#app.listen) |
24+
| RI_SERVER_TLS_KEY | Private key for HTTPS | | Private key in [PEM format](https://www.ssl.com/guide/pem-der-crt-and-cer-x-509-encodings-and-conversions/#ftoc-heading-3). May be a path to a file or a string in PEM format. |
25+
| RI_SERVER_TLS_CERT | Certificate for supplied private key | | Public certificate in [PEM format](https://www.ssl.com/guide/pem-der-crt-and-cer-x-509-encodings-and-conversions/#ftoc-heading-3) |
26+
| RI_ENCRYPTION_KEY | Key to encrypt data with | | Redisinsight stores some data such as connection details locally (using [sqlite3](https://github.com/TryGhost/node-sqlite3)). It might be usefull to store sensitive data such as passwords, or private keys encrypted. For this case RedisInsight supports encryption with provided key.<br />Note: The Key must be the same for the same RedisInsight instance to be able to decrypt exising data. If for some reason the key was changed, you will have to enter the credentials again to connect to the Redis database. |

redisinsight/api/config/default.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export default {
5555
pluginsAssetsUri: '/static/resources/plugins',
5656
base: process.env.RI_BASE || '/',
5757
secretStoragePassword: process.env.SECRET_STORAGE_PASSWORD,
58+
encryptionKey: process.env.RI_ENCRYPTION_KEY,
5859
tlsCert: process.env.RI_SERVER_TLS_CERT,
5960
tlsKey: process.env.RI_SERVER_TLS_KEY,
6061
staticContent: !!process.env.SERVER_STATIC_CONTENT || false,

redisinsight/api/src/__mocks__/encryption.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { EncryptionStrategy } from 'src/modules/encryption/models';
22

33
export const mockDataToEncrypt = 'stringtoencrypt';
44
export const mockKeytarPassword = 'somepassword';
5+
export const mockEncryptionKey = 'somepassword';
56

67
export const mockEncryptionStrategy = EncryptionStrategy.KEYTAR;
78

@@ -10,6 +11,13 @@ export const mockEncryptResult = {
1011
encryption: mockEncryptionStrategy,
1112
};
1213

14+
export const mockKeyEncryptionStrategy = EncryptionStrategy.KEY;
15+
16+
export const mockKeyEncryptResult = {
17+
data: '4a558dfef5c1abbdf745232614194ee9',
18+
encryption: mockKeyEncryptionStrategy,
19+
};
20+
1321
export const mockEncryptionService = jest.fn(() => ({
1422
getAvailableEncryptionStrategies: jest.fn(),
1523
encrypt: jest.fn(),
@@ -22,6 +30,12 @@ export const mockEncryptionStrategyInstance = jest.fn(() => ({
2230
decrypt: jest.fn(),
2331
}));
2432

33+
export const mockKeyEncryptionStrategyInstance = jest.fn(() => ({
34+
isAvailable: jest.fn(),
35+
encrypt: jest.fn(),
36+
decrypt: jest.fn(),
37+
}));
38+
2539
export const mockKeytarModule = {
2640
getPassword: jest.fn(),
2741
setPassword: jest.fn(),

redisinsight/api/src/modules/encryption/encryption.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
22
import { PlainEncryptionStrategy } from 'src/modules/encryption/strategies/plain-encryption.strategy';
33
import { KeytarEncryptionStrategy } from 'src/modules/encryption/strategies/keytar-encryption.strategy';
44
import { EncryptionService } from 'src/modules/encryption/encryption.service';
5+
import { KeyEncryptionStrategy } from 'src/modules/encryption/strategies/key-encryption.strategy';
56

67
@Module({})
78
export class EncryptionModule {
@@ -11,6 +12,7 @@ export class EncryptionModule {
1112
providers: [
1213
PlainEncryptionStrategy,
1314
KeytarEncryptionStrategy,
15+
KeyEncryptionStrategy,
1416
EncryptionService,
1517
],
1618
exports: [

redisinsight/api/src/modules/encryption/encryption.service.spec.ts

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import { Test, TestingModule } from '@nestjs/testing';
22
import {
3-
mockAppSettings, mockAppSettingsInitial, mockAppSettingsWithoutPermissions,
3+
mockAppSettings,
4+
mockAppSettingsInitial,
5+
mockAppSettingsWithoutPermissions,
46
mockEncryptionStrategyInstance,
57
mockEncryptResult,
8+
mockKeyEncryptionStrategyInstance,
9+
mockKeyEncryptResult,
610
mockSettingsService,
711
MockType,
812
} from 'src/__mocks__';
@@ -12,11 +16,13 @@ import { KeytarEncryptionStrategy } from 'src/modules/encryption/strategies/keyt
1216
import { EncryptionStrategy } from 'src/modules/encryption/models';
1317
import { UnsupportedEncryptionStrategyException } from 'src/modules/encryption/exceptions';
1418
import { SettingsService } from 'src/modules/settings/settings.service';
19+
import { KeyEncryptionStrategy } from 'src/modules/encryption/strategies/key-encryption.strategy';
1520

1621
describe('EncryptionService', () => {
1722
let service: EncryptionService;
1823
let plainEncryptionStrategy: MockType<PlainEncryptionStrategy>;
1924
let keytarEncryptionStrategy: MockType<KeytarEncryptionStrategy>;
25+
let keyEncryptionStrategy: MockType<KeyEncryptionStrategy>;
2026
let settingsService: MockType<SettingsService>;
2127

2228
beforeEach(async () => {
@@ -33,6 +39,10 @@ describe('EncryptionService', () => {
3339
provide: KeytarEncryptionStrategy,
3440
useFactory: mockEncryptionStrategyInstance,
3541
},
42+
{
43+
provide: KeyEncryptionStrategy,
44+
useFactory: mockKeyEncryptionStrategyInstance,
45+
},
3646
{
3747
provide: SettingsService,
3848
useFactory: mockSettingsService,
@@ -43,22 +53,43 @@ describe('EncryptionService', () => {
4353
service = module.get(EncryptionService);
4454
plainEncryptionStrategy = module.get(PlainEncryptionStrategy);
4555
keytarEncryptionStrategy = module.get(KeytarEncryptionStrategy);
56+
keyEncryptionStrategy = module.get(KeyEncryptionStrategy);
4657
settingsService = module.get(SettingsService);
4758

4859
settingsService.getAppSettings.mockResolvedValue(mockAppSettings);
4960
});
5061

5162
describe('getAvailableEncryptionStrategies', () => {
52-
it('Should return list 2 strategies available', async () => {
63+
it('Should return list 2 strategies available (KEYTAR and PLAIN)', async () => {
5364
keytarEncryptionStrategy.isAvailable.mockResolvedValueOnce(true);
65+
keyEncryptionStrategy.isAvailable.mockResolvedValueOnce(false);
5466

5567
expect(await service.getAvailableEncryptionStrategies()).toEqual([
5668
EncryptionStrategy.PLAIN,
5769
EncryptionStrategy.KEYTAR,
5870
]);
5971
});
72+
it('Should return list 2 strategies available (KEY and PLAIN)', async () => {
73+
keytarEncryptionStrategy.isAvailable.mockResolvedValueOnce(false);
74+
keyEncryptionStrategy.isAvailable.mockResolvedValueOnce(true);
75+
76+
expect(await service.getAvailableEncryptionStrategies()).toEqual([
77+
EncryptionStrategy.PLAIN,
78+
EncryptionStrategy.KEY,
79+
]);
80+
});
81+
it('Should return list 2 strategies available (KEY and PLAIN) even when KEYTAR available', async () => {
82+
keytarEncryptionStrategy.isAvailable.mockResolvedValueOnce(true);
83+
keyEncryptionStrategy.isAvailable.mockResolvedValueOnce(true);
84+
85+
expect(await service.getAvailableEncryptionStrategies()).toEqual([
86+
EncryptionStrategy.PLAIN,
87+
EncryptionStrategy.KEY,
88+
]);
89+
});
6090
it('Should return list with one strategy available', async () => {
6191
keytarEncryptionStrategy.isAvailable.mockResolvedValueOnce(false);
92+
keyEncryptionStrategy.isAvailable.mockResolvedValueOnce(false);
6293

6394
expect(await service.getAvailableEncryptionStrategies()).toEqual([
6495
EncryptionStrategy.PLAIN,
@@ -70,7 +101,15 @@ describe('EncryptionService', () => {
70101
it('Should return KEYTAR strategy based on app agreements', async () => {
71102
expect(await service.getEncryptionStrategy()).toEqual(keytarEncryptionStrategy);
72103
});
73-
it('Should return PLAIN strategy based on app agreements', async () => {
104+
it('Should return KEY strategy based on app agreements even when KEYTAR available', async () => {
105+
keytarEncryptionStrategy.isAvailable.mockResolvedValueOnce(true);
106+
keyEncryptionStrategy.isAvailable.mockResolvedValueOnce(true);
107+
108+
expect(await service.getEncryptionStrategy()).toEqual(keyEncryptionStrategy);
109+
});
110+
it('Should return PLAIN strategy based on app agreements even when KEY available', async () => {
111+
keyEncryptionStrategy.isAvailable.mockResolvedValueOnce(true);
112+
74113
settingsService.getAppSettings.mockResolvedValueOnce(mockAppSettingsWithoutPermissions);
75114

76115
expect(await service.getEncryptionStrategy()).toEqual(plainEncryptionStrategy);
@@ -83,19 +122,37 @@ describe('EncryptionService', () => {
83122
});
84123

85124
describe('encrypt', () => {
86-
it('Should encrypt data and return proper response', async () => {
125+
it('Should encrypt data and return proper response (KEYTAR)', async () => {
126+
keyEncryptionStrategy.isAvailable.mockResolvedValueOnce(false);
127+
87128
keytarEncryptionStrategy.encrypt.mockResolvedValueOnce(mockEncryptResult);
88129

89130
expect(await service.encrypt('string')).toEqual(mockEncryptResult);
90131
});
132+
it('Should encrypt data and return proper response (KEY)', async () => {
133+
keyEncryptionStrategy.isAvailable.mockResolvedValueOnce(true);
134+
135+
keyEncryptionStrategy.encrypt.mockResolvedValueOnce(mockKeyEncryptResult);
136+
137+
expect(await service.encrypt('string')).toEqual(mockKeyEncryptResult);
138+
});
91139
});
92140

93141
describe('decrypt', () => {
94-
it('Should return decrypted string', async () => {
142+
it('Should return decrypted string (KEYTAR)', async () => {
143+
keyEncryptionStrategy.isAvailable.mockResolvedValueOnce(false);
144+
95145
keytarEncryptionStrategy.decrypt.mockResolvedValueOnce(mockEncryptResult.data);
96146

97147
expect(await service.decrypt('string', EncryptionStrategy.KEYTAR)).toEqual(mockEncryptResult.data);
98148
});
149+
it('Should return decrypted string (KEY)', async () => {
150+
keyEncryptionStrategy.isAvailable.mockResolvedValueOnce(true);
151+
152+
keyEncryptionStrategy.decrypt.mockResolvedValueOnce(mockKeyEncryptResult.data);
153+
154+
expect(await service.decrypt('string', EncryptionStrategy.KEY)).toEqual(mockKeyEncryptResult.data);
155+
});
99156
it('Should return null when no data passed', async () => {
100157
expect(await service.decrypt(null, EncryptionStrategy.KEYTAR)).toEqual(null);
101158
});

redisinsight/api/src/modules/encryption/encryption.service.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ import {
77
UnsupportedEncryptionStrategyException,
88
} from 'src/modules/encryption/exceptions';
99
import { SettingsService } from 'src/modules/settings/settings.service';
10+
import { KeyEncryptionStrategy } from 'src/modules/encryption/strategies/key-encryption.strategy';
1011

1112
@Injectable()
1213
export class EncryptionService {
1314
constructor(
1415
private readonly settingsService: SettingsService,
1516
private readonly keytarEncryptionStrategy: KeytarEncryptionStrategy,
1617
private readonly plainEncryptionStrategy: PlainEncryptionStrategy,
18+
private readonly keyEncryptionStrategy: KeyEncryptionStrategy,
1719
) {}
1820

1921
/**
@@ -25,7 +27,9 @@ export class EncryptionService {
2527
EncryptionStrategy.PLAIN,
2628
];
2729

28-
if (await this.keytarEncryptionStrategy.isAvailable()) {
30+
if (await this.keyEncryptionStrategy.isAvailable()) {
31+
strategies.push(EncryptionStrategy.KEY);
32+
} else if (await this.keytarEncryptionStrategy.isAvailable()) {
2933
strategies.push(EncryptionStrategy.KEYTAR);
3034
}
3135

@@ -43,6 +47,9 @@ export class EncryptionService {
4347
const settings = await this.settingsService.getAppSettings('1');
4448
switch (settings.agreements?.encryption) {
4549
case true:
50+
if (await this.keyEncryptionStrategy.isAvailable()) {
51+
return this.keyEncryptionStrategy;
52+
}
4653
return this.keytarEncryptionStrategy;
4754
case false:
4855
return this.plainEncryptionStrategy;

redisinsight/api/src/modules/encryption/exceptions/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
export * from './encryption-service-error.exception';
2+
export * from './key-decryption-error.exception';
3+
export * from './key-encryption-error.exception';
4+
export * from './key-unavailable.exception';
25
export * from './keytar-decryption-error.exception';
36
export * from './keytar-encryption-error.exception';
47
export * from './keytar-unavailable.exception';
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import {
2+
EncryptionServiceErrorException,
3+
} from 'src/modules/encryption/exceptions/encryption-service-error.exception';
4+
5+
export class KeyDecryptionErrorException extends EncryptionServiceErrorException {
6+
constructor(message = 'Unable to decrypt data') {
7+
super({
8+
message,
9+
name: 'KeyDecryptionError',
10+
statusCode: 500,
11+
}, 500);
12+
}
13+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import {
2+
EncryptionServiceErrorException,
3+
} from 'src/modules/encryption/exceptions/encryption-service-error.exception';
4+
5+
export class KeyEncryptionErrorException extends EncryptionServiceErrorException {
6+
constructor(message = 'Unable to encrypt data') {
7+
super({
8+
message,
9+
name: 'KeyEncryptionError',
10+
statusCode: 500,
11+
}, 500);
12+
}
13+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import {
2+
EncryptionServiceErrorException,
3+
} from 'src/modules/encryption/exceptions/encryption-service-error.exception';
4+
5+
export class KeyUnavailableException extends EncryptionServiceErrorException {
6+
constructor(message = 'Encryption key unavailable') {
7+
super({
8+
message,
9+
name: 'KeyUnavailable',
10+
statusCode: 503,
11+
}, 503);
12+
}
13+
}

0 commit comments

Comments
 (0)