evergreen/distribution/client/src/lib/registration.ts

302 lines
7.0 KiB
TypeScript

/*
* The registration module is responsible for managing the initial registration
* of the evergreen-client with the backend services layer
*/
import ecc from 'elliptic';
import * as logger from 'winston';
import path from 'path';
import mkdirp from 'mkdirp'
import fs from 'fs';
import Storage from './storage';
require('./rand-patch');
export interface FileOptions {
encoding?: string,
flag?: string,
};
export default class Registration {
protected readonly app : any;
protected readonly options : Map<string, any>;
protected readonly curve : string;
protected privateKey : string;
protected publicKey : string;
protected fileOptions : FileOptions;
public uuid : string;
public token : string;
constructor(app) {
this.app = app;
this.uuid = null;
this.publicKey = null;
this.privateKey = null;
this.fileOptions = { encoding: 'utf8' };
this.curve = 'secp256k1';
}
isRegistered() {
if (!this.hasKeys()) {
return false;
}
try {
fs.statSync(this.uuidPath());
return true;
} catch (err) {
if (err.code == 'ENOENT') {
return false;
}
throw err;
}
}
/*
* Execute the registration process if the instance has not been registered,
* otherwise just resolve the Promise
*
* @return Promise
*/
async register() {
const self = this;
return new Promise((resolve, reject) => {
const api = self.app.service('registration');
logger.info('Checking registration status..');
if (self.hasKeys() && self.hasUUID()) {
logger.info('We have keys and a UUID already');
self.loadKeysSync();
self.loadUUIDSync();
return self.login().then(res => resolve(
{
result: res,
created: false,
}));
} else {
if (!self.generateKeys()) {
return reject('Failed to generate keys');
}
if (!self.saveKeysSync()) {
return reject('Failed to save keys to disk');
}
logger.info('Creating registration..');
api.create({
pubKey: self.getPublicKey(),
curve: self.curve
}).then((res) => {
logger.info('Registration create:', res);
self.uuid = res.uuid;
if (!self.saveUUIDSync()) {
reject('Failed to save UUID!');
} else {
return self.login().then(res => resolve({
result: res,
created: true,
}));
}
}).catch((res) => {
logger.error('Failed to register:', res);
reject(res);
});
}
});
}
/*
* Execute the login process once the registration has completed.
*
* @return Promise
*/
async login() {
logger.info('Attempting to log in');
const ec = new ecc.ec(this.curve);
const key = ec.keyFromPrivate(this.privateKey, 'hex');
const signature = key.sign(this.uuid);
const response = await this.app.authenticate({
strategy: 'local',
uuid: this.uuid,
signature: signature
});
logger.info('Logged in and received JWT:', response.accessToken);
}
/*
* Generate the keys necessary for this instance
*
* @return Boolean
*/
generateKeys() {
const ec = new ecc.ec(this.curve);
const privkey = ec.genKeyPair();
this.publicKey = privkey.getPublic('hex');
this.privateKey = privkey.getPrivate('hex');
/* If we have a private key, that's good enough! */
if (this.privateKey) {
return true;
}
return false;
}
/*
* Return the base64 encoded public key associated with this instance
*
* Will return null if no public key has yet been generated
*
* @return string
*/
getPublicKey() {
return this.publicKey;
}
/*
* Persist the UUID generated by the call to the registration service
*
* @return Boolean on success
*/
saveUUIDSync() {
if (this.uuid != null) {
const config = {
uuid: this.uuid,
};
logger.info('Saving uuid to disk at', this.uuidPath());
fs.writeFileSync(
this.uuidPath(),
JSON.stringify(config),
this.fileOptions);
return true;
}
return false;
}
loadUUIDSync() {
const config = JSON.parse(fs.readFileSync(this.uuidPath(), this.fileOptions) as string);
this.uuid = config.uuid;
return (!!this.uuid);
}
/*
* Determine whether the uuid.json is already present. Indicating a
* successful registration has happened at some point in the past.
*
* @return Boolean
*/
hasUUID() {
try {
fs.statSync(this.uuidPath());
return true;
} catch (err) {
if (err.code == 'ENOENT') {
return false;
} else {
throw err;
}
}
}
/*
* Persist generated keys
*
* @return Boolean on success
*/
saveKeysSync() {
if ((!this.publicKey) || (!this.privateKey)) {
logger.warn('saveKeysSync() called without a private or public key on Registration');
return false;
}
const publicKeyPath = this.publicKeyPath();
const privateKeyPath = this.privateKeyPath();
logger.debug('Writing public key to', publicKeyPath);
fs.writeFileSync(publicKeyPath, this.publicKey, this.fileOptions);
logger.debug('Writing private key to', privateKeyPath);
fs.writeFileSync(privateKeyPath, this.privateKey, this.fileOptions);
return true;
}
/*
* Load generated keys from disk assuming they are present
*
* @return Boolean on successful load of keys
*/
loadKeysSync() {
if ((this.publicKey != null) || (this.privateKey != null)) {
logger.warn('loadKeysSync() called despite public/private keys already having been loaded', this.publicKey);
return false;
}
if (!this.hasKeys()) {
return false;
}
this.publicKey = fs.readFileSync(this.publicKeyPath(), this.fileOptions) as string;
this.privateKey = fs.readFileSync(this.privateKeyPath(), this.fileOptions) as string;
return true;
}
/*
* Determine whether the keys are already present on the filesystem
*
* @return Boolean
*/
hasKeys() {
try {
fs.statSync(this.publicKeyPath());
return true;
} catch (err) {
if (err.code == 'ENOENT') {
return false;
} else {
throw err;
}
}
}
/* Return the directory where registration keys should be stored
*
* @return string
*/
keyPath() {
const keys = path.join(Storage.homeDirectory(), 'keys');
/* Only bother making the directory if it doesn't already exist */
try {
fs.statSync(keys);
} catch (err) {
if (err.code == 'ENOENT') {
mkdirp.sync(keys);
} else {
throw err;
}
}
return keys;
}
/* Return the absolute path to the evergreen public key
*
* This public key is safe to be shared with external services
*
* @return string
*/
publicKeyPath() {
return path.join(this.keyPath(), 'evergreen.pub');
}
privateKeyPath() {
return path.join(this.keyPath(), 'evergreen-private-key');
}
uuidPath() {
return path.join(this.keyPath(), 'uuid.json');
}
}