dwnpm
Version:
Decentralized Registry Package Manager (DRPM) helps developers publish, install, find and manage Decentralized Packages (DPKs) published to Decentralized Web Nodes (DWNs). DRPM does this by looking up a Decentralized Identifier (DID) to find its DID docum
252 lines (216 loc) • 8.75 kB
text/typescript
import * as Inquirer from '@inquirer/prompts';
import { createCipheriv, createDecipheriv, pbkdf2Sync, randomBytes } from 'crypto';
import { readFileSync, writeFileSync } from 'fs';
import { ensureDir } from 'fs-extra';
import { readFile, writeFile } from 'fs/promises';
import { DEFAULT_PASSWORD, DEFAULT_PROFILE, DRPM_HOME, DRPM_PROFILE } from '../config.js';
import { Setup } from '../lib/setup.js';
import formatter from '../utils/formatter.js';
import { Logger } from '../utils/logger.js';
import { createPassword, secureProfile, secureProfileContext, stringifier } from '../utils/misc.js';
import { ProfileJson } from '../utils/types.js';
import { Context } from './context.js';
type ProfileBackup = {
data: string;
salt: string;
iv: string;
authTag: string;
}
export class Profile {
static json: ProfileJson;
context: Context;
constructor(name?: string) {
if(!this.exists()) {
Logger.error('ProfileError: No profile found.');
this.template();
}
if(!this.isSetup()) {
Logger.error('ProfileError: Setup not completed.');
// TODO: run setup? await Setup.run();
// process.exit(1);
}
name ??= Profile.loadStaticSync()?.name;
this.context = new Context(name, Profile.json?.[name]);
}
isSetup(): boolean {
return Setup.isDone();
}
static loadStaticSync(): ProfileJson {
const profile = readFileSync(DRPM_PROFILE, 'utf8');
this.json = JSON.parse(profile);
return this.json;
}
template(): void {
writeFileSync(DRPM_PROFILE, stringifier(DEFAULT_PROFILE));
}
// Helper function to validate profile data
valid(data?: ProfileJson): boolean | ProfileJson {
if(!data) {
Logger.error('ProfileError: No profile data found.');
return false;
}
const { did, password, dwnEndpoints, web5DataPath } = data?.[data?.name] ?? {};
// Check for empty or invalid DID
if (!did || did.trim() === '') {
Logger.error('ProfileError: DID cannot be blank.');
return false;
}
// Check for empty or default password
if (!password || password === DEFAULT_PASSWORD) {
Logger.error('ProfileError: Password cannot be blank or set to the default password.');
return false;
}
// Check that dwnEndpoint has at least one valid entry
if (!Array.isArray(dwnEndpoints) || dwnEndpoints.length === 0) {
Logger.error('ProfileError: DWN endpoint cannot be empty.');
return false;
}
// Check if dataPath is empty or invalid
if (!web5DataPath || web5DataPath.trim() === '') {
Logger.error('ProfileError: Web5 Data Path cannot be empty.');
return false;
}
// If no errors, return the profile
return true;
}
// Helper function to check if a profile exists
exists(profile?: ProfileJson, method?: string): boolean | ProfileJson {
try {
profile ??= Profile.loadStaticSync();
if(!profile) return false;
const data = profile[profile.name ?? method];
if(!data) return false;
return profile;
} catch (error: any) {
Logger.error('ProfileCommand: Failed to load profile', error);
return false;
}
}
// Helper function to load existing profile or create a new one
async load(): Promise<ProfileJson> {
const profile = await readFile(DRPM_PROFILE, 'utf8');
Profile.json = JSON.parse(profile);
return Profile.json;
}
static async staticSave(): Promise<void> {
const profile = Profile.json ?? this.loadStaticSync();
await writeFile(DRPM_PROFILE, stringifier(profile), 'utf8');
Logger.log('Saved profile.json', secureProfile(Profile.json));
}
async save(): Promise<void> {
const profile = Profile.json ?? this.load();
await writeFile(DRPM_PROFILE, stringifier(profile), 'utf8');
Logger.log('Saved profile.json', secureProfile(Profile.json));
}
async read(options: { text?: boolean }): Promise<void> {
Logger.plain('DRPM Profile:', options.text ?? false ? Profile.json : secureProfile(Profile.json));
}
async add(options: any): Promise<void> {
// TODO: implement add
throw new Error('ProfileCommand: add not implemented: ' + options);
}
encrypt({ password }:{ password: string }): ProfileBackup {
password ??= createPassword();
const jsonString = JSON.stringify(Profile.json);
const salt = randomBytes(16);
const iv = randomBytes(12);
const key = pbkdf2Sync(password, salt, 100000, 32, 'sha256');
const cipher = createCipheriv('aes-256-gcm', key, iv);
let encrypted = cipher.update(jsonString, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag().toString('hex');
return {
data : encrypted,
salt : salt.toString('hex'),
iv : iv.toString('hex'),
authTag : authTag
};
}
// Function to decrypt JSON
async delete({ password, force }: { password: string; force?: boolean }): Promise<void> {
password ??= createPassword();
const answer: string = force
? 'Force'
: await Inquirer.select({
choices : ['Yes', 'No'],
message : 'Are you sure you want to delete profile.json?'
}) ?? 'No';
if(answer === 'No') {
Logger.log('Cancelling deletion');
return process.exit(0);
}
if(answer !== 'Force' ) {
const encrypted = this.encrypt({ password });
const encryptedFilepath = `${DRPM_HOME}/profile-${encrypted.authTag}.enc`;
await writeFile(encryptedFilepath, stringifier(encrypted));
Logger.log(`Created encrypted backup at ${encryptedFilepath}`);
} else {
Logger.log('Creating encrypted backup of profile.json');
await writeFile(DRPM_PROFILE, stringifier(DEFAULT_PROFILE));
Logger.log('Deleted all profiles!');
}
}
async list(): Promise<void> {
Logger.plain(
`Available Profile Contexts:\n${
Object.keys(Profile.json)
.filter((key) => key !== 'name')
.map((key, i) => ` ${i+1}. ${key.trim()} (${key === Profile.json.name ? formatter.green('active') : formatter.red('inactive')})`).join('\n')
}`);
}
// Subcommand function to switch between profiles
async switch({ name }: { name?: string }): Promise<void> {
if(name) {
Profile.json.name = name;
} else {
const choices = Object.keys(Profile.json)
.filter((key) => key !== 'name')
.map((key) => `${key.trim()} (${key === Profile.json.name ? 'active' : 'inactive'})`);
const name: string = await Inquirer.select({ choices, message: 'Which profile context would you like to switch to?' });
Profile.json.name = name.replace(/ \(.*\)/, '');
}
await this.save();
const profile = Profile.json;
const activeName = Profile.json.name;
const context = profile[activeName];
Logger.log(`Switched profile context to ${activeName}: ${secureProfileContext(context)}`);
}
static async recover({ password, file }: { password: string; file: string }): Promise<void> {
const profileBackup: ProfileBackup = JSON.parse(await readFile(file, 'utf8'));
const { data, salt, iv, authTag } = profileBackup ?? {};
if(!(data && salt && iv && authTag)) {
Logger.error(`ProfileError: Invalid backup file ${file}`);
process.exit(1);
}
const key = pbkdf2Sync(password, Buffer.from(salt, 'hex'), 100000, 32, 'sha256');
const decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(iv, 'hex'));
decipher.setAuthTag(Buffer.from(authTag, 'hex'));
let decrypted = decipher.update(data, 'hex', 'utf8');
decrypted += decipher.final('utf8');
Profile.json = JSON.parse(decrypted);
await this.staticSave();
Logger.log(`Recovered profile.json from backup ${file}`);
}
async backup({ password }: {password: string}): Promise<void> {
const writePassword = !password;
password ??= createPassword();
const jsonString = JSON.stringify(Profile.json);
const salt = randomBytes(16);
const iv = randomBytes(12);
const key = pbkdf2Sync(password, salt, 100000, 32, 'sha256');
const cipher = createCipheriv('aes-256-gcm', key, iv);
let encryptedData = cipher.update(jsonString, 'utf8', 'hex');
encryptedData += cipher.final('hex');
const encrypted = {
data : encryptedData,
salt : salt.toString('hex'),
iv : iv.toString('hex'),
authTag : cipher.getAuthTag().toString('hex')
};
const encryptedDir = `${DRPM_HOME}/bak/${encrypted.authTag}`;
await ensureDir(encryptedDir);
if(writePassword) await writeFile(`${encryptedDir}/profile.key`, password, 'utf8');
await writeFile(`${encryptedDir}/profile.enc`, stringifier(encrypted), 'utf8');
Logger.log(`Backed up profile.json to ${encryptedDir}`, encrypted);
}
}