Skip to content

Commit

Permalink
Fix station-id test noise. Closes #438 (#440)
Browse files Browse the repository at this point in the history
* silence log messages

* silence experimental warning

* fix moca bin on windows
  • Loading branch information
juliangruber authored May 3, 2024
1 parent 01e9a9f commit 14ced48
Show file tree
Hide file tree
Showing 3 changed files with 33 additions and 21 deletions.
29 changes: 18 additions & 11 deletions lib/station-id.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,23 @@ import { subtle, getRandomValues } from 'node:crypto'
* @param {object} args
* @param {string} args.secretsDir
* @param {string} args.passphrase
* @param {import('node:console')} [args.log]
* @returns {Promise<{publicKey: string, privateKey: string}>}
*/
export async function getStationId ({ secretsDir, passphrase }) {
export async function getStationId ({ secretsDir, passphrase, log = console }) {
assert.strictEqual(typeof secretsDir, 'string', 'secretsDir must be a string')

await fs.mkdir(secretsDir, { recursive: true })
const keystore = path.join(secretsDir, 'station_id')

try {
const keypair = await loadKeypair(keystore, passphrase)
console.error('Loaded Station ID: %s', keypair.publicKey)
const keypair = await loadKeypair(keystore, passphrase, { log })
log.error('Loaded Station ID: %s', keypair.publicKey)
return keypair
} catch (err) {
if (err.code === 'ENOENT' && err.path === keystore) {
// the keystore file does not exist, create a new key
return await generateKeypair(keystore, passphrase)
return await generateKeypair(keystore, passphrase, { log })
} else {
throw new Error(
`Cannot load Station ID from file "${keystore}". ${err.message}`,
Expand All @@ -35,9 +36,11 @@ export async function getStationId ({ secretsDir, passphrase }) {
/**
* @param {string} keystore
* @param {string} passphrase
* @param {object} args
* @param {import('node:console')} args.log
* @returns {Promise<{publicKey: string, privateKey: string}>}
*/
async function loadKeypair (keystore, passphrase) {
async function loadKeypair (keystore, passphrase, { log }) {
const ciphertext = await fs.readFile(keystore)
let plaintext

Expand All @@ -49,7 +52,7 @@ async function loadKeypair (keystore, passphrase) {
ciphertext[ciphertext.length - 1] === '}'.charCodeAt(0)

if (looksLikeJson) {
const keypair = await tryUpgradePlaintextToCiphertext(passphrase, keystore, ciphertext)
const keypair = await tryUpgradePlaintextToCiphertext(passphrase, keystore, ciphertext, { log })
if (keypair) return keypair
// fall back and continue the original path to decrypt the file
}
Expand All @@ -71,9 +74,11 @@ async function loadKeypair (keystore, passphrase) {
* @param {string} keystore
* @param {string} passphrase
* @param {Buffer} maybeCiphertext
* @param {object} args
* @param {import('node:console')} args.log
* @returns
*/
async function tryUpgradePlaintextToCiphertext (passphrase, keystore, maybeCiphertext) {
async function tryUpgradePlaintextToCiphertext (passphrase, keystore, maybeCiphertext, { log }) {
let keypair
try {
keypair = parseStoredKeys(maybeCiphertext)
Expand All @@ -84,7 +89,7 @@ async function tryUpgradePlaintextToCiphertext (passphrase, keystore, maybeCiphe

// re-create the keypair file with encrypted keypair
await storeKeypair(passphrase, keystore, keypair)
console.error('Encrypted the Station ID file using the provided PASSPHRASE.')
log.error('Encrypted the Station ID file using the provided PASSPHRASE.')
return keypair
}
/**
Expand All @@ -101,11 +106,13 @@ function parseStoredKeys (json) {
/**
* @param {string} keystore
* @param {string} passphrase
* @param {object} args
* @param {import('node:console')} [args.log]
* @returns {Promise<{publicKey: string, privateKey: string}>}
*/
async function generateKeypair (keystore, passphrase) {
async function generateKeypair (keystore, passphrase, { log }) {
if (!passphrase) {
console.warn(`
log.warn(`
*****************************************************************************************
The private key of the identity of your Station instance will be stored in plaintext.
We strongly recommend you to configure PASSPHRASE environment variable to enable
Expand All @@ -120,7 +127,7 @@ async function generateKeypair (keystore, passphrase) {
)
const publicKey = Buffer.from(await subtle.exportKey('spki', keyPair.publicKey)).toString('hex')
const privateKey = Buffer.from(await subtle.exportKey('pkcs8', keyPair.privateKey)).toString('hex')
console.error('Generated a new Station ID:', publicKey)
log.error('Generated a new Station ID:', publicKey)
await storeKeypair(passphrase, keystore, { publicKey, privateKey })
return { publicKey, privateKey }
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"test": "npm run test:lint && npm run test:unit",
"test:lint": "prettier --check . && standard",
"test:types": "tsc -p .",
"test:unit": "cross-env STATE_ROOT=test/.state CACHE_ROOT=test/.cache mocha",
"test:unit": "cross-env STATE_ROOT=test/.state CACHE_ROOT=test/.cache node --no-warnings=ExperimentalWarning node_modules/mocha/bin/_mocha",
"version": "node ./scripts/version.js",
"postinstall": "node ./scripts/post-install.js",
"postpublish": "node ./scripts/post-publish.js",
Expand Down
23 changes: 14 additions & 9 deletions test/station-id.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,58 +4,63 @@ import path from 'node:path'
import { decrypt, encrypt, getStationId } from '../lib/station-id.js'
import { getUniqueTempDir } from './util.js'

const log = {
error () {},
warn () {}
}

describe('station-id', () => {
describe('getStationId', () => {
it('creates a new key and stores it in the given path', async () => {
const secretsDir = getUniqueTempDir()
const generated = await getStationId({ secretsDir, passphrase: 'secret' })
const generated = await getStationId({ secretsDir, passphrase: 'secret', log })
assert.match(generated.publicKey, /^[0-9a-z]+$/)
assert.match(generated.privateKey, /^[0-9a-z]+$/)

await fs.stat(path.join(secretsDir, 'station_id'))
// the check passes if the statement above does not throw

const loaded = await getStationId({ secretsDir, passphrase: 'secret' })
const loaded = await getStationId({ secretsDir, passphrase: 'secret', log })
assert.deepStrictEqual(loaded, generated)
})

it('returns a public key that is exactly 88 characters long', async () => {
// spark-api is enforcing this constraint and rejecting measurements containing stationId
// in a different format
const secretsDir = getUniqueTempDir()
const { publicKey } = await await getStationId({ secretsDir, passphrase: 'secret' })
const { publicKey } = await await getStationId({ secretsDir, passphrase: 'secret', log })
assert.strictEqual(publicKey.length, 88, 'publicKey.length')
assert.match(publicKey, /^[0-9A-Za-z]*$/)
})

it('skips encryption when passphrase is not set', async () => {
const secretsDir = getUniqueTempDir()
const generated = await getStationId({ secretsDir, passphrase: '' })
const generated = await getStationId({ secretsDir, passphrase: '', log })
assert.match(generated.publicKey, /^[0-9a-z]+$/)
assert.match(generated.privateKey, /^[0-9a-z]+$/)

await fs.stat(path.join(secretsDir, 'station_id'))
// the check passes if the statement above does not throw

const loaded = await getStationId({ secretsDir, passphrase: '' })
const loaded = await getStationId({ secretsDir, passphrase: '', log })
assert.deepStrictEqual(loaded, generated)
})

it('provides a helpful error message when the file cannot be decrypted', async () => {
const secretsDir = getUniqueTempDir()
await getStationId({ secretsDir, passphrase: 'secret' })
await getStationId({ secretsDir, passphrase: 'secret', log })
await assert.rejects(
getStationId({ secretsDir, passphrase: 'wrong pass' }),
getStationId({ secretsDir, passphrase: 'wrong pass', log }),
/Cannot decrypt Station ID file. Did you configure the correct PASSPHRASE/
)
})

it('encrypts plaintext station_id file when PASSPHRASE is provided', async () => {
const secretsDir = getUniqueTempDir()
const generated = await getStationId({ secretsDir, passphrase: '' })
const generated = await getStationId({ secretsDir, passphrase: '', log })
const plaintext = await fs.readFile(path.join(secretsDir, 'station_id'))

const loaded = await getStationId({ secretsDir, passphrase: 'super-secret' })
const loaded = await getStationId({ secretsDir, passphrase: 'super-secret', log })
assert.deepStrictEqual(loaded, generated)

const ciphertext = await fs.readFile(path.join(secretsDir, 'station_id'))
Expand Down

0 comments on commit 14ced48

Please sign in to comment.