beeline-cli
Version:
A terminal wallet for the Hive blockchain - type, sign, rule the chain
257 lines • 10.3 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.KeyManager = void 0;
const crypto_js_1 = __importDefault(require("crypto-js"));
const dhive_1 = require("@hiveio/dhive");
const keytar = __importStar(require("keytar"));
const fs = __importStar(require("fs-extra"));
const path = __importStar(require("path"));
const os = __importStar(require("os"));
const SERVICE_NAME = 'beeline-wallet';
const CONFIG_DIR = path.join(os.homedir(), '.beeline');
const CONFIG_FILE = path.join(CONFIG_DIR, 'wallet.json');
class KeyManager {
constructor() {
this.config = {
accounts: {},
encryptionEnabled: true
};
}
async initialize() {
await fs.ensureDir(CONFIG_DIR);
if (await fs.pathExists(CONFIG_FILE)) {
try {
this.config = await fs.readJson(CONFIG_FILE);
}
catch (error) {
// If config is corrupted, start fresh
this.config = {
accounts: {},
encryptionEnabled: true
};
}
}
}
async saveConfig() {
await fs.writeJson(CONFIG_FILE, this.config, { spaces: 2 });
}
getKeyId(account, role) {
return `${SERVICE_NAME}:${account}:${role}`;
}
async importPrivateKey(account, role, privateKeyWif, pin) {
try {
// Validate the private key
const privateKey = dhive_1.PrivateKey.fromString(privateKeyWif);
const publicKey = privateKey.createPublic().toString();
const keyId = this.getKeyId(account, role);
if (this.config.encryptionEnabled && pin) {
// Store encrypted in OS keychain
const encrypted = crypto_js_1.default.AES.encrypt(privateKeyWif, pin).toString();
await keytar.setPassword(SERVICE_NAME, keyId, encrypted);
}
else {
// Store in OS keychain without additional encryption (relies on OS security)
await keytar.setPassword(SERVICE_NAME, keyId, privateKeyWif);
}
// Update config
if (!this.config.accounts[account]) {
this.config.accounts[account] = [];
}
// Remove existing key for this role if it exists
this.config.accounts[account] = this.config.accounts[account].filter(key => key.role !== role);
// Add new key
this.config.accounts[account].push({
account,
role,
publicKey,
encrypted: this.config.encryptionEnabled && !!pin
});
// Set as default if first account
if (!this.config.defaultAccount) {
this.config.defaultAccount = account;
}
await this.saveConfig();
}
catch (error) {
throw new Error(`Invalid private key: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async getPrivateKey(account, role, pin) {
const keyId = this.getKeyId(account, role);
try {
const stored = await keytar.getPassword(SERVICE_NAME, keyId);
if (!stored)
return null;
const keyConfig = this.config.accounts[account]?.find(k => k.role === role);
if (keyConfig?.encrypted) {
if (!pin) {
throw new Error(`PIN required to decrypt ${role} key for @${account}`);
}
// Decrypt with PIN
const decrypted = crypto_js_1.default.AES.decrypt(stored, pin).toString(crypto_js_1.default.enc.Utf8);
if (!decrypted) {
throw new Error('Invalid PIN');
}
return decrypted;
}
return stored;
}
catch (error) {
throw new Error(`Failed to retrieve key: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async listAccounts() {
return Object.keys(this.config.accounts);
}
async listKeys(account) {
return this.config.accounts[account] || [];
}
async removeKey(account, role) {
const keyId = this.getKeyId(account, role);
try {
await keytar.deletePassword(SERVICE_NAME, keyId);
if (this.config.accounts[account]) {
this.config.accounts[account] = this.config.accounts[account].filter(key => key.role !== role);
// Remove account if no keys left
if (this.config.accounts[account].length === 0) {
delete this.config.accounts[account];
// Update default account if needed
if (this.config.defaultAccount === account) {
const remaining = Object.keys(this.config.accounts);
this.config.defaultAccount = remaining.length > 0 ? remaining[0] : undefined;
}
}
}
await this.saveConfig();
}
catch (error) {
throw new Error(`Failed to remove key: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
getDefaultAccount() {
return this.config.defaultAccount;
}
async setDefaultAccount(account) {
if (!this.config.accounts[account]) {
throw new Error(`Account ${account} not found`);
}
this.config.defaultAccount = account;
await this.saveConfig();
}
// Derive all keys from master password
deriveKeysFromPassword(account, password) {
try {
// Use Hive's standard key derivation
const ownerKey = dhive_1.PrivateKey.fromLogin(account, password, 'owner');
const activeKey = dhive_1.PrivateKey.fromLogin(account, password, 'active');
const postingKey = dhive_1.PrivateKey.fromLogin(account, password, 'posting');
const memoKey = dhive_1.PrivateKey.fromLogin(account, password, 'memo');
return {
owner: ownerKey,
active: activeKey,
posting: postingKey,
memo: memoKey
};
}
catch (error) {
throw new Error(`Failed to derive keys: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async loginWithPassword(account, password, pin, roles = ['posting', 'active', 'memo']) {
try {
// Derive all keys from password
const derivedKeys = this.deriveKeysFromPassword(account, password);
// Import specified roles
for (const role of roles) {
const privateKey = derivedKeys[role];
await this.importPrivateKey(account, role, privateKey.toString(), pin);
}
// Set as default if first account
if (!this.config.defaultAccount) {
this.config.defaultAccount = account;
await this.saveConfig();
}
// Memory scrubbing
this.scrubMemory(password);
Object.values(derivedKeys).forEach(key => {
this.scrubMemory(key.toString());
});
}
catch (error) {
// Memory scrubbing on error too
this.scrubMemory(password);
throw new Error(`Login failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
async hasAccount(account) {
return !!this.config.accounts[account] && this.config.accounts[account].length > 0;
}
async getAccountSummary(account) {
const keys = this.config.accounts[account];
if (!keys)
return null;
return {
account,
keyCount: keys.length,
roles: keys.map(k => k.role),
isDefault: this.config.defaultAccount === account
};
}
async getAllAccountSummaries() {
const accounts = await this.listAccounts();
const summaries = [];
for (const account of accounts) {
const summary = await this.getAccountSummary(account);
if (summary)
summaries.push(summary);
}
return summaries;
}
// Memory scrubbing utility
scrubMemory(data) {
// Fill memory with random data to prevent key recovery
if (typeof data === 'string') {
for (let i = 0; i < data.length; i++) {
data = data.substring(0, i) + String.fromCharCode(Math.floor(Math.random() * 256)) + data.substring(i + 1);
}
}
}
}
exports.KeyManager = KeyManager;
//# sourceMappingURL=crypto.js.map