Skip to content

Commit

Permalink
Merge pull request #16 from goodwid/master
Browse files Browse the repository at this point in the history
  • Loading branch information
lastguest authored Jun 17, 2019
2 parents a10c2ca + a7653d2 commit 496426e
Show file tree
Hide file tree
Showing 6 changed files with 1,086 additions and 77 deletions.
157 changes: 93 additions & 64 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,49 @@
'use strict'
const os = require('os');
const path = require('path')

const { encode, decode, legacyDecode } = require('./lib/crypto')
const padText = require('./lib/toThirtytwo');
const fs = require('graceful-fs')
const writeFileAtomic = require('write-file-atomic')
const mkdirp = require('mkdirp')
const yaml = require('js-yaml')

const defaultOptions = {
key: null,
file: null,
encrypt: true,
format: 'json',
}

function Preferences (id, defs, options) {
options = options || {
key: null,
file: null,
encrypt: true,
format: 'json'
}
var self = this
var identifier = id.replace(/[\/\?<>\\:\*\|" :]/g, '.').replace(/\.+/g, '.')
var path = require('path')
var homedir = require('os-homedir')()
var dirpath = options.file ? path.dirname(options.file) : path.join(homedir, '.config', 'preferences')
var filepath = options.file ? path.join(dirpath, path.basename(options.file)) : path.join(dirpath, identifier + '.pref')
var encrypt = options.encrypt;
var format = options.format;
var fs = require('graceful-fs')
var writeFileAtomic = require('write-file-atomic')
var mkdirp = require('mkdirp')
var crypto = require('crypto')
var yaml = require('js-yaml')
var password = (function () {
var key = options.key || path.join(homedir, '.ssh', 'id_rsa')
function Preferences (id, defs, options = defaultOptions) {
const homedir = os.homedir()
// eslint-disable-next-line no-useless-escape
const identifier = id.replace(/[\/\?<>\\:\*\|" :]/g, '.').replace(/\.+/g, '.')
const dirpath = options.file ? path.dirname(options.file) : path.join(homedir, '.config', 'preferences')
const filepath = options.file ? path.join(dirpath, path.basename(options.file)) : path.join(dirpath, identifier + '.pref')
const { encrypt, format } = options

let savePristine = false
let savedData = null
let convertFromLegacy = false

const key = (function () {
let key;
if (options.key) key = padText(options.key, identifier)
else key = path.join(homedir, '.ssh', 'id_rsa')
try {
// Use private SSH key or...
return fs.readFileSync(key).toString('utf8').slice(0, 32)
} catch (e) {
// ...fallback to an id dependant password
return padText('PREFS', identifier)
}
})()

// retaining old code in order to be able to decode exisiting preferences
const legacyPassword = (function () {
const key = options.key || path.join(homedir, '.ssh', 'id_rsa')
try {
// Use private SSH key or...
return fs.readFileSync(key).toString('utf8')
Expand All @@ -30,82 +52,89 @@ function Preferences (id, defs, options) {
return 'PREFS-' + identifier
}
})()
var savePristine = false
var savedData = null

function encode (text) {
if (!encrypt) return text;
var cipher = crypto.createCipher('aes128', password)
return cipher.update(new Buffer(text).toString('utf8'), 'utf8', 'hex') + cipher.final('hex')
}

function decode (text) {
if (!encrypt) return text;
var decipher = crypto.createDecipher('aes128', password)
return decipher.update(String(text), 'hex', 'utf8') + decipher.final('utf8')
}

function serialize (data) {
if (format == 'yaml') return yaml.safeDump(data)
return JSON.stringify(data)
}

function deserialize (text) {
if (format == 'yaml') return yaml.safeLoad(text)
return JSON.parse(text)
}

function save () {
var payload = encode(String(serialize(self) || '{}'))
const serialize = (data) => {
return format == 'yaml'
? yaml.safeDump(data)
: JSON.stringify(data)
};

const deserialize = (text) => {
return format == 'yaml'
? yaml.safeLoad(text)
: JSON.parse(text)
};

const save = () => {
let payload = String(serialize(this) || '{}')
if (encrypt) payload = encode(payload, key);
try {
mkdirp.sync(dirpath, parseInt('0700', 8))
writeFileAtomic.sync(filepath, payload, {
mode: parseInt('0600', 8)
mode: parseInt('0600', 8),
})
} catch (err) {}
} catch (err) {
// eslint-disable-next-line no-console
console.error(err)
}
}

function clear () {
for (var o in self) delete self[o]
const clear = () => {
for (let o in this) delete this[o]
save()
}

if (Object.defineProperty) {
Object.defineProperty(self, "save", {
Object.defineProperty(this, 'save', {
enumerable: false,
writable: false,
value: save
value: save,
});

Object.defineProperty(self, "clear", {
Object.defineProperty(this, 'clear', {
enumerable: false,
writable: false,
value: clear
value: clear,
});
}

// Try to read and decode preferences saved on disc
let fileContents = '';
try {
// Try to read and decode preferences saved on disc
savedData = deserialize(decode(fs.readFileSync(filepath, 'utf8')))
} catch (err) {
fileContents = fs.readFileSync(filepath, 'utf8')
}
catch (err) {
// Read error (maybe file doesn't exist) so update with defaults
savedData = defs || {}
savePristine = true
}

if (fileContents) {
let results = fileContents
if (encrypt) {
try {
results = decode(fileContents, key)
}
catch (e) {
results = legacyDecode(fileContents, legacyPassword)
convertFromLegacy = true;
}
}
savedData = deserialize(results)
}

// Clone object
for (var o in savedData) self[o] = savedData[o]
for (let o in savedData) this[o] = savedData[o]

// Config file was empty, save default values
savePristine && save()
savePristine || convertFromLegacy && save()

// Save all on program exit
process.on('exit', save)

// If supported observe object for saving on modify
if (Object.observe) Object.observe(self, save)
if (Object.observe) Object.observe(this, save)

return self
return this
}

module.exports = Preferences
29 changes: 29 additions & 0 deletions lib/crypto.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const crypto = require('crypto')

const algorithm = 'aes-256-ctr'
const inputEncoding = 'utf8'
const outputEncoding = 'hex'

function encode (text, key) {
const iv = crypto.randomBytes(8).toString('hex')
const cipher = crypto.createCipheriv(algorithm, key, iv)
const crypted = cipher.update(text, inputEncoding, outputEncoding) + cipher.final(outputEncoding)
return `${iv.toString('hex')}:${crypted.toString()}`
}

function decode (text, key) {
const [ivstring, encrypted] = text.split(':')
const decipher = crypto.createDecipheriv(algorithm, key, ivstring)
return decipher.update(encrypted, outputEncoding, inputEncoding) + decipher.final(inputEncoding)
}

function legacyDecode (text, password) {
const decipher = crypto.createDecipher('aes128', password)
return decipher.update(String(text), outputEncoding, inputEncoding) + decipher.final(inputEncoding)
}

module.exports = {
encode,
decode,
legacyDecode,
};
5 changes: 5 additions & 0 deletions lib/toThirtytwo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = (text, padding) => {
if (text.length === 32) return text
if (text.length < 32) return text.padEnd(32, padding);
if (text.length > 32) return text.slice(0, 32);
};
Loading

0 comments on commit 496426e

Please sign in to comment.