@axway/amplify-sdk
Version:
Axway Amplify SDK for Node.js
249 lines (227 loc) • 6.38 kB
JavaScript
import crypto from 'crypto';
import errors from '../errors.js';
import fs from 'fs-extra';
import path from 'path';
import snooplogg from 'snooplogg';
import TokenStore from './token-store.js';
import { writeFileSync } from '@axway/amplify-utils';
import 'pluralize';
const { log, warn } = snooplogg('amplify-auth:file-store');
const { highlight } = snooplogg.styles;
/**
* The algorithm for encrypting and decrypting.
* @type {String}
*/
const algorithm = 'aes-128-cbc';
/**
* The initialization vector for encrypting and decrypting.
* @type {Buffer}
*/
const iv = new Buffer.alloc(16);
/**
* A file-based token store.
*/
class FileStore extends TokenStore {
/**
* The name of the token store file.
* @type {String}
*/
filename = '.tokenstore.v2';
/**
* Initializes the file store.
*
* @param {Object} opts - Various options.
* @param {String} opts.homeDir - The path to the home directory containing.
* @param {String} [opts.tokenStoreDir] - DEPRECATED. The path to the file-based token store.
* Use `opts.homeDir` instead.
* @access public
*/
constructor(opts = {}) {
super(opts);
let { homeDir, tokenStoreDir } = opts;
if (!homeDir || typeof homeDir !== 'string') {
if (tokenStoreDir && typeof tokenStoreDir === 'string') {
homeDir = tokenStoreDir;
} else {
throw errors.MISSING_REQUIRED_PARAMETER('Token store requires a home directory');
}
}
this.homeDir = path.resolve(homeDir);
this.tokenStoreDir = path.join(this.homeDir, 'axway-cli');
this.tokenStoreFile = path.join(this.tokenStoreDir, this.filename);
}
/**
* Removes all tokens.
*
* @param {String} [baseUrl] - The base URL used to filter accounts.
* @returns {Promise<Array>}
* @access public
*/
async clear(baseUrl) {
const { entries, removed } = await super._clear(baseUrl);
if (entries.length) {
await this.save(entries);
} else {
await this.remove();
}
return removed;
}
/**
* Decodes the supplied string into an object.
*
* @param {String} str - The string to decode into an object.
* @returns {Array}
* @access private
*/
async decode(str) {
let decipher;
try {
decipher = crypto.createDecipheriv(algorithm, await this.getKey(), iv);
} catch (e) {
e.amplifyCode = 'ERR_BAD_KEY';
throw e;
}
try {
return JSON.parse(decipher.update(str, 'hex', 'utf8') + decipher.final('utf8'));
} catch (e) {
// it's possible that there was a tokenstore on disk that was encrypted with an old key
// that no longer exists and the new key can't decode it, so just nuke the tokenstore
await this.remove();
throw e;
}
}
/**
* Deletes a token from the store.
*
* @param {String|Array.<String>} accounts - The account name(s) to delete.
* @param {String} [baseUrl] - The base URL used to filter accounts.
* @returns {Promise<Array>}
* @access public
*/
async delete(accounts, baseUrl) {
const { entries, removed } = await super._delete(accounts, baseUrl);
if (entries.length) {
await this.save(entries);
} else {
await this.remove();
}
return removed;
}
/**
* Encodes an object into a string.
*
* @param {Object} data - The object to encode into a string.
* @returns {String}
* @access private
*/
async encode(data) {
let cipher;
try {
cipher = crypto.createCipheriv(algorithm, await this.getKey(), iv);
} catch (e) {
e.amplifyCode = 'ERR_BAD_KEY';
throw e;
}
return cipher.update(JSON.stringify(data), 'utf8', 'hex') + cipher.final('hex');
}
/**
* Gets the decipher key or generates a new one if it doesn't exist.
*
* @returns {Buffer}
* @access private
*/
async getKey() {
if (!this._key) {
Object.defineProperty(this, '_key', {
value: 'd4be0906bc9fae40'
});
}
return this._key;
}
/**
* Retreives all tokens from the store.
*
* @returns {Promise<Array>} Resolves an array of tokens.
* @access public
*/
async list() {
if (fs.existsSync(this.tokenStoreFile)) {
try {
log(`Reading ${highlight(this.tokenStoreFile)}`);
const entries = await this.decode(fs.readFileSync(this.tokenStoreFile, 'utf8'));
const validEntries = this.purge(entries);
if (validEntries.length < entries.length) {
// something was purged, update the token store
await this.save(validEntries);
}
return validEntries;
} catch (e) {
// the decode failed (or there was a keytar problem), so just log a warning and
// return an empty result
warn(e);
}
}
return [];
}
/**
* Removes both v1 and v2 token store files.
*
* @returns {Promise}
* @access private
*/
async remove() {
for (let ver = 1; ver <= 2; ver++) {
const file = ver === 2 ? this.tokenStoreFile : path.join(this.homeDir, this.filename.replace(/\.v2$/, ''));
log(`Removing ${highlight(file)}`);
await fs.remove(file);
}
}
/**
* Saves the entires to both v1 and v2 token store files.
*
* @param {Array} entries - The list of entries.
* @returns {Promise}
* @access private
*/
async save(entries) {
// Auth SDK v2 changed the structure of the data in the token store, but some dependencies
// still rely on Auth SDK v1's structure. We can't change force them to update and we can't
// change the structure, so we have to write two versions of the token store. v2 is written
// as is, but for v1, the data is translated into Auth SDK v1's structure.
for (let ver = 1; ver <= 2; ver++) {
const data = await this.encode(ver === 2 ? entries : entries.map(acct => {
const v1 = {
...acct,
...acct.auth,
org: {
...acct.org,
org_id: acct.org?.id
},
orgs: !Array.isArray(acct.orgs) ? [] : acct.orgs.map(org => {
const o = { ...org, org_id: org.id };
delete o.id;
return o;
})
};
delete v1.auth;
delete v1.org.id;
return v1;
}));
const file = ver === 2 ? this.tokenStoreFile : path.join(this.homeDir, this.filename.replace(/\.v2$/, ''));
log(`Writing ${highlight(file)}`);
writeFileSync(file, data, { mode: 384 /* 600 */ });
}
}
/**
* Saves account credentials. If exists, the old one is deleted.
*
* @param {Object} obj - The token data.
* @returns {Promise}
* @access public
*/
async set(obj) {
await this.save(await super._set(obj));
}
}
export { FileStore as default };
//# sourceMappingURL=file-store.js.map