Skip to content

Commit e7710c5

Browse files
committed
allow salt value to be injected into loadConfig
The salt value can now either be included in the loaded configuration file, or passed in as an argument into the loadConfig function. The config file option has priority.
1 parent b760ba4 commit e7710c5

11 files changed

Lines changed: 244 additions & 93 deletions

src/config/configStore.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ interface ConfigStorePaths {
4747
config: string;
4848
appConfig: string;
4949
backupConfig: string;
50-
salt?: string;
5150
}
5251

5352
export class ConfigStore {

src/config/loadConfig.ts

Lines changed: 52 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -38,22 +38,16 @@ type LoadConfigInput = {
3838
configPath: string;
3939
algorithmId: string;
4040
usingUI?: boolean;
41+
embeddedSalt?: Config.FileBasedSalt | Config.StringBasedSalt;
4142
validateConfig?: boolean;
4243
}
43-
// Main entry point for loading a config file.
44-
// returns:
45-
// - { success: true } if the config can be loaded
46-
// - { success: false, error: "string" } if there are errors
47-
// - { success: false, isSaltFileError: true, error: "string"}
48-
// if there is something wrong with the salt file
49-
export function loadConfig({ configPath, algorithmId, usingUI=false, validateConfig=true }: LoadConfigInput): LoadConfigResult {
50-
log('Loading config from', configPath);
51-
52-
// attempt to read the file
44+
45+
export function loadConfig({ configPath, algorithmId, embeddedSalt, usingUI=false, validateConfig=true }: LoadConfigInput): LoadConfigResult {
46+
log('[INFO] Loading config from', configPath);
5347
const configData = attemptToReadTOMLData<Config.FileConfiguration>(configPath, CONFIG_FILE_ENCODING);
5448

55-
// if cannot be read, we have an error
5649
if (!configData) {
50+
log('[ERROR] Unable to read config file', configPath);
5751
return {
5852
success: false,
5953
error: `Unable to read config file '${configPath}'`,
@@ -78,7 +72,7 @@ export function loadConfig({ configPath, algorithmId, usingUI=false, validateCon
7872
}
7973
// TODO: check sinature validity before salt injection
8074
const configHash = generateConfigHash(configData);
81-
log('CONFIG HASH:', configHash);
75+
log('[INFO] Generated config hash:', configHash);
8276

8377
// fail if the signature is not OK
8478
if (configHash !== configData.meta.signature) {
@@ -96,50 +90,56 @@ export function loadConfig({ configPath, algorithmId, usingUI=false, validateCon
9690
configData.algorithm.columns.reference = configData.algorithm.columns.reference.sort();
9791
configData.algorithm.columns.static = configData.algorithm.columns.static.sort();
9892

99-
// TODO: check whether embedded salt file path is provided, config should override embedded.
93+
// salt is either provided in the config file (STRING | FILE) or is explicitly provided to
94+
// to this function (e.g. by the UI). Precedence should be given to the config file, although
95+
// the config fields are optional. The programme should fail if no salt is provided.
10096

101-
// check if we need to inject the salt data into the config
102-
// if not, the config loading is finished
103-
if (configData.algorithm.salt.source === 'STRING') {
104-
return {
105-
success: true,
106-
lastUpdated: lastUpdateDate,
107-
config: configData,
108-
};
97+
if (configData.algorithm.salt && configData.algorithm.salt.source == "STRING") {
98+
return { success: true, lastUpdated: lastUpdateDate, config: configData };
99+
}
100+
101+
if (configData.algorithm.salt && configData.algorithm.salt.source == "FILE") {
102+
// load the file, convert to a string value, update the config to be of type: "STRING"
103+
const saltFilePath = configData.algorithm.salt.value;
104+
const validatorRegexp = configData.algorithm.salt.validator_regex ? new RegExp(configData.algorithm.salt.validator_regex) : undefined;
105+
return tryLoadSaltFile({ saltFilePath, validatorRegexp, configData, lastUpdateDate, label: "salt" });
106+
}
107+
108+
if (embeddedSalt && embeddedSalt.source == "STRING") {
109+
configData.algorithm.salt = { source: "STRING", value: embeddedSalt.value }
110+
return { success: true, lastUpdated: lastUpdateDate, config: configData };
109111
}
110112

111-
// figure out the file path and the validation regexp
112-
const saltFilePath = configData.algorithm.salt.value;
113-
const saltFileValidatorRegexp = configData.algorithm.salt.validator_regex
114-
? RegExp(configData.algorithm.salt.validator_regex)
115-
: undefined;
113+
if (embeddedSalt && embeddedSalt.source == "FILE") {
114+
const saltFilePath = embeddedSalt.value;
115+
const validatorRegexp = embeddedSalt.validator_regex ? new RegExp(embeddedSalt.validator_regex) : undefined;
116+
return tryLoadSaltFile({ saltFilePath, validatorRegexp, configData, lastUpdateDate, label: "embedded salt" });
117+
}
116118

117-
// attempt to load the salt file
118-
// saltFilePath must be { win32: string, darwin: string } at this stage since salt.source is guaranteed to be "FILE".
119-
const saltData = loadSaltFile({ saltFilePath: saltFilePath, validatorRegexp: saltFileValidatorRegexp });
119+
return { success: false, error: `No salt configuration provided: either specify salt in config file, or pass in path on config load.`, isSaltFileError: true, config: configData };
120+
}
120121

121-
// if the salt file load failed, we have failed
122-
if (!saltData) {
123-
log('[SALT] Error while loading the salt file!');
124-
return {
125-
success: false,
126-
isSaltFileError: true,
127-
error: `Invalid salt file: '${saltFilePath}'`,
128-
// send the existing config alongside so if this config is the backup one, error messages
129-
// can still be loaded
130-
config: configData,
131-
};
122+
interface TryLoadSaltFileInput {
123+
saltFilePath: string;
124+
validatorRegexp?: RegExp;
125+
configData: Config.FileConfiguration;
126+
lastUpdateDate: Date;
127+
label: string;
128+
}
129+
130+
function tryLoadSaltFile({ saltFilePath, validatorRegexp, configData, lastUpdateDate, label="salt"}: TryLoadSaltFileInput): LoadConfigResult {
131+
log('[INFO] Loading salt from', saltFilePath);
132+
133+
const loadSaltResponse = loadSaltFile({ saltFilePath, validatorRegexp });
134+
if (!loadSaltResponse.success) {
135+
log(loadSaltResponse.message);
136+
// send the existing config alongside so if this config is the backup one, error messages can still be loaded
137+
return { success: false, isSaltFileError: true, error: `Invalid salt file: '${saltFilePath}'`, config: configData };
132138
}
133139

134-
// replace the "FILE" with "STRING" amd embed the salt data
135-
configData.algorithm.salt = configData.algorithm.salt as unknown as Config.StringBasedSalt;
136-
configData.algorithm.salt.source = 'STRING';
137-
configData.algorithm.salt.value = saltData;
138-
139-
// return the freshly injected config
140-
return {
141-
success: true,
142-
lastUpdated: lastUpdateDate,
143-
config: configData,
144-
};
145-
}
140+
if (loadSaltResponse.message) log(loadSaltResponse.message);
141+
142+
// update the config to be of salt type: "STRING" with loaded file data
143+
configData.algorithm.salt = { source: "STRING", value: loadSaltResponse.data }
144+
return { success: true, lastUpdated: lastUpdateDate, config: configData };
145+
}

src/config/loadSaltFile.ts

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,8 @@
1515
// along with this program. If not, see <http://www.gnu.org/licenses/>.
1616
import fs from 'node:fs';
1717
import path from 'node:path';
18-
import Debug from 'debug';
19-
const log = Debug('CID:loadSaltFile');
2018

2119
import { attemptToReadFileData } from './utils';
22-
import type { Config } from './Config';
2320

2421
// the encoding used for the salt file
2522
const SALT_FILE_ENCODING: fs.EncodingOption = 'utf-8';
@@ -33,22 +30,20 @@ interface LoadSaltFileInput {
3330
validatorRegexp?: RegExp;
3431
}
3532

33+
type LoadSaltFileOutput = { success: true; data: string; message?: string } | { success: false; data?: never, message: string; }
34+
3635
// Attempts to load and clean up the salt file data
37-
export function loadSaltFile({ saltFilePath, validatorRegexp = DEFAULT_VALIDATOR_REGEXP }: LoadSaltFileInput) {
38-
log('Attempting to load salt file from ', path.resolve(saltFilePath));
36+
export function loadSaltFile({ saltFilePath, validatorRegexp = DEFAULT_VALIDATOR_REGEXP }: LoadSaltFileInput): LoadSaltFileOutput {
3937

4038
// TODO: potentially clean up line endings and whitespace here
4139
const saltData = attemptToReadFileData(saltFilePath, SALT_FILE_ENCODING);
42-
if (!saltData) return null;
40+
if (!saltData) return { success: false, message: `[ERROR] Unable to read salt file at path: ${saltFilePath}` };
4341

4442
// check if the structure is correct for the file
4543
const CHECK_RX = new RegExp(validatorRegexp);
46-
4744
if (!CHECK_RX.test(saltData)) {
48-
log('SALT FILE Regexp error');
49-
return null;
45+
return { success: false, message: `[ERROR] Salt file failed validator regexp check at path: ${saltFilePath}, with regexp: ${validatorRegexp}` };
5046
}
5147

52-
log('SALT FILE looks OK');
53-
return saltData;
48+
return { success: true, data: saltData, message: `[INFO] Successfully loaded salt file from ${saltFilePath}` };
5449
}

src/config/utils.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@
1515
// along with this program. If not, see <http://www.gnu.org/licenses/>.
1616

1717
import fs from 'node:fs';
18-
import os from 'node:os';
19-
import path from 'node:path';
2018
import toml from 'toml';
2119
import { createHash } from 'node:crypto';
2220
import stableStringify from 'safe-stable-stringify';
@@ -92,13 +90,6 @@ export function generateConfigHash<T extends Config.CoreConfiguration>(config: T
9290
delete configCopy.messages;
9391
}
9492

95-
// remove the "algorithm.salt" part as it may have injected keys
96-
// TODO: this enables messing with the salt file path pre-injection without signature validations, but is required for compatibility w/ the injection workflow
97-
delete configCopy.algorithm!.salt!.value;
98-
// mock the salt source as STRING to ensure that both imported and saved
99-
// (with pre-injected salt) config files work
100-
configCopy.algorithm!.salt!.source = 'STRING';
101-
10293
// generate a stable JSON representation
10394
const stableJson = stableStringify(configCopy);
10495
if (typeof stableJson !== 'string') throw new Error(`Unable to serialise config object to JSON.`);

src/config/validateConfig.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -205,9 +205,17 @@ const checkAlgorithm = (algorithm: Config.CoreConfiguration['algorithm'], source
205205
isOneOf('[algorithm].hash.strategy', ['SHA256'], algorithm.hash.strategy);
206206
if (exists) return exists;
207207

208-
exists =
209-
isObject('[algorithm].salt', algorithm.salt) ||
210-
isOneOf('[algorithm].salt.source', ['FILE', 'STRING'], algorithm.salt.source);
208+
209+
// NOTE: technically the salt configuration is optional to provide in the configuration file
210+
// since it can be provided directly to the algorithm. The Config type requires a salt
211+
// configuration though, so adding an additional check here as well.
212+
213+
exists = isOptional('[algorithm].salt', algorithm.salt, isObject)
214+
if (exists) return exists;
215+
216+
if (!algorithm.salt) return undefined;
217+
218+
exists = isOneOf('[algorithm].salt.source', ['FILE', 'STRING'], algorithm.salt.source);
211219
if (exists) return exists;
212220

213221
if (algorithm.salt.source === 'STRING') {

tests/config/files/test-config.backup.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[meta]
22
id="ANY"
33
version="0.1.0"
4-
signature="244fb3e1e1846ae1385a64cf1c3a0ce5"
4+
signature="5dd28139ed139a7aba91aa8808a42e2a"
55

66
[source]
77
columns = [

tests/config/files/test-config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"meta": {
33
"id": "ANY",
44
"version": "0.1.0",
5-
"signature": "244fb3e1e1846ae1385a64cf1c3a0ce5"
5+
"signature": "5dd28139ed139a7aba91aa8808a42e2a"
66
},
77
"source": {
88
"columns": [
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
{
2+
"meta": {
3+
"id": "ANY",
4+
"version": "0.1.0",
5+
"signature": "244fb3e1e1846ae1385a64cf1c3a0ce5"
6+
},
7+
"source": {
8+
"columns": [
9+
{
10+
"name": "Column A",
11+
"alias": "col_a"
12+
},
13+
{
14+
"name": "Column B",
15+
"alias": "col_b"
16+
},
17+
{
18+
"name": "Column C",
19+
"alias": "col_c"
20+
},
21+
{
22+
"name": "Column D",
23+
"alias": "col_d"
24+
},
25+
{
26+
"name": "Column E",
27+
"alias": "col_e"
28+
},
29+
{
30+
"name": "Column 1",
31+
"alias": "col_1"
32+
},
33+
{
34+
"name": "Column 2",
35+
"alias": "col_2"
36+
},
37+
{
38+
"name": "Column 3",
39+
"alias": "col_3"
40+
}
41+
]
42+
},
43+
"validations": {
44+
"*": [
45+
{
46+
"op": "max_field_length",
47+
"value": 200
48+
}
49+
]
50+
},
51+
"algorithm": {
52+
"columns": {
53+
"process": ["col_a", "col_c", "col_e", "col_b", "col_d"],
54+
"static": ["col_a"],
55+
"reference": ["col_3", "col_1", "col_2"]
56+
},
57+
"hash": {
58+
"strategy": "SHA256"
59+
}
60+
},
61+
"destination": {
62+
"columns": [
63+
{
64+
"name": "Common Identifier",
65+
"alias": "hashed_id"
66+
}
67+
],
68+
"postfix": "-OUTPUT-0.0.9-{{yyyy-MM-dd--HH-mm-ss}}"
69+
},
70+
"destination_map": {
71+
"columns": [
72+
{
73+
"name": "Column A",
74+
"alias": "col_a"
75+
},
76+
{
77+
"name": "Common Identifier",
78+
"alias": "hashed_id"
79+
}
80+
],
81+
"postfix": "-MAPPING-0.0.9-{{yyyy-MM-dd--HH-mm-ss}}"
82+
},
83+
"destination_errors": {
84+
"columns": [
85+
{
86+
"name": "Errors",
87+
"alias": "errors"
88+
},
89+
{
90+
"name": "Column A",
91+
"alias": "col_a"
92+
},
93+
{
94+
"name": "Column B",
95+
"alias": "col_b"
96+
},
97+
{
98+
"name": "Column C",
99+
"alias": "col_b"
100+
}
101+
],
102+
"postfix": "-ERRORS-0.0.9-{{yyyy-MM-dd--HH-mm-ss}}"
103+
}
104+
}

tests/config/files/test-salt-loading-config.json

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,7 @@
6060
"salt": {
6161
"source": "FILE",
6262
"validator_regex": "BEGIN TEST[A-Za-z0-9+/=\\s]+END TEST",
63-
"value": {
64-
"darwin": "<SALT_PATH>",
65-
"win32": "<SALT_PATH>"
66-
}
63+
"value": "<SALT_PATH>"
6764
}
6865
},
6966
"destination": {

0 commit comments

Comments
 (0)