diff --git a/README.md b/README.md index 3123ec3f4af..88eca06e7ef 100644 --- a/README.md +++ b/README.md @@ -238,7 +238,7 @@ To learn more about the Nightscout API, visit https://YOUR-SITE.com/api-docs/ or ### Required * `MONGODB_URI` - The connection string for your Mongo database. Something like `mongodb://sally:sallypass@ds099999.mongolab.com:99999/nightscout`. - * `API_SECRET` - A secret passphrase that must be at least 12 characters long. + * `API_SECRET` - A secret passphrase that must be at least 12 characters long. Alternatively, if `API_SECRET_FILE` is defined, the secret passphrase will be read from the specified file. * `MONGODB_COLLECTION` (`entries`) - The Mongo collection where CGM entries are stored. * `DISPLAY_UNITS` (`mg/dl`) - Options are `mg/dl` or `mmol/L` (or just `mmol`). Setting to `mmol/L` puts the entire server into `mmol/L` mode by default, no further settings needed. diff --git a/lib/server/enclave.js b/lib/server/enclave.js index 03dc8facf96..ff02dbc5ec3 100644 --- a/lib/server/enclave.js +++ b/lib/server/enclave.js @@ -44,7 +44,7 @@ const init = function init () { } enclave.isApiKeySet = function isApiKeySet () { - return isApiKeySet; + return apiKeySet; } enclave.isApiKey = function isApiKey (keyValue) { diff --git a/lib/server/env.js b/lib/server/env.js index afb53d51332..7e316f05524 100644 --- a/lib/server/env.js +++ b/lib/server/env.js @@ -77,22 +77,19 @@ function setSSL () { env.secureCspReportOnly = readENVTruthy("SECURE_CSP_REPORT_ONLY", false); } -// A little ugly, but we don't want to read the secret into a var function setAPISecret () { - var useSecret = (readENV('API_SECRET') && readENV('API_SECRET').length > 0); + // if no value is provided as an environment variable, try to read it from a file + const apiSecret = readENV('API_SECRET') || readEnvFile('API_SECRET_FILE'); //TODO: should we clear API_SECRET from process env? env.api_secret = null; // if a passphrase was provided, get the hex digest to mint a single token - if (useSecret) { - if (readENV('API_SECRET').length < consts.MIN_PASSPHRASE_LENGTH) { + if (apiSecret && apiSecret.length > 0) { + if (apiSecret.length < consts.MIN_PASSPHRASE_LENGTH) { var msg = ['API_SECRET should be at least', consts.MIN_PASSPHRASE_LENGTH, 'characters'].join(' '); console.error(msg); env.err.push({ desc: msg }); } else { - - const apiSecret = readENV('API_SECRET'); delete process.env.API_SECRET; - env.enclave.setApiKey(apiSecret); var testresult = stringEntropy(apiSecret); @@ -108,7 +105,6 @@ function setAPISecret () { env.notifies.push({ persistent: true, title: 'Security issue', message: 'MongoDB password and API_SECRET match. This is a really bad idea. Please change both and do not reuse passwords across the system.' }); } } - } } } @@ -185,6 +181,21 @@ function readENVTruthy (varName, defaultValue) { return value; } +function readEnvFile(varName) { + let value = null; + const fileName = readENV(varName); + + if (fileName && fileName.length > 0) { + try { + value = fs.readFileSync(fileName); + } catch (error) { + env.err.push({ desc: `Unable to read ${varName}: ${error.message}` }); + } + } + + return value; +} + function findExtendedSettings (envs) { var extended = {}; diff --git a/tests/env.test.js b/tests/env.test.js index 0de2a686054..0e3e0f4f5d0 100644 --- a/tests/env.test.js +++ b/tests/env.test.js @@ -1,8 +1,71 @@ 'use strict'; require('should'); +const fs = require('fs'); +const os = require('os'); +const path = require('path'); describe('env', function () { + function writeTempFile(fileName, data) { + const fullPath = path.join(os.tmpdir(), fileName); + fs.writeFileSync(fullPath, data); + return fullPath; + } + + it('should not set the API key without API_SECRET or API_SECRET_FILE', function () { + delete process.env.API_SECRET; + var env = require( '../lib/server/env' )(); + env.enclave.isApiKeySet().should.equal(false); + }); + + it('should read the API key from API_SECRET_FILE if it is valid', function () { + const apiSecretFile = 'this is another pass phrase'; + const hashFile = 'c79c6db1070da3537d0162e60647b0a588769f8d'; + process.env.API_SECRET_FILE = writeTempFile('api_secret_file', apiSecretFile); + + var env = require( '../lib/server/env' )(); + env.enclave.isApiKeySet().should.equal(true); + env.enclave.isApiKey(hashFile).should.equal(true); + + fs.rmSync(process.env.API_SECRET_FILE); + delete process.env.API_SECRET_FILE; + }); + + it('should raise an error when API_SECRET_FILE does not exist', function () { + const nonexistentPath = path.join(os.tmpdir(), 'api_secret_file'); + process.env.API_SECRET_FILE = nonexistentPath; + + var env = require( '../lib/server/env' )(); + env.enclave.isApiKeySet().should.equal(false); + env.err.length.should.equal(1); + + const error = env.err.pop(); + error.should.have.property('desc'); + error.desc.should.match(/API_SECRET_FILE/); + error.desc.should.match(/no such file or directory/); + + delete process.env.API_SECRET_FILE; + }); + + it('should use API_SECRET when API_SECRET_FILE is also specified', function () { + const apiSecretEnv = 'this is my long pass phrase'; + const hashEnv = 'b723e97aa97846eb92d5264f084b2823f57c4aa1'; + process.env.API_SECRET = apiSecretEnv; + + const apiSecretFile = 'this is another pass phrase'; + const hashFile = 'c79c6db1070da3537d0162e60647b0a588769f8d'; + process.env.API_SECRET_FILE = writeTempFile('api_secret_file', apiSecretFile); + + var env = require( '../lib/server/env' )(); + env.enclave.isApiKeySet().should.equal(true); + env.enclave.isApiKey(hashEnv).should.equal(true); + env.enclave.isApiKey(hashFile).should.equal(false); + + fs.rmSync(process.env.API_SECRET_FILE); + delete process.env.API_SECRET_FILE; + delete process.env.API_SECRET; + }); + it( 'show the right plugins', function () { process.env.SHOW_PLUGINS = 'iob'; process.env.ENABLE = 'iob cob';