UNPKG

@buttercup/credentials

Version:

Encrypted credentials handler for Buttercup

266 lines (242 loc) 7.3 kB
const { getSignature } = require("@buttercup/signing"); const hash = require("hash.js"); const { getDecryptFn, getEncryptFn } = require("./appEnv.js"); /** * The credentials type key * @private * @type {String} */ const CREDENTIALS_ATTR = "@@bcup-role"; /** * The signature of encrypted credentials * @private * @type {string} */ const SIGNING_KEY = getSignature() + "creds.v2."; /** * The old signing signature * @private * @type {string} */ const SIGNING_KEY_OLD = getSignature() + "creds."; /** * Sign encrypted content * @see SIGNING_KEY * @private * @param {String} content The encrypted text * @returns {String} The signed key */ function signEncryptedContent(content) { return `${SIGNING_KEY}${content}`; } /** * Remove the signature from encrypted content * @private * @param {String} content The encrypted text * @returns {String} The unsigned encrypted key * @throws {Error} Throws if no SIGNING_KEY is detected * @see SIGNING_KEY */ function unsignEncryptedContent(content) { const newIndex = content.indexOf(SIGNING_KEY); const oldIndex = content.indexOf(SIGNING_KEY_OLD); if (newIndex === -1 && oldIndex === -1) { throw new Error("Invalid credentials content (unknown signature)"); } return newIndex >= 0 ? content.substr(SIGNING_KEY.length) : content.substr(SIGNING_KEY_OLD.length); } /** * Credentials instance */ class Credentials { /** * Create a new instance from an insecure string * @param {String} content The insecure string representation * @memberof Credentials * @returns {Credentials} A new instance * @static */ static fromInsecureString(content) { const data = JSON.parse(content); return new Credentials(data); } /** * Create a new instance from a password * @param {String} password The password * @returns {Credentials} A new instance * @memberof Credentials * @static */ static fromPassword(password) { return new Credentials({ type: "password", password }); } /** * Create a new instance from a secure string * @param {String} content Encrypted content * @param {String} password The password for decryption * @returns {Promise.<Credentials>} A promise that resolves with the new instance * @static * @memberof Credentials */ static fromSecureString(content, password) { const decrypt = getDecryptFn(); return decrypt(unsignEncryptedContent(content), password) .then(decryptedContent => JSON.parse(decryptedContent)) .then(credentialsData => Array.isArray(credentialsData) ? new Credentials( Object.assign({}, credentialsData[1], { type: credentialsData[0] }) ) : new Credentials(credentialsData) ); } /** * Check if an item is a Credentials instance * @param {Object} target The target to check * @returns {Boolean} True if a credentials instance * @memberof Credentials * @static */ static isCredentials(target) { return ( typeof target === "object" && target !== null && target[CREDENTIALS_ATTR] === "credentials" ); } /** * Check if a string is a secure string * @param {String} str The string to check * @returns {Boolean} True if the string is a secure string * @static * @memberof Credentials */ static isSecureString(str) { try { unsignEncryptedContent(str); return true; } catch (err) { return false; } } /** * Constructor for the Credentials class * @param {String|Object} typeOrData The type (string), or an object representing * the Credentials data */ constructor(typeOrData) { this[CREDENTIALS_ATTR] = "credentials"; this.data = typeof typeOrData === "string" ? { type: typeOrData } : Object.assign( { type: "" }, typeOrData ); } /** * The password * @type {String|undefined} * @memberof Credentials */ get password() { return this.data.password; } /** * The credentials type * @type {String} * @memberof Credentials * @readonly */ get type() { return this.data.type; } /** * The username * @type {String|undefined} * @memberof Credentials */ get username() { return this.data.username; } set password(newPassword) { this.data.password = newPassword; } set username(newUsername) { this.data.username = newUsername; } /** * Get A unique ID of the datasource based on its data * @returns {String} A hash of the data * @memberof Credentials */ getID() { return hash .sha256() .update(JSON.stringify(this.data)) .digest("hex"); } /** * Get a value from the credentials * @param {String} property The property to fetch * @returns {*|undefined} Returns the value if found, or undefined * @memberof Credentials */ getValue(property) { return this.data[property]; } /** * Get a value, or fail if it doesn't exist or isn't set * @throws {Error} Throws if the value is undefined * @param {String} property The property to fetch * @returns {*} The value (not undefined) * @memberof Credentials * @instance */ getValueOrFail(property) { const value = this.getValue(property); if (typeof value === "undefined") { throw new Error(`Failed retrieving required credentials property: ${property}`); } return value; } /** * Set a value for a property * @param {String} property The property to set * @param {*} value The value to set for the property * @returns {Credentials} Returns self, for chaining * @memberof Credentials */ setValue(property, value) { this.data[property] = value; return this; } /** * Convert the credentials to an insecure string * @returns {String} The string-encoded credentials * @memberof Credentials */ toInsecureString() { return JSON.stringify(this.data); } /** * Convert the credentials to an encrypted string, for storage * @param {string} masterPassword The password for encrypting * @returns {Promise} A promise that resolves with the encrypted credentials * @see signEncryptedContent * @throws {Error} Rejects when masterPassword is not a string * @memberof Credentials */ toSecureString(masterPassword) { if (typeof masterPassword !== "string") { return Promise.reject(new Error("Master password must be a string")); } const encrypt = getEncryptFn(); return encrypt(this.toInsecureString(), masterPassword).then(signEncryptedContent); } } module.exports = Credentials;