From 5460848ea923835db36d58a8ec2fb9298dad5c8a Mon Sep 17 00:00:00 2001 From: antelle Date: Sun, 11 Jun 2017 00:32:51 +0200 Subject: [PATCH] read private keys from external device --- package.json | 12 +- scripts/download-translations.js | 310 ++++++++++++++++--------------- scripts/sign.js | 25 +++ scripts/start.js | 4 + scripts/update-plugins.js | 22 +-- 5 files changed, 207 insertions(+), 166 deletions(-) create mode 100644 scripts/sign.js create mode 100644 scripts/start.js diff --git a/package.json b/package.json index 54c69da..842321b 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,9 @@ "doc": "docs" }, "scripts": { - "start": "npm run-script translations && npm run-script update", - "update": "node scripts/update-plugins.js", - "translations": "node scripts/download-translations.js" + "start": "node --harmony-async-await scripts/start.js", + "update": "node --harmony-async-await scripts/update-plugins.js", + "translations": "node --harmony-async-await scripts/download-translations.js" }, "repository": { "type": "git", @@ -20,5 +20,9 @@ "bugs": { "url": "https://github.com/keeweb/keeweb-plugins/issues" }, - "homepage": "https://github.com/keeweb/keeweb-plugins#readme" + "homepage": "https://github.com/keeweb/keeweb-plugins#readme", + "devDependencies": { + "keychain": "^1.3.0", + "pkcs15-smartcard-sign": "^1.0.0" + } } diff --git a/scripts/download-translations.js b/scripts/download-translations.js index 31be108..12b7303 100644 --- a/scripts/download-translations.js +++ b/scripts/download-translations.js @@ -3,6 +3,7 @@ const https = require('https'); const crypto = require('crypto'); const fs = require('fs'); +const sign = require('./sign'); const keys = require('../keys/onesky.json'); @@ -26,176 +27,185 @@ const urlParams = { const pluginManifest = fs.readFileSync('tmpl/manifest.json', 'utf8'); const pluginIndexPage = fs.readFileSync('tmpl/language-index.html', 'utf8'); -const privateKey = fs.readFileSync('keys/private-key.pem', 'binary'); const publicKey = fs.readFileSync('keys/public-key.pem', 'utf8') .match(/-+BEGIN PUBLIC KEY-+([\s\S]+?)-+END PUBLIC KEY-+/)[1] .replace(/\n/g, ''); const defaultCountries = { 'SE': 'sv' }; -loadLanguages(languages => - loadTranslations(translations => - processData(languages, translations) - ) -); - -function loadLanguages(callback) { - if (USE_FILES) { - return callback(JSON.parse(fs.readFileSync('./data/languages.json', 'utf8'))); - } - console.log('Loading language names...'); - const url = API_URL_LANGUAGES.replace(':project_id', PROJECT_ID) + '?' + - Object.keys(urlParams).map(param => param + '=' + urlParams[param]).join('&'); - https.get(url, res => { - if (res.statusCode !== 200) { - console.error(`API error ${res.statusCode}`); - return; - } - console.log('Response received, reading...'); - const data = []; - res.on('data', chunk => data.push(chunk)); - res.on('end', () => { - console.log('Data received, parsing...'); - const json = Buffer.concat(data).toString('utf8'); - const parsed = JSON.parse(json); - fs.writeFileSync('data/languages.json', JSON.stringify(parsed, null, 2)); - callback(parsed); - }); +module.exports = function() { + return new Promise(resolve => { + loadLanguages(languages => + loadTranslations(translations => + resolve(processData(languages, translations)) + ) + ); }); -} -function loadTranslations(callback) { - if (USE_FILES) { - return callback(JSON.parse(fs.readFileSync('./data/translations.json', 'utf8'))); - } - console.log('Loading translations...'); - const url = API_URL.replace(':project_id', PROJECT_ID) + '?' + - Object.keys(urlParams).map(param => param + '=' + urlParams[param]).join('&'); - https.get(url, res => { - if (res.statusCode !== 200) { - console.error(`API error ${res.statusCode}`); - return; + async function loadLanguages(callback) { + if (USE_FILES) { + return callback(JSON.parse(fs.readFileSync('./data/languages.json', 'utf8'))); } - console.log('Response received, reading...'); - const data = []; - res.on('data', chunk => data.push(chunk)); - res.on('end', () => { - console.log('Data received, parsing...'); - const json = Buffer.concat(data).toString('utf8'); - const parsed = JSON.parse(json); - fs.writeFileSync('data/translations.json', JSON.stringify(parsed, null, 2)); - callback(parsed); + console.log('Loading language names...'); + const url = API_URL_LANGUAGES.replace(':project_id', PROJECT_ID) + '?' + + Object.keys(urlParams).map(param => param + '=' + urlParams[param]).join('&'); + https.get(url, res => { + if (res.statusCode !== 200) { + console.error(`API error ${res.statusCode}`); + return; + } + console.log('Response received, reading...'); + const data = []; + res.on('data', chunk => data.push(chunk)); + res.on('end', () => { + console.log('Data received, parsing...'); + const json = Buffer.concat(data).toString('utf8'); + const parsed = JSON.parse(json); + fs.writeFileSync('data/languages.json', JSON.stringify(parsed, null, 2)); + callback(parsed); + }); }); - }); -} + } -function processData(languages, translations) { - let langCount = 0; - let skipCount = 0; - const enUs = translations['en-US'].translation; - const totalPhraseCount = Object.keys(enUs).length; - let errors = 0; - const meta = {}; - Object.keys(translations).forEach(lang => { - const languageTranslations = translations[lang].translation; - if (lang === 'en-US' || !languageTranslations) { - return; + async function loadTranslations (callback) { + if (USE_FILES) { + return callback(JSON.parse(fs.readFileSync('./data/translations.json', 'utf8'))); } - const langPhraseCount = Object.keys(languageTranslations).length; - const percentage = Math.round(langPhraseCount / totalPhraseCount * 100); - const included = percentage >= PHRASE_COUNT_THRESHOLD_PERCENT; - const action = included ? '\x1b[36mOK\x1b[0m' : '\x1b[35mSKIP\x1b[0m'; - console.log(`[${lang}] ${langPhraseCount} / ${totalPhraseCount} (${percentage}%) -> ${action}`); - if (included) { - langCount++; - for (const name of Object.keys(languageTranslations)) { - let text = languageTranslations[name]; - let enText = enUs[name]; - if (text instanceof Array) { - if (!(enText instanceof Array)) { - languageTranslations[name] = text.join('\n'); - console.error(`[${lang}] \x1b[31mERROR:ARRAY\x1b[0m ${name}`); - enText = [enText]; + console.log('Loading translations...'); + const url = API_URL.replace(':project_id', PROJECT_ID) + '?' + + Object.keys(urlParams).map(param => param + '=' + urlParams[param]).join('&'); + https.get(url, res => { + if (res.statusCode !== 200) { + console.error(`API error ${res.statusCode}`); + return; + } + console.log('Response received, reading...'); + const data = []; + res.on('data', chunk => data.push(chunk)); + res.on('end', () => { + console.log('Data received, parsing...'); + const json = Buffer.concat(data).toString('utf8'); + const parsed = JSON.parse(json); + fs.writeFileSync('data/translations.json', JSON.stringify(parsed, null, 2)); + callback(parsed); + }); + }); + } + + async function processData (languages, translations) { + let langCount = 0; + let skipCount = 0; + const enUs = translations['en-US'].translation; + const totalPhraseCount = Object.keys(enUs).length; + let errors = 0; + const meta = {}; + for (const lang of Object.keys(translations)) { + const languageTranslations = translations[lang].translation; + if (lang === 'en-US' || !languageTranslations) { + return; + } + const langPhraseCount = Object.keys(languageTranslations).length; + const percentage = Math.round(langPhraseCount / totalPhraseCount * 100); + const included = percentage >= PHRASE_COUNT_THRESHOLD_PERCENT; + const action = included ? '\x1b[36mOK\x1b[0m' : '\x1b[35mSKIP\x1b[0m'; + console.log(`[${lang}] ${langPhraseCount} / ${totalPhraseCount} (${percentage}%) -> ${action}`); + if (included) { + langCount++; + for (const name of Object.keys(languageTranslations)) { + let text = languageTranslations[name]; + let enText = enUs[name]; + if (text instanceof Array) { + if (!(enText instanceof Array)) { + languageTranslations[name] = text.join('\n'); + console.error(`[${lang}] \x1b[31mERROR:ARRAY\x1b[0m ${name}`); + enText = [enText]; + errors++; + } + text = text.join('\n'); + enText = enText.join('\n'); + } + if (!enText) { + console.warn(`[${lang}] SKIP ${name}`); + delete languageTranslations[name]; + continue; + } + const textMatches = text.match(/"/g); + const textMatchesCount = textMatches && textMatches.length || 0; + const enTextMatches = enText.match(/"/g); + const enTextMatchesCount = enTextMatches && enTextMatches.length || 0; + if (enTextMatchesCount !== textMatchesCount) { + const textHl = text.replace(/"/g, '\x1b[33m"\x1b[0m'); + console.warn(`[${lang}] \x1b[33mWARN:"\x1b[0m ${name}: ${textHl}`); + } + if (/[<>&]/.test(text)) { + const textHl = text.replace(/([<>&])/g, '\x1b[31m$1\x1b[0m'); + console.error(`[${lang}] \x1b[31mERROR:<>\x1b[0m ${name}: ${textHl}`); + errors++; + } + if (text.indexOf('{}') >= 0 && enText.indexOf('{}') < 0) { + const textHl = text.replace(/\{}/g, '\x1b[31m{}\x1b[0m'); + console.error(`[${lang}] \x1b[31mERROR:{}\x1b[0m ${name}: ${textHl}`); + errors++; + } + if (enText.indexOf('{}') >= 0 && text.indexOf('{}') < 0) { + const enTextHl = enText.replace(/\{}/g, '\x1b[31m{}\x1b[0m'); + console.error(`[${lang}] \x1b[31mERROR:NO{}\x1b[0m ${name}: ${text} <--> ${enTextHl}`); errors++; } - text = text.join('\n'); - enText = enText.join('\n'); } - if (!enText) { - console.warn(`[${lang}] SKIP ${name}`); - delete languageTranslations[name]; - continue; - } - const textMatches = text.match(/"/g); - const textMatchesCount = textMatches && textMatches.length || 0; - const enTextMatches = enText.match(/"/g); - const enTextMatchesCount = enTextMatches && enTextMatches.length || 0; - if (enTextMatchesCount !== textMatchesCount) { - const textHl = text.replace(/"/g, '\x1b[33m"\x1b[0m'); - console.warn(`[${lang}] \x1b[33mWARN:"\x1b[0m ${name}: ${textHl}`); - } - if (/[<>&]/.test(text)) { - const textHl = text.replace(/([<>&])/g, '\x1b[31m$1\x1b[0m'); - console.error(`[${lang}] \x1b[31mERROR:<>\x1b[0m ${name}: ${textHl}`); - errors++; - } - if (text.indexOf('{}') >= 0 && enText.indexOf('{}') < 0) { - const textHl = text.replace(/\{}/g, '\x1b[31m{}\x1b[0m'); - console.error(`[${lang}] \x1b[31mERROR:{}\x1b[0m ${name}: ${textHl}`); - errors++; - } - if (enText.indexOf('{}') >= 0 && text.indexOf('{}') < 0) { - const enTextHl = enText.replace(/\{}/g, '\x1b[31m{}\x1b[0m'); - console.error(`[${lang}] \x1b[31mERROR:NO{}\x1b[0m ${name}: ${text} <--> ${enTextHl}`); - errors++; - } - } - const languageJson = JSON.stringify(languageTranslations, null, 2); - const sign = crypto.createSign('RSA-SHA256'); - sign.write(Buffer.from(languageJson)); - sign.end(); - const signature = sign.sign(privateKey).toString('base64'); + const languageJson = JSON.stringify(languageTranslations, null, 2); - const langInfo = languages.data.filter(x => x.code === lang)[0]; - const region = (defaultCountries[langInfo.region] || langInfo.region).toLowerCase(); - const langName = langInfo.locale === region ? langInfo.local_name.replace(/\s*\(.*\)/, '') : langInfo.local_name; - const langNameEn = langInfo.locale === region ? langInfo.english_name.replace(/\s*\(.*\)/, '') : langInfo.english_name; - meta[lang] = { name: langName, nameEn: langNameEn, count: langPhraseCount }; + const data = Buffer.from(languageJson); + const signature = await sign(data).catch(e => { + console.log('Sign error', e); + process.exit(1); + }); - if (fs.existsSync(`docs/translations/${lang}`)) { - const manifest = JSON.parse(fs.readFileSync(`docs/translations/${lang}/manifest.json`, 'utf8')); - if (manifest.resources.loc !== signature) { - const parts = manifest.version.split('.'); - manifest.version = parts[0] + '.' + (+parts[1] + 1) + '.0'; - manifest.resources.loc = signature; - fs.writeFileSync(`docs/translations/${lang}/manifest.json`, JSON.stringify(manifest, null, 2)); + const langInfo = languages.data.filter(x => x.code === lang)[0]; + const region = (defaultCountries[langInfo.region] || langInfo.region).toLowerCase(); + const langName = langInfo.locale === region ? langInfo.local_name.replace(/\s*\(.*\)/, '') : langInfo.local_name; + const langNameEn = langInfo.locale === region ? langInfo.english_name.replace(/\s*\(.*\)/, '') : langInfo.english_name; + meta[lang] = {name: langName, nameEn: langNameEn, count: langPhraseCount}; + + if (fs.existsSync(`docs/translations/${lang}`)) { + const manifest = JSON.parse(fs.readFileSync(`docs/translations/${lang}/manifest.json`, 'utf8')); + if (manifest.resources.loc !== signature) { + const parts = manifest.version.split('.'); + manifest.version = parts[0] + '.' + (+parts[1] + 1) + '.0'; + manifest.resources.loc = signature; + fs.writeFileSync(`docs/translations/${lang}/manifest.json`, JSON.stringify(manifest, null, 2)); + fs.writeFileSync(`docs/translations/${lang}/${lang}.json`, languageJson); + } + meta[lang].version = manifest.version; + } else { + fs.mkdirSync(`docs/translations/${lang}`); + fs.writeFileSync(`docs/translations/${lang}/manifest.json`, pluginManifest + .replace(/{lang}/g, lang) + .replace(/{name_ascii}/g, langNameEn.replace(/\W+/g, '-').toLowerCase()) + .replace(/{name_en}/g, langNameEn) + .replace(/{name}/g, langName) + .replace(/{signature}/g, signature) + .replace(/{key}/g, publicKey) + ); fs.writeFileSync(`docs/translations/${lang}/${lang}.json`, languageJson); + fs.writeFileSync(`docs/translations/${lang}/index.html`, pluginIndexPage + .replace(/{lang}/g, lang) + .replace(/{name}/g, langName) + ); + meta[lang].version = '1.0.0'; } - meta[lang].version = manifest.version; } else { - fs.mkdirSync(`docs/translations/${lang}`); - fs.writeFileSync(`docs/translations/${lang}/manifest.json`, pluginManifest - .replace(/{lang}/g, lang) - .replace(/{name_ascii}/g, langNameEn.replace(/\W+/g, '-').toLowerCase()) - .replace(/{name_en}/g, langNameEn) - .replace(/{name}/g, langName) - .replace(/{signature}/g, signature) - .replace(/{key}/g, publicKey) - ); - fs.writeFileSync(`docs/translations/${lang}/${lang}.json`, languageJson); - fs.writeFileSync(`docs/translations/${lang}/index.html`, pluginIndexPage - .replace(/{lang}/g, lang) - .replace(/{name}/g, langName) - ); - meta[lang].version = '1.0.0'; + skipCount++; } - } else { - skipCount++; } - }); - console.log(`Done: ${langCount} written, ${skipCount} skipped, ${errors} errors`); - if (errors) { - console.error('There were errors, please check the output.'); - process.exit(1); + console.log(`Done: ${langCount} written, ${skipCount} skipped, ${errors} errors`); + if (errors) { + console.error('There were errors, please check the output.'); + process.exit(1); + } + fs.writeFileSync('docs/translations/meta.json', JSON.stringify(meta, null, 2)); } - fs.writeFileSync('docs/translations/meta.json', JSON.stringify(meta, null, 2)); +}; + +if (require.main === module) { + module.exports(); } diff --git a/scripts/sign.js b/scripts/sign.js new file mode 100644 index 0000000..7da5c5b --- /dev/null +++ b/scripts/sign.js @@ -0,0 +1,25 @@ +const fs = require('fs'); +const signer = require('pkcs15-smartcard-sign'); +const keychain = require('keychain'); + +const verifyKey = fs.readFileSync('keys/public-key.pem'); + +function getPin() { + if (getPin.pin) { + return Promise.resolve(getPin.pin); + } + return new Promise((resolve, reject) => { + keychain.getPassword({ account: 'keeweb', service: 'keeweb.pin', type: 'generic' }, (err, pass) => { + if (err) { + reject(err); + } else { + getPin.pin = pass; + resolve(pass); + } + }); + }); +} + +module.exports = function sign(data) { + return getPin().then(pin => signer.sign({ data, verifyKey, pin }).then(data => data.toString('base64'))); +}; diff --git a/scripts/start.js b/scripts/start.js new file mode 100644 index 0000000..53dbb0a --- /dev/null +++ b/scripts/start.js @@ -0,0 +1,4 @@ +(async function() { + await require('./download-translations')(); + require('./update-plugins'); +})(); diff --git a/scripts/update-plugins.js b/scripts/update-plugins.js index 1cdd4da..57f7efe 100644 --- a/scripts/update-plugins.js +++ b/scripts/update-plugins.js @@ -1,14 +1,13 @@ /* eslint-disable no-console */ const fs = require('fs'); -const crypto = require('crypto'); +const sign = require('./sign'); console.log('Welcome to plugins updater'); console.log('Loading...'); const data = JSON.parse(fs.readFileSync('docs/plugins.json', 'utf8')); -const privateKey = fs.readFileSync('keys/private-key.pem', 'binary'); data.signature = ''; data.date = ''; @@ -54,14 +53,13 @@ data.date = new Date().toISOString(); console.log('Signing...'); -const dataToSign = JSON.stringify(data, null, 2); +const dataToSign = Buffer.from(JSON.stringify(data, null, 2)); -const sign = crypto.createSign('RSA-SHA256'); -sign.write(new Buffer(dataToSign)); -sign.end(); - -data.signature = sign.sign(privateKey).toString('base64'); - -fs.writeFileSync('docs/plugins.json', JSON.stringify(data, null, 2)); - -console.log('Done'); +sign(dataToSign).then(signature => { + data.signature = signature.toString('base64'); + fs.writeFileSync('docs/plugins.json', JSON.stringify(data, null, 2)); + console.log('Done'); +}).catch(err => { + console.error('Sign error', err); + process.exit(1); +});