302 lines
7.0 KiB
TypeScript
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');
|
|
}
|
|
}
|