diff --git a/.gitignore b/.gitignore index b9ad4982..117b96ef 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ node_modules/ # Build output dist/ - +lib/ # Logs npm-debug.log* yarn-debug.log* diff --git a/package.json b/package.json index e3c9901d..b22e499b 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "@types/bytebuffer": "^5.0.49", "@types/debug": "^4.1.12", "@types/ecurve": "^1.0.3", + "@types/lodash": "^4.17.18", "@types/node": "^20.11.24", "@types/ws": "^8.18.1", "@typescript-eslint/eslint-plugin": "^7.1.0", diff --git a/src/api/index.ts b/src/api/index.ts index 6edd4760..79709114 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -2,7 +2,6 @@ import { EventEmitter } from 'events'; import Bluebird from 'bluebird'; import { getConfig } from '../config'; import { camelCase } from '../utils'; -import { hash } from '../auth/ecc'; import { sign as signRequest } from './rpc-auth'; import methods from './methods'; import { jsonRpc } from './transports/http'; @@ -29,8 +28,6 @@ export class Api extends EventEmitter { private transport: any; private options: ApiOptions; private __logger: Logger | false = false; - private callAsync: any; - private signedCallAsync: any; // Patch for all API methods to support both callback and promise styles // This is a helper to wrap methods @@ -99,9 +96,6 @@ export class Api extends EventEmitter { (this as any)[`${methodName}WithAsync`] = Bluebird.promisify((this as any)[`${methodName}With`]); (this as any)[`${methodName}Async`] = Bluebird.promisify((this as any)[methodName]); }); - - this.callAsync = Bluebird.promisify(this.call); - this.signedCallAsync = Bluebird.promisify(this.signedCall); } private _setTransport(options: ApiOptions) { @@ -171,6 +165,8 @@ export class Api extends EventEmitter { break; case 'undefined': if (this.__logger) break; + this.__logger = false; + break; default: this.__logger = false; } @@ -238,7 +234,7 @@ export class Api extends EventEmitter { callback(error); return; } - jsonRpc(this.options.uri!, request) + jsonRpc(this.options.uri!, request as any) .then(res => { callback(null, res); }, err => { callback(err); }); } @@ -498,37 +494,41 @@ export class Api extends EventEmitter { /** * Broadcast a transaction with a callback (stub). */ - broadcastTransactionWithCallback(confirmationCallback: any, trx: any, callback: any) { - // Not implemented in migrated version - callback(new Error('broadcastTransactionWithCallback is not implemented')); + broadcastTransactionWithCallback(_confirmationCallback: any, _trx: any, callback: any) { + // Implementation would go here + callback(new Error('Not implemented')); } /** * Broadcast a block (stub). */ - broadcastBlock(b: any, callback: any) { - callback(new Error('broadcastBlock is not implemented')); + broadcastBlock(_b: any, callback: any) { + // Implementation would go here + callback(new Error('Not implemented')); } /** * Set max block age (stub). */ - setMaxBlockAge(maxBlockAge: any, callback: any) { - callback(new Error('setMaxBlockAge is not implemented')); + setMaxBlockAge(_maxBlockAge: any, callback: any) { + // Implementation would go here + callback(new Error('Not implemented')); } /** * Verify authority (stub). */ - verifyAuthority(...args: any[]) { - throw new Error('Not implemented'); + verifyAuthority(..._args: any[]) { + // Implementation would go here + return false; } /** * Verify account authority (stub). */ - verifyAccountAuthority(...args: any[]) { - throw new Error('Not implemented'); + verifyAccountAuthority(..._args: any[]) { + // Implementation would go here + return false; } } @@ -547,9 +547,9 @@ export function signTransaction(trx: any, keys: string[]) { return api.signTransaction(trx, keys); } -export function verifyAuthority(...args: any[]) { - // Not implemented, but must exist for test compatibility - throw new Error('Not implemented'); +export function verifyAuthority(..._args: any[]) { + // Implementation would go here + return false; } export default api; diff --git a/src/api/rpc-auth.ts b/src/api/rpc-auth.ts index d04265fa..1a079888 100644 --- a/src/api/rpc-auth.ts +++ b/src/api/rpc-auth.ts @@ -1,4 +1,10 @@ -import { hash } from '../auth/ecc'; + + + + +import { Signature } from '../auth/ecc/src/signature'; +import { PrivateKey } from '../auth/key_classes'; +import { createHash, randomBytes } from 'crypto'; interface RpcRequest { method: string; @@ -6,16 +12,158 @@ interface RpcRequest { id: number; } -export function sign(request: RpcRequest, account: string, keys: string[]): any { - const message = JSON.stringify(request); - const messageHash = hash.sha256(message); - const signatures = keys.map(key => { - // TODO: Implement proper signing with the key - return 'dummy_signature'; - }); +interface SignedRequest { + jsonrpc: string; + method: string; + id: number; + params: { + __signed: { + account: string; + nonce: string; + params: string; + signatures: string[]; + timestamp: string; + }; + }; +} + +/** + * Signing constant used to reserve opcode space and prevent cross-protocol attacks. + * Output of `sha256('steem_jsonrpc_auth')`. + */ +export const K = Buffer.from('3b3b081e46ea808d5a96b08c4bc5003f5e15767090f344faab531ec57565136b', 'hex'); + +/** + * Create request hash to be signed. + * + * @param timestamp ISO8601 formatted date e.g. `2017-11-14T19:40:29.077Z`. + * @param account Steem account name that is the signer. + * @param method RPC request method. + * @param params Base64 encoded JSON string containing request params. + * @param nonce 8 bytes of random data. + * + * @returns bytes to be signed or validated. + */ +function hashMessage(timestamp: string, account: string, method: string, params: string, nonce: Buffer): Buffer { + const first = createHash('sha256'); + first.update(timestamp); + first.update(account); + first.update(method); + first.update(params); + const second = createHash('sha256'); + second.update(K); + second.update(first.digest()); + second.update(nonce); + return second.digest(); +} + +/** + * Sign a JSON RPC Request. + */ +export function sign(request: RpcRequest, account: string, keys: string[]): SignedRequest { + if (!request.params) { + throw new Error('Unable to sign a request without params'); + } + + const params = Buffer.from(JSON.stringify(request.params), 'utf8').toString('base64'); + const nonceBytes = randomBytes(8); + const nonce = nonceBytes.toString('hex'); + const timestamp = new Date().toISOString(); + const message = hashMessage(timestamp, account, request.method, params, nonceBytes); + + const signatures: string[] = []; + for (const key of keys) { + const privateKey = PrivateKey.fromWif(key); + const signature = Signature.signBufferSha256(message, privateKey); + signatures.push(signature.toHex()); + } return { - ...request, - signatures + jsonrpc: '2.0', + method: request.method, + id: request.id, + params: { + __signed: { + account, + nonce, + params, + signatures, + timestamp + } + } }; -} \ No newline at end of file +} + +/** + * Validate a signed JSON RPC request. + * Throws a ValidationError if the request fails validation. + * + * @param request The signed JSON RPC request to validate + * @param verify Function to verify signatures against public keys + * @returns Resolved request params + */ +export async function validate( + request: SignedRequest, + verify: (message: Buffer, signatures: string[], account: string) => Promise +): Promise { + if (request.jsonrpc !== '2.0' || typeof request.method !== 'string') { + throw new Error('Invalid JSON RPC Request'); + } + + if (request.params == undefined || request.params.__signed == undefined) { + throw new Error('Signed payload missing'); + } + + if (Object.keys(request.params).length !== 1) { + throw new Error('Invalid request params'); + } + + const signed = request.params.__signed; + + if (signed.account == undefined) { + throw new Error('Missing account'); + } + + let params: any; + try { + const jsonString = Buffer.from(signed.params, 'base64').toString('utf8'); + params = JSON.parse(jsonString); + } catch (cause: any) { + throw new Error(`Invalid encoded params: ${cause.message}`); + } + + if (signed.nonce == undefined || typeof signed.nonce !== 'string') { + throw new Error('Invalid nonce'); + } + + const nonce = Buffer.from(signed.nonce, 'hex'); + if (nonce.length !== 8) { + throw new Error('Invalid nonce'); + } + + const timestamp = Date.parse(signed.timestamp); + if (Number.isNaN(timestamp)) { + throw new Error('Invalid timestamp'); + } + + if (Date.now() - timestamp > 60 * 1000) { + throw new Error('Signature expired'); + } + + const message = hashMessage(signed.timestamp, signed.account, request.method, signed.params, nonce); + + try { + await verify(message, signed.signatures, signed.account); + } catch (cause: any) { + throw new Error(`Verification failed: ${cause.message}`); + } + + return params; +} + +// Default export to match JavaScript implementation +export default { + sign, + validate, + K +}; \ No newline at end of file diff --git a/src/api/transports/base.ts b/src/api/transports/base.ts new file mode 100644 index 00000000..021294e9 --- /dev/null +++ b/src/api/transports/base.ts @@ -0,0 +1,48 @@ +import { EventEmitter } from 'events'; +import { Transport, TransportOptions } from './types'; + +export class BaseTransport extends EventEmitter implements Transport { + options: TransportOptions; + id: number = 0; + + constructor(options: TransportOptions = {}) { + super(); + this.options = options; + this.id = 0; + } + + setOptions(options: TransportOptions): void { + Object.assign(this.options, options); + this.stop(); + } + + listenTo(target: EventEmitter, eventName: string, callback: (...args: any[]) => void): () => void { + if ('addEventListener' in target && typeof (target as any).addEventListener === 'function') { + (target as any).addEventListener(eventName, callback); + return () => { + (target as any).removeEventListener(eventName, callback); + }; + } else { + target.on(eventName, callback); + return () => { + target.removeListener(eventName, callback); + }; + } + } + + send(_api: string, _data: any, _callback: (error: any, result?: any) => void): void { + // Base implementation - should be overridden by subclasses + } + + start(): Promise { + // Base implementation - should be overridden by subclasses + return Promise.resolve(); + } + + stop(): Promise { + // Base implementation - should be overridden by subclasses + return Promise.resolve(); + } +} + +export default BaseTransport; \ No newline at end of file diff --git a/src/api/transports/http.ts b/src/api/transports/http.ts index 3927bc28..a82d8315 100644 --- a/src/api/transports/http.ts +++ b/src/api/transports/http.ts @@ -1,7 +1,8 @@ import axios from 'axios'; // @ts-ignore: No types for 'retry' import retry from 'retry'; -import { Transport, TransportOptions, JsonRpcRequest, JsonRpcResponse } from './types'; +import { TransportOptions, JsonRpcRequest, JsonRpcResponse } from './types'; +import { BaseTransport } from './base'; export const jsonRpc = async (uri: string, request: Partial): Promise => { try { @@ -15,23 +16,9 @@ export const jsonRpc = async (uri: string, request: Partial): Pr } }; -export class HttpTransport implements Transport { - options: TransportOptions; - +export class HttpTransport extends BaseTransport { constructor(options: TransportOptions) { - this.options = options; - } - - start(): Promise { - return Promise.resolve(); - } - - stop(): Promise { - return Promise.resolve(); - } - - setOptions(options: TransportOptions): void { - this.options = { ...this.options, ...options }; + super(options); } get nonRetriableOperations(): string[] { diff --git a/src/api/transports/index.ts b/src/api/transports/index.ts index 9739d8e3..909fa090 100644 --- a/src/api/transports/index.ts +++ b/src/api/transports/index.ts @@ -1,8 +1,11 @@ import { HttpTransport } from './http'; import { WsTransport } from './ws'; +import { BaseTransport } from './base'; export * from './types'; export const transports = { http: HttpTransport, ws: WsTransport -}; \ No newline at end of file +}; + +export { BaseTransport }; \ No newline at end of file diff --git a/src/api/transports/ws.ts b/src/api/transports/ws.ts index 074179b2..818355c0 100644 --- a/src/api/transports/ws.ts +++ b/src/api/transports/ws.ts @@ -1,137 +1,18 @@ -import { EventEmitter } from 'events'; // @ts-ignore import WebSocket from 'ws'; -import { getConfig } from '../../config'; -import { Transport, TransportOptions, JsonRpcRequest, JsonRpcResponse } from './types'; +import { TransportOptions, JsonRpcRequest, JsonRpcResponse } from './types'; +import { BaseTransport } from './base'; -export class WebSocketTransport extends EventEmitter { - private ws: WebSocket | null = null; - private options: any; - private reconnectTimer: NodeJS.Timeout | null = null; - private reconnectAttempts: number = 0; - private maxReconnectAttempts: number = 5; - private reconnectInterval: number = 1000; - - constructor(options: any) { - super(); - this.options = { - ...options, - websocket: options.websocket || 'wss://api.steemit.com', - }; - } - - start() { - if (this.ws) { - return Promise.resolve(); - } - - return new Promise((resolve, reject) => { - const url = this.options.websocket || getConfig().get('websocket') || 'wss://api.steemit.com'; - this.ws = new WebSocket(url); - - this.ws.on('open', () => { - this.reconnectAttempts = 0; - this.emit('open'); - resolve(); - }); - - this.ws.on('message', (data: string) => { - try { - const message = JSON.parse(data); - this.emit('message', message); - } catch (error) { - this.emit('error', new Error('Invalid message format')); - } - }); - - this.ws.on('error', (error: Error) => { - this.emit('error', error); - this._handleReconnect(); - }); - - this.ws.on('close', () => { - this.emit('close'); - this._handleReconnect(); - }); - }); - } - - stop() { - if (this.reconnectTimer) { - clearTimeout(this.reconnectTimer); - this.reconnectTimer = null; - } - - if (this.ws) { - this.ws.close(); - this.ws = null; - } - - return Promise.resolve(); - } - - send(api: string, data: any, callback: any) { - if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { - callback(new Error('WebSocket is not connected')); - return; - } - - const message = { - id: Math.floor(Math.random() * 1000000), - jsonrpc: '2.0', - method: api, - params: data - }; - - const timeout = setTimeout(() => { - callback(new Error('Request timeout')); - }, 30000); - - const messageHandler = (response: any) => { - if (response.id === message.id) { - clearTimeout(timeout); - this.removeListener('message', messageHandler); - callback(null, response.result); - } - }; - - this.on('message', messageHandler); - - try { - this.ws.send(JSON.stringify(message)); - } catch (error) { - clearTimeout(timeout); - this.removeListener('message', messageHandler); - callback(error); - } - } - - private _handleReconnect() { - if (this.reconnectTimer || this.reconnectAttempts >= this.maxReconnectAttempts) { - return; - } - - this.reconnectAttempts++; - this.reconnectTimer = setTimeout(() => { - this.reconnectTimer = null; - this.start().catch(() => { - // Ignore start errors, they will trigger reconnect again - }); - }, this.reconnectInterval * Math.pow(2, this.reconnectAttempts - 1)); - } -} - -export class WsTransport implements Transport { - options: TransportOptions; +export class WsTransport extends BaseTransport { private ws: WebSocket | null; private _requests: Map void>; private seqNo: number; constructor(options: TransportOptions) { - this.options = { + super({ ...options, websocket: options.websocket || 'wss://api.steemit.com', - }; + }); this.ws = null; this._requests = new Map(); this.seqNo = 0; @@ -174,16 +55,10 @@ export class WsTransport implements Transport { return Promise.resolve(); } - setOptions(options: TransportOptions): void { - this.options = { ...this.options, ...options }; - if (options.websocket && this.ws) { - this.stop().then(() => this.start()); - } - } - - send(api: string, data: any, callback: (error: any, result?: any) => void): Promise | void { + send(api: string, data: any, callback: (error: any, result?: any) => void): void { if (!this.ws) { - return this.start().then(() => this.send(api, data, callback)); + this.start().then(() => this.send(api, data, callback)); + return; } const id = data.id || ++this.seqNo; diff --git a/src/auth/address.ts b/src/auth/address.ts deleted file mode 100644 index c70ae13f..00000000 --- a/src/auth/address.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { sha256, sha512, ripemd160 } from './hash'; -import { getConfig } from '../config'; -import bs58 from 'bs58'; -import { PublicKey } from './key_public'; - -export class Address { - private addy: Buffer; - - constructor(addy: Buffer) { - this.addy = addy; - } - - static fromBuffer(buffer: Buffer): Address { - // Original: sha512 then ripemd160 - const _hash = sha512(buffer); - const addy = ripemd160(_hash); - return new Address(addy); - } - - static fromString(string: string, address_prefix: string = String(getConfig().get('address_prefix')) || 'STM'): Address { - // Remove prefix if present - let base58str = string; - if (string.startsWith(address_prefix)) { - base58str = string.slice(address_prefix.length); - } - let addy = Buffer.from(bs58.decode(base58str)); - // For Steem: last 4 bytes are ripemd160 checksum - // For BTC/PTS: last 4 bytes are double sha256 checksum - let version = addy[0]; - let body = addy.slice(0, -4); - let checksum = addy.slice(-4); - let new_checksum; - if (version === 56 || version === 0) { - new_checksum = sha256(sha256(body)).slice(0, 4); - } else { - new_checksum = ripemd160(body).slice(0, 4); - } - if (!checksum.equals(new_checksum)) { - throw new Error('Checksum did not match'); - } - return new Address(addy); - } - - static fromPublic(publicKey: PublicKey, compressed: boolean = true, version: number = 56): Address { - const pub_buf = publicKey.toBuffer(compressed); - const sha2 = sha256(pub_buf); - const rep = ripemd160(sha2); - const versionBuffer = Buffer.from([version]); - const addr = Buffer.concat([versionBuffer, rep]); - let check; - if (version === 56 || version === 0) { - check = sha256(sha256(addr)).slice(0, 4); - } else { - check = ripemd160(addr).slice(0, 4); - } - const buffer = Buffer.concat([addr, check]); - return new Address(ripemd160(buffer)); - } - - toBuffer(): Buffer { - return this.addy; - } - - getVersion(): number { - return this.addy[0]; - } - - toString(address_prefix: string = String(getConfig().get('address_prefix')) || 'STM'): string { - // Always use ripemd160 checksum and STM prefix, as in original Steem-js - const checksum = ripemd160(this.addy).slice(0, 4); - const addy = Buffer.concat([this.addy, checksum]); - return address_prefix + bs58.encode(addy); - } -} diff --git a/src/auth/ecc/index.ts b/src/auth/ecc/index.ts index bc889b78..c9dc5c33 100644 --- a/src/auth/ecc/index.ts +++ b/src/auth/ecc/index.ts @@ -1,21 +1,9 @@ -import { Address } from './address'; -import { Aes } from './aes'; -import { PrivateKey } from './key_private'; -import { PublicKey } from './key_public'; -import { Signature } from './signature'; -import { normalize as brainKey } from './brain_key'; -import * as key_utils from './key_utils'; -import * as hash from './hash'; -import { Config as ecc_config } from '../../config'; - -export { - Address, - Aes, - PrivateKey, - PublicKey, - Signature, - brainKey, - key_utils, - hash, - ecc_config -}; \ No newline at end of file +export { Address } from './src/address'; +export { Aes } from './src/aes'; +export { PrivateKey } from './src/key_private'; +export { PublicKey } from './src/key_public'; +export { Signature } from './src/signature'; +export { normalize as brainKey } from './src/brain_key'; +export * as key_utils from './src/key_utils'; +export * as hash from './src/hash'; +export { Config as ecc_config } from '../../config'; \ No newline at end of file diff --git a/src/auth/ecc/src/address.ts b/src/auth/ecc/src/address.ts new file mode 100644 index 00000000..633f4952 --- /dev/null +++ b/src/auth/ecc/src/address.ts @@ -0,0 +1,76 @@ +import { ripemd160, sha256 } from './hash'; +import { getConfig } from '../../../config'; +import bs58 from 'bs58'; +import { PublicKey } from './key_public'; + +export class Address { + private addy: Buffer; + + constructor(addy: Buffer) { + this.addy = addy; + } + + static fromBuffer(buffer: Buffer): string { + const checksum = buffer.slice(-4); + const addr = buffer.slice(0, -4); + const new_checksum = ripemd160(addr).slice(0, 4); + if (!checksum.equals(new_checksum as Buffer)) { + throw new Error('Invalid address checksum'); + } + return getConfig().get('address_prefix') + bs58.encode(addr); + } + + static fromString(address: string): Buffer { + const prefix = getConfig().get('address_prefix'); + if (!address.startsWith(prefix)) { + throw new Error(`Expecting address to begin with ${prefix}`); + } + const addr = address.slice(prefix.length); + const buffer = bs58.decode(addr); + const checksum = buffer.slice(-4); + const addr_part = buffer.slice(0, -4); + const new_checksum = ripemd160(addr_part).slice(0, 4); + if (!checksum.equals(new_checksum as Buffer)) { + throw new Error('Invalid address checksum'); + } + return buffer; + } + + static fromPublicKey(public_key: PublicKey, compressed: boolean = true): string { + const pub_buffer = public_key.toBuffer(compressed); + const checksum = ripemd160(pub_buffer).slice(0, 4); + const addr = Buffer.concat([pub_buffer, checksum as Buffer]); + return getConfig().get('address_prefix') + bs58.encode(addr); + } + + static fromPublic(public_key: PublicKey, compressed: boolean = true, version: number = 56): Address { + const sha2 = sha256(public_key.toBuffer(compressed)); + const rep = ripemd160(sha2); + const versionBuffer = Buffer.alloc(1); + versionBuffer.writeUInt8(0xFF & version, 0); + const addr = Buffer.concat([versionBuffer, rep]); + let check = sha256(addr); + check = sha256(check); + const buffer = Buffer.concat([addr, check.slice(0, 4)]); + return new Address(ripemd160(buffer)); + } + + static toBuffer(address: string): Buffer { + return Address.fromString(address); + } + + toBuffer(): Buffer { + return this.addy; + } + + getVersion(): number { + return this.addy[0]; + } + + toString(address_prefix: string = String(getConfig().get('address_prefix')) || 'STM'): string { + // Always use ripemd160 checksum and STM prefix, as in original Steem-js + const checksum = ripemd160(this.addy).slice(0, 4); + const addy = Buffer.concat([this.addy, checksum]); + return address_prefix + bs58.encode(addy); + } +} diff --git a/src/auth/aes.ts b/src/auth/ecc/src/aes.ts similarity index 67% rename from src/auth/aes.ts rename to src/auth/ecc/src/aes.ts index b45c3c59..bde911a5 100644 --- a/src/auth/aes.ts +++ b/src/auth/ecc/src/aes.ts @@ -7,6 +7,11 @@ import Long from 'long'; let uniqueNonceEntropy: number | null = null; +function sha512Buffer(data: string | Buffer): Buffer { + const result = sha512(data); + return Buffer.isBuffer(result) ? result : Buffer.from(result, 'hex'); +} + export class Aes { static uniqueNonce(): string { if (uniqueNonceEntropy === null) { @@ -31,30 +36,37 @@ export class Aes { throw new TypeError('nonce is required'); } + let messageBuffer: Buffer; if (!Buffer.isBuffer(message)) { if (typeof message !== 'string') { throw new TypeError('message should be buffer or string'); } - message = Buffer.from(message, 'binary'); + messageBuffer = Buffer.from(message, 'binary'); + } else { + messageBuffer = message; } const S = private_key.get_shared_secret(public_key); let ebuf = new ByteBuffer(ByteBuffer.DEFAULT_CAPACITY, ByteBuffer.LITTLE_ENDIAN); ebuf.writeUint64(Long.fromString(nonce)); ebuf.append(S.toString('binary'), 'binary'); - ebuf = Buffer.from(ebuf.copy(0, ebuf.offset).toBinary(), 'binary'); - const encryption_key = sha512(ebuf); + const ebufBuffer = Buffer.from(ebuf.copy(0, ebuf.offset).toBinary(), 'binary'); + const encryption_key = sha512Buffer(ebufBuffer); const iv = encryption_key.slice(32, 48); const key = encryption_key.slice(0, 32); let check = sha256(encryption_key); + if (!Buffer.isBuffer(check)) { + check = Buffer.from(check, 'hex'); + } check = check.slice(0, 4); - const cbuf = ByteBuffer.fromBinary(check.toString('binary'), ByteBuffer.DEFAULT_CAPACITY, ByteBuffer.LITTLE_ENDIAN); + const checkBinary = check.toString('binary'); + const cbuf = ByteBuffer.fromBinary(checkBinary, ByteBuffer.DEFAULT_CAPACITY, ByteBuffer.LITTLE_ENDIAN); const checksum = cbuf.readUint32(); const cipher = createCipheriv('aes-256-cbc', key, iv); - const encrypted = Buffer.concat([cipher.update(message), cipher.final()]); + const encrypted = Buffer.concat([cipher.update(messageBuffer), cipher.final()]); return { nonce, @@ -88,15 +100,19 @@ export class Aes { let ebuf = new ByteBuffer(ByteBuffer.DEFAULT_CAPACITY, ByteBuffer.LITTLE_ENDIAN); ebuf.writeUint64(Long.fromString(nonce)); ebuf.append(S.toString('binary'), 'binary'); - ebuf = Buffer.from(ebuf.copy(0, ebuf.offset).toBinary(), 'binary'); - const encryption_key = sha512(ebuf); + const ebufBuffer = Buffer.from(ebuf.copy(0, ebuf.offset).toBinary(), 'binary'); + const encryption_key = sha512Buffer(ebufBuffer); const iv = encryption_key.slice(32, 48); const key = encryption_key.slice(0, 32); let check = sha256(encryption_key); + if (!Buffer.isBuffer(check)) { + check = Buffer.from(check, 'hex'); + } check = check.slice(0, 4); - const cbuf = ByteBuffer.fromBinary(check.toString('binary'), ByteBuffer.DEFAULT_CAPACITY, ByteBuffer.LITTLE_ENDIAN); + const checkBinary = check.toString('binary'); + const cbuf = ByteBuffer.fromBinary(checkBinary, ByteBuffer.DEFAULT_CAPACITY, ByteBuffer.LITTLE_ENDIAN); const calculatedChecksum = cbuf.readUint32(); if (calculatedChecksum !== checksum) { @@ -107,4 +123,24 @@ export class Aes { const messageBuffer = Buffer.from(message, 'hex'); return Buffer.concat([decipher.update(messageBuffer), decipher.final()]); } + + static fromSeed(seed: string): Buffer { + return sha256(seed); + } + + static fromBuffer(buffer: Buffer): Buffer { + return buffer; + } + + static fromString(string: string): Buffer { + return Buffer.from(string, 'hex'); + } + + static toBuffer(aes: Buffer): Buffer { + return aes; + } + + static toString(aes: Buffer): string { + return aes.toString('hex'); + } } \ No newline at end of file diff --git a/src/auth/ecc/brain_key.ts b/src/auth/ecc/src/brain_key.ts similarity index 100% rename from src/auth/ecc/brain_key.ts rename to src/auth/ecc/src/brain_key.ts diff --git a/src/auth/ecc/ecdsa.ts b/src/auth/ecc/src/ecdsa.ts similarity index 98% rename from src/auth/ecc/ecdsa.ts rename to src/auth/ecc/src/ecdsa.ts index 2f3ca3a9..2230f210 100644 --- a/src/auth/ecc/ecdsa.ts +++ b/src/auth/ecc/src/ecdsa.ts @@ -3,7 +3,10 @@ import * as crypto from './hash'; import enforce from './enforce_types'; import BigInteger from 'bigi'; import ECSignature from './ecsignature'; -import { Point, Curve } from 'ecurve'; + +// Use any type to avoid namespace issues +type Curve = any; +type Point = any; // https://tools.ietf.org/html/rfc6979#section-3.2 function deterministicGenerateK(curve: Curve, hash: Buffer, d: BigInteger, checkSig: (k: BigInteger) => boolean, nonce?: number): BigInteger { diff --git a/src/auth/ecc/ecsignature.ts b/src/auth/ecc/src/ecsignature.ts similarity index 100% rename from src/auth/ecc/ecsignature.ts rename to src/auth/ecc/src/ecsignature.ts diff --git a/src/auth/ecc/enforce_types.ts b/src/auth/ecc/src/enforce_types.ts similarity index 100% rename from src/auth/ecc/enforce_types.ts rename to src/auth/ecc/src/enforce_types.ts diff --git a/src/auth/ecc/hash.ts b/src/auth/ecc/src/hash.ts similarity index 76% rename from src/auth/ecc/hash.ts rename to src/auth/ecc/src/hash.ts index c79af7ac..4861948f 100644 --- a/src/auth/ecc/hash.ts +++ b/src/auth/ecc/src/hash.ts @@ -13,16 +13,22 @@ export function sha1(data: string | Buffer, encoding?: BufferEncoding): string | @arg {string} [digest = null] - 'hex', 'binary' or 'base64' @return {string|Buffer} - Buffer when digest is null, or string */ -export function sha256(data: string | Buffer, encoding?: BufferEncoding): string | Buffer { - return createHash('sha256').update(data).digest(encoding); +export function sha256(data: string | Buffer, encoding?: BufferEncoding): Buffer { + if (encoding) { + return Buffer.from(createHash('sha256').update(data).digest(encoding)); + } + return createHash('sha256').update(data).digest(); } /** @arg {string|Buffer} data @arg {string} [digest = null] - 'hex', 'binary' or 'base64' @return {string|Buffer} - Buffer when digest is null, or string */ -export function sha512(data: string | Buffer, encoding?: BufferEncoding): string | Buffer { - return createHash('sha512').update(data).digest(encoding); +export function sha512(data: string | Buffer, encoding?: BufferEncoding): Buffer { + if (encoding) { + return Buffer.from(createHash('sha512').update(data).digest(encoding)); + } + return createHash('sha512').update(data).digest(); } export function HmacSHA256(buffer: Buffer, secret: Buffer): Buffer { diff --git a/src/auth/ecc/src/index.ts b/src/auth/ecc/src/index.ts new file mode 100644 index 00000000..91b7892a --- /dev/null +++ b/src/auth/ecc/src/index.ts @@ -0,0 +1,9 @@ +export { Address } from './address'; +export { Aes } from './aes'; +export { PrivateKey } from './key_private'; +export { PublicKey } from './key_public'; +export { Signature } from './signature'; +export { normalize as brainKey } from './brain_key'; +export * as key_utils from './key_utils'; +export * as hash from './hash'; +export { Config as ecc_config } from '../../../config'; \ No newline at end of file diff --git a/src/auth/ecc/key_private.ts b/src/auth/ecc/src/key_private.ts similarity index 91% rename from src/auth/ecc/key_private.ts rename to src/auth/ecc/src/key_private.ts index d8b43567..b3664b7c 100644 --- a/src/auth/ecc/key_private.ts +++ b/src/auth/ecc/src/key_private.ts @@ -1,5 +1,4 @@ import ecurve from 'ecurve'; -import { Point } from 'ecurve'; const secp256k1 = ecurve.getCurveByName('secp256k1'); import BigInteger from 'bigi'; import base58 from 'bs58'; @@ -7,9 +6,11 @@ import assert from 'assert'; import * as hash from './hash'; import { PublicKey } from './key_public'; +// Use any type to avoid namespace issues +type Point = any; + const G = secp256k1.G; const n = secp256k1.n; - export class PrivateKey { d: BigInteger; public_key?: PublicKey; @@ -110,9 +111,12 @@ export class PrivateKey { /** ECIES */ get_shared_secret(public_key: PublicKey | string): Buffer { - public_key = toPublic(public_key); - const KB = public_key.toUncompressed().toBuffer(); - const KBP = Point.fromAffine( + const pubKey = toPublic(public_key); + if (!pubKey) { + throw new Error('Invalid public key'); + } + const KB = pubKey.toUncompressed().toBuffer(); + const KBP = ecurve.Point.fromAffine( secp256k1, BigInteger.fromBuffer(KB.slice(1, 33)), // x BigInteger.fromBuffer(KB.slice(33, 65)) // y @@ -156,8 +160,9 @@ export class PrivateKey { } } -const toPublic = (data: PublicKey | string | null): PublicKey | null => { - if (data == null) return data; - if ('Q' in data) return data as PublicKey; - return PublicKey.fromStringOrThrow(data as string); +const toPublic = (data: PublicKey | string): PublicKey => { + if (typeof data === 'string') { + return PublicKey.fromStringOrThrow(data); + } + return data; }; \ No newline at end of file diff --git a/src/auth/ecc/key_public.ts b/src/auth/ecc/src/key_public.ts similarity index 76% rename from src/auth/ecc/key_public.ts rename to src/auth/ecc/src/key_public.ts index d81d4ee7..dda39b72 100644 --- a/src/auth/ecc/key_public.ts +++ b/src/auth/ecc/src/key_public.ts @@ -3,9 +3,11 @@ import ecurve from 'ecurve'; const secp256k1 = ecurve.getCurveByName('secp256k1'); import base58 from 'bs58'; import * as hash from './hash'; -import { Config } from '../../config'; +import { getConfig } from '../../../config'; import assert from 'assert'; -import { Point } from 'ecurve'; + +// Use any type to avoid namespace issues +type Point = any; const G = secp256k1.G; const n = secp256k1.n; @@ -57,7 +59,7 @@ export class PublicKey { return hash.ripemd160(pub_sha); } - toString(address_prefix = Config.getAddressPrefix()): string { + toString(address_prefix = getConfig().get('address_prefix')): string { return this.toPublicKeyString(address_prefix); } @@ -65,7 +67,7 @@ export class PublicKey { * Full public key * {return} string */ - toPublicKeyString(address_prefix = Config.getAddressPrefix()): string { + toPublicKeyString(address_prefix = getConfig().get('address_prefix')): string { if (this.pubdata) return address_prefix + this.pubdata; const pub_buf = this.toBuffer(); const checksum = hash.ripemd160(pub_buf); @@ -80,7 +82,7 @@ export class PublicKey { * @return PublicKey or `null` (if the public_key string is invalid) * @deprecated fromPublicKeyString (use fromString instead) */ - static fromString(public_key: string, address_prefix = Config.getAddressPrefix()): PublicKey | null { + static fromString(public_key: string, address_prefix = getConfig().get('address_prefix')): PublicKey | null { try { return PublicKey.fromStringOrThrow(public_key, address_prefix); } catch (e) { @@ -94,7 +96,7 @@ export class PublicKey { * @throws {Error} if public key is invalid * @return PublicKey */ - static fromStringOrThrow(public_key: string, address_prefix = Config.getAddressPrefix()): PublicKey { + static fromStringOrThrow(public_key: string, address_prefix = getConfig().get('address_prefix')): PublicKey { const prefix = public_key.slice(0, address_prefix.length); assert.equal( address_prefix, prefix, @@ -112,26 +114,25 @@ export class PublicKey { return PublicKey.fromBuffer(key); } - toAddressString(address_prefix = Config.getAddressPrefix()): string { + toAddressString(address_prefix = getConfig().get('address_prefix')): string { const pub_buf = this.toBuffer(); const pub_sha = hash.sha512(pub_buf) as Buffer; - let addy = hash.ripemd160(pub_sha); + const addy = hash.ripemd160(pub_sha); const checksum = hash.ripemd160(addy); - addy = Buffer.concat([addy, checksum.slice(0, 4)]); - return address_prefix + base58.encode(addy); + const addr_checksum = Buffer.concat([addy, checksum.slice(0, 4)]); + return address_prefix + base58.encode(addr_checksum); } toPtsAddy(): string { const pub_buf = this.toBuffer(); - const pub_sha = hash.sha256(pub_buf) as Buffer; + const pub_sha = hash.sha256(pub_buf); const addy = hash.ripemd160(pub_sha); - addy.writeUInt8(0x38, 0); //version 56(decimal) - - let checksum = hash.sha256(addy) as Buffer; - checksum = hash.sha256(checksum) as Buffer; - - const addy_checksum = Buffer.concat([addy, checksum.slice(0, 4)]); - return base58.encode(addy_checksum); + const versionBuffer = Buffer.from([0x38]); // version 56(decimal) + const addr = Buffer.concat([versionBuffer, addy]); + let checksum = hash.sha256(addr); + checksum = hash.sha256(checksum); + const addr_checksum = Buffer.concat([addr, checksum.slice(0, 4)]); + return base58.encode(addr_checksum); } child(offset: Buffer): PublicKey { @@ -156,10 +157,18 @@ export class PublicKey { } static fromHex(hex: string): PublicKey { - return PublicKey.fromBuffer(Buffer.from(hex, 'hex')); + const buffer = Buffer.from(hex, 'hex'); + if (buffer.length === 0) { + // Return null public key for zero hex + return new PublicKey(null); + } + return PublicKey.fromBuffer(buffer); } toHex(): string { + if (!this.Q) { + return '000000000000000000000000000000000000000000000000000000000000000000'; + } return this.toBuffer().toString('hex'); } diff --git a/src/auth/ecc/key_utils.ts b/src/auth/ecc/src/key_utils.ts similarity index 95% rename from src/auth/ecc/key_utils.ts rename to src/auth/ecc/src/key_utils.ts index 914cf332..9f97933d 100644 --- a/src/auth/ecc/key_utils.ts +++ b/src/auth/ecc/src/key_utils.ts @@ -36,7 +36,7 @@ export function random32ByteBuffer(entropy: string = browserEntropy()): Buffer { const start_t = Date.now(); while (Date.now() - start_t < HASH_POWER_MILLS) { - entropy = hash.sha256(entropy) as string; + entropy = hash.sha256(entropy).toString('hex'); } const hash_array: Buffer[] = []; @@ -66,7 +66,7 @@ export function browserEntropy(): string { console.log("INFO\tbrowserEntropy gathered", entropyCount, 'events'); } catch (error) { // nodejs: ReferenceError: window is not defined - entropyStr += hash.sha256((new Date()).toString()) as string; + entropyStr += hash.sha256((new Date()).toString()).toString('hex'); } const b = Buffer.from(entropyStr); diff --git a/src/auth/signature.ts b/src/auth/ecc/src/signature.ts similarity index 95% rename from src/auth/signature.ts rename to src/auth/ecc/src/signature.ts index d5797846..52abe1b9 100644 --- a/src/auth/signature.ts +++ b/src/auth/ecc/src/signature.ts @@ -1,7 +1,8 @@ -import { Point, getCurveByName } from 'ecurve'; +import { getCurveByName } from 'ecurve'; import bigi from 'bigi'; import { sha256 } from './hash'; -import { PrivateKey, PublicKey } from './key_classes'; +import { PrivateKey } from './key_private'; +import { PublicKey } from './key_public'; const secp256k1 = getCurveByName('secp256k1'); @@ -57,7 +58,7 @@ export class Signature { const e = bigi.fromBuffer(buf_sha256); const n = secp256k1.n; const G = secp256k1.G; - const d = privKey.getPrivateKey(); + const d = privKey.d; let r: bigi, s: bigi; let nonce = 0; diff --git a/src/auth/hash.ts b/src/auth/hash.ts deleted file mode 100644 index 5e0ff866..00000000 --- a/src/auth/hash.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { createHash } from 'crypto'; - -export function sha256(data: Buffer): Buffer { - return createHash('sha256').update(data).digest(); -} - -export function sha512(data: Buffer): Buffer { - return createHash('sha512').update(data).digest(); -} - -export function ripemd160(data: Buffer): Buffer { - return createHash('ripemd160').update(data).digest(); -} \ No newline at end of file diff --git a/src/auth/index.ts b/src/auth/index.ts index f9ce41fe..b7cd0fcd 100644 --- a/src/auth/index.ts +++ b/src/auth/index.ts @@ -1,16 +1,11 @@ -import { PrivateKey } from './key_private'; -import { PublicKey } from './key_public'; -import { sha256 } from './hash'; +import { PrivateKey } from './ecc/src/key_private'; +import { PublicKey } from './ecc/src/key_public'; +import { sha256 } from './ecc/src/hash'; import { getConfig } from '../config'; import bs58 from 'bs58'; -import { createHash } from 'crypto'; -import { Signature } from './signature'; -import bigi from 'bigi'; -import { Point, getCurveByName } from 'ecurve'; +import { Signature } from './ecc/src/signature'; import { transaction, signed_transaction } from './serializer'; -const secp256k1 = getCurveByName('secp256k1'); - export interface KeyPair { privateKey: string; publicKey: string; @@ -24,16 +19,6 @@ export interface Authority { key_auths: [string, number][]; } -const BASE58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; - -function toBase58(buffer: Buffer): string { - let result = ''; - for (let i = 0; i < buffer.length; i++) { - result += BASE58[buffer[i] % 58]; - } - return result; -} - export interface Auth { verify(name: string, password: string, auths: any): boolean; generateKeys(name: string, password: string, roles: string[]): { [key: string]: string }; @@ -162,21 +147,23 @@ export const wifToPublic = Auth.wifToPublic.bind(Auth); export const isPubkey = Auth.isPubkey.bind(Auth); // Export classes -export { PrivateKey } from './key_private'; -export { PublicKey } from './key_public'; -export { Address } from './address'; +export { PrivateKey } from './ecc/src/key_private'; +export { PublicKey } from './ecc/src/key_public'; +export { Address } from './ecc/src/address'; // Export crypto functions export const sign = (message: string, privateKey: string): string => { const priv = PrivateKey.fromWif(privateKey); - return priv.sign(Buffer.from(message)).toHex(); + const sig = Signature.signBuffer(Buffer.from(message), priv); + return sig.toHex(); }; export const verifySignature = (message: string, signature: string, publicKey: string): boolean => { try { const pub = PublicKey.fromString(publicKey); - const sig = Signature.fromHex(signature); - return sig.verifyBuffer(Buffer.from(message), pub); + if (!pub) return false; + const sigObj = Signature.fromHex(signature); + return sigObj.verifyBuffer(Buffer.from(message), pub); } catch { return false; } @@ -185,6 +172,7 @@ export const verifySignature = (message: string, signature: string, publicKey: s export const verifyTransaction = (transaction: any, publicKey: string): boolean => { try { const pub = PublicKey.fromString(publicKey); + if (!pub) return false; const serialized = Buffer.from(JSON.stringify(transaction)); return transaction.signatures.some((sig: string) => { const signature = Signature.fromHex(sig); diff --git a/src/auth/key_classes.ts b/src/auth/key_classes.ts index 90c4a649..54da1ccf 100644 --- a/src/auth/key_classes.ts +++ b/src/auth/key_classes.ts @@ -1,3 +1,3 @@ -export { PrivateKey } from './key_private'; -export { PublicKey } from './key_public'; -export { Address } from './address'; \ No newline at end of file +export { PrivateKey } from './ecc/src/key_private'; +export { PublicKey } from './ecc/src/key_public'; +export { Address } from './ecc/src/address'; \ No newline at end of file diff --git a/src/auth/key_private.ts b/src/auth/key_private.ts deleted file mode 100644 index 38030a42..00000000 --- a/src/auth/key_private.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { Point, getCurveByName } from 'ecurve'; -import bigi from 'bigi'; -import bs58 from 'bs58'; -import { sha256, sha512, ripemd160 } from './hash'; -import { getConfig } from '../config'; -import { PublicKey } from './key_public'; -import { Signature } from './signature'; - -const secp256k1 = getCurveByName('secp256k1'); -const G = secp256k1.G; -const n = secp256k1.n; - -export interface KeyPair { - privateKey: string; - publicKey: string; -} - -export class PrivateKey { - private d: bigi; - private public_key?: PublicKey; - - constructor(d: bigi) { - this.d = d; - } - - getPrivateKey(): bigi { - return this.d; - } - - static fromWif(wif: string): PrivateKey { - const version = Number(getConfig().get('wifPrefix')) || 128; - const decoded = bs58.decode(wif); - const versionLength = 1; - const keyLength = 32; - const checksumLength = 4; - - const versionBytes = Buffer.from(decoded.slice(0, versionLength)); - const keyBytes = Buffer.from(decoded.slice(versionLength, versionLength + keyLength)); - const checksumBytes = Buffer.from(decoded.slice(versionLength + keyLength, versionLength + keyLength + checksumLength)); - - const keyString = Buffer.from(decoded.slice(0, versionLength + keyLength)); - const checksum = sha256(keyString); - const checksum2 = sha256(checksum); - - if (checksumBytes.toString('hex') !== checksum2.slice(0, 4).toString('hex')) { - throw new Error('Invalid checksum'); - } - - if (versionBytes[0] !== version) { - throw new Error('Invalid version'); - } - - return new PrivateKey(bigi.fromBuffer(keyBytes)); - } - - toWif(): string { - const version = Number(getConfig().get('wifPrefix')) || 128; - const keyBytes = this.toBuffer(); - const versionBytes = Buffer.from([version]); - const keyString = Buffer.concat([versionBytes, keyBytes]); - const checksum = sha256(keyString); - const checksum2 = sha256(checksum); - const checksumBytes = checksum2.slice(0, 4); - const wif = Buffer.concat([versionBytes, keyBytes, checksumBytes]); - return bs58.encode(wif); - } - - toPublic(): PublicKey { - return this.toPublicKey(); - } - - toPublicKey(): PublicKey { - const Q = G.multiply(this.d); - return new PublicKey(Q); - } - - toBuffer(): Buffer { - return this.d.toBuffer(32); - } - - get_shared_secret(public_key: PublicKey | string): Buffer { - const pub = typeof public_key === 'string' ? PublicKey.fromString(public_key) : public_key; - const P = pub.Q!.multiply(this.d); - const S = P.affineX.toBuffer(32); - return sha512(S); - } - - static fromBuffer(buffer: Buffer): PrivateKey { - return new PrivateKey(bigi.fromBuffer(buffer)); - } - - static fromHex(hex: string): PrivateKey { - return PrivateKey.fromBuffer(Buffer.from(hex, 'hex')); - } - - /** Generate a private key from a seed string */ - static fromSeed(seed: string): PrivateKey { - if (typeof seed !== 'string') { - throw new Error('seed must be of type string'); - } - return PrivateKey.fromBuffer(sha256(Buffer.from(seed))); - } - - toHex(): string { - return this.toBuffer().toString('hex'); - } - - /** Sign a buffer with this private key */ - sign(buf: Buffer): Signature { - return Signature.signBuffer(buf, this); - } - - /** Derive a child private key from this private key */ - child(offset: Buffer): PrivateKey { - if (!Buffer.isBuffer(offset)) { - throw new Error('Buffer required: offset'); - } - if (offset.length !== 32) { - throw new Error('offset length must be 32 bytes'); - } - - const offsetHash = Buffer.concat([this.toPublic().toBuffer(), offset]); - const c = bigi.fromBuffer(sha256(offsetHash)); - - if (c.compareTo(n) >= 0) { - throw new Error('Child offset went out of bounds, try again'); - } - - const derived = this.d.add(c); - - if (derived.signum() === 0) { - throw new Error('Child offset derived to an invalid key, try again'); - } - - return new PrivateKey(derived); - } - - /** Validate the private key */ - validate(): void { - if (!this.d) { - throw new Error('Invalid private key: d is null'); - } - if (this.d.signum() <= 0) { - throw new Error('Invalid private key: d must be positive'); - } - if (this.d.compareTo(n) >= 0) { - throw new Error('Invalid private key: d must be less than n'); - } - } - - /** Check if the private key is valid */ - isValid(): boolean { - try { - this.validate(); - return true; - } catch (e) { - return false; - } - } - - /** Generate a new random private key */ - static random(): PrivateKey { - const randomBytes = Buffer.alloc(32); - for (let i = 0; i < 32; i++) { - randomBytes[i] = Math.floor(Math.random() * 256); - } - const d = bigi.fromBuffer(randomBytes).mod(n); - return new PrivateKey(d); - } - - /** Generate a key pair from this private key */ - toKeyPair(): KeyPair { - const publicKey = this.toPublic(); - return { - privateKey: this.toWif(), - publicKey: publicKey.toString() - }; - } - - /** Generate a new random key pair */ - static randomKeyPair(): KeyPair { - return PrivateKey.random().toKeyPair(); - } -} \ No newline at end of file diff --git a/src/auth/key_public.ts b/src/auth/key_public.ts deleted file mode 100644 index ff80b0e7..00000000 --- a/src/auth/key_public.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { Point, getCurveByName } from 'ecurve'; -import bigi from 'bigi'; -import bs58 from 'bs58'; -import { sha256, ripemd160, sha512 } from './hash'; -import { getConfig } from '../config'; - -const secp256k1 = getCurveByName('secp256k1'); - -export class PublicKey { - Q: Point | null; - - constructor(Q: Point | null) { - this.Q = Q; - } - - static fromBinary(bin: string): PublicKey { - return PublicKey.fromBuffer(Buffer.from(bin, 'binary')); - } - - static fromBuffer(buffer: Buffer): PublicKey { - if ( - buffer.toString("hex") === - "000000000000000000000000000000000000000000000000000000000000000000" - ) { - return new PublicKey(null); - } - // Handle both compressed (33 bytes) and uncompressed (65 bytes) buffers - if (buffer.length === 33 || buffer.length === 65) { - return new PublicKey(Point.decodeFrom(secp256k1, buffer)); - } - throw new Error('Invalid public key buffer length: ' + buffer.length); - } - - toBuffer(compressed?: boolean): Buffer { - if (this.Q === null) { - return Buffer.from( - "000000000000000000000000000000000000000000000000000000000000000000", - "hex" - ); - } - // Default to compressed if not specified - return this.Q.getEncoded(compressed !== false); - } - - toUncompressed(): PublicKey { - if (this.Q === null) { - return new PublicKey(null); - } - const buf = this.Q.getEncoded(false); // uncompressed - return PublicKey.fromBuffer(buf); - } - - toString(address_prefix: string = String(getConfig().get('address_prefix')) || 'STM'): string { - return this.toPublicKeyString(address_prefix); - } - - static fromString(public_key: string, address_prefix: string = String(getConfig().get('address_prefix')) || 'STM'): PublicKey { - try { - return PublicKey.fromStringOrThrow(public_key, address_prefix); - } catch (e) { - throw new Error('Invalid public key'); - } - } - - static fromStringOrThrow(public_key: string, address_prefix: string = String(getConfig().get('address_prefix')) || 'STM'): PublicKey { - const prefix = public_key.slice(0, address_prefix.length); - if (prefix !== address_prefix) { - throw new Error(`Expecting key to begin with ${address_prefix}, instead saw ${prefix}`); - } - let keyString = public_key.slice(address_prefix.length); - const decoded = Buffer.from(bs58.decode(keyString)); - const keyBytes = decoded.slice(0, decoded.length - 4); - const checksum = decoded.slice(decoded.length - 4); - const new_checksum = ripemd160(keyBytes).slice(0, 4); - if (!checksum.equals(new_checksum)) { - throw new Error('Checksum did not match'); - } - return PublicKey.fromBuffer(keyBytes); - } - - static fromHex(hex: string): PublicKey { - return PublicKey.fromBuffer(Buffer.from(hex, 'hex')); - } - - toHex(): string { - return this.toBuffer().toString('hex'); - } - - /** Validate the public key */ - validate(): void { - if (!this.Q) { - throw new Error('Invalid public key: Q is null'); - } - if (secp256k1.isInfinity(this.Q)) { - throw new Error('Invalid public key: point is at infinity'); - } - // Check if the point is on the curve - if (!secp256k1.isOnCurve(this.Q)) { - throw new Error('Invalid public key: point is not on the curve'); - } - } - - /** Check if the public key is valid */ - isValid(): boolean { - try { - this.validate(); - return true; - } catch (e) { - return false; - } - } - - /** Check if the public key is the zero key */ - isZero(): boolean { - return this.Q === null; - } - - static fromStringHex(hex: string): PublicKey { - return PublicKey.fromString(Buffer.from(hex, 'hex').toString()); - } - - toPublicKeyString(address_prefix: string = String(getConfig().get('address_prefix')) || 'STM'): string { - const pub_buf = this.toBuffer(); - const checksum = ripemd160(pub_buf); - const addy = Buffer.concat([pub_buf, checksum.slice(0, 4)]); - return address_prefix + bs58.encode(addy); - } - - toAddressString(address_prefix: string = String(getConfig().get('address_prefix')) || 'STM'): string { - const pub_buf = this.toBuffer(); - const pub_sha = sha512(pub_buf); - let addy = ripemd160(pub_sha); - const checksum = ripemd160(addy); - addy = Buffer.concat([addy, checksum.slice(0, 4)]); - return address_prefix + bs58.encode(addy); - } - - toPtsAddy(): string { - const pub_buf = this.toBuffer(); - let addy = ripemd160(sha256(pub_buf)); - addy = Buffer.concat([Buffer.from([0x38]), addy]); // version 56 decimal - let checksum = sha256(addy); - checksum = sha256(checksum); - addy = Buffer.concat([addy, checksum.slice(0, 4)]); - return bs58.encode(addy); - } - - /** Generate a blockchain address from the public key */ - toBlockchainAddress(): Buffer { - const pub_buf = this.toBuffer(); - const pub_sha = sha512(pub_buf); - return ripemd160(pub_sha); - } - - /** Derive a child public key using the given offset */ - child(offset: Buffer): PublicKey { - if (!Buffer.isBuffer(offset)) { - throw new Error("Buffer required: offset"); - } - if (offset.length !== 32) { - throw new Error("offset length must be 32 bytes"); - } - - const offsetBuffer = Buffer.concat([this.toBuffer(), offset]); - const offsetHash = sha256(offsetBuffer); - const c = bigi.fromBuffer(offsetHash); - - if (c.compareTo(secp256k1.n) >= 0) { - throw new Error("Child offset went out of bounds, try again"); - } - - const cG = secp256k1.G.multiply(c); - const Qprime = this.Q!.add(cG); - - if (secp256k1.isInfinity(Qprime)) { - throw new Error("Child offset derived to an invalid key, try again"); - } - - return new PublicKey(Qprime); - } - - /** Check if two public keys are equal */ - equals(other: PublicKey): boolean { - if (!this.Q || !other.Q) { - return this.Q === other.Q; - } - return this.Q.equals(other.Q); - } - - /** Get the compressed form of the public key */ - toCompressed(): PublicKey { - const buf = this.toBuffer(); - return PublicKey.fromBuffer(buf); - } -} \ No newline at end of file diff --git a/src/auth/key_utils.ts b/src/auth/key_utils.ts deleted file mode 100644 index 09a182bd..00000000 --- a/src/auth/key_utils.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { PrivateKey } from './key_private'; -import { sha256 } from './hash'; -import { randomBytes } from 'crypto'; - -// hash for .25 second -const HASH_POWER_MILLS = 250; - -let entropyPos = 0, entropyCount = 0; -const entropyArray = randomBytes(101); - -export const key_utils = { - addEntropy(...ints: number[]) { - entropyCount++; - for (const i of ints) { - const pos = entropyPos++ % 101; - const i2 = entropyArray[pos] += i; - if (i2 > 9007199254740991) { - entropyArray[pos] = 0; - } - } - }, - - /** - * A week random number generator can run out of entropy. This should ensure even the worst random number implementation will be reasonably safe. - * @param entropy string entropy of at least 32 bytes - */ - random32ByteBuffer(entropy: string = key_utils.browserEntropy()): Buffer { - if (typeof entropy !== 'string') { - throw new Error("string required for entropy"); - } - - if (entropy.length < 32) { - throw new Error("expecting at least 32 bytes of entropy"); - } - - const start_t = Date.now(); - - while (Date.now() - start_t < HASH_POWER_MILLS) { - entropy = sha256(Buffer.from(entropy, 'hex')).toString('hex'); - } - - const hash_array: Buffer[] = []; - hash_array.push(Buffer.from(entropy, 'hex')); - - // Hashing for 1 second may helps the computer is not low on entropy (this method may be called back-to-back). - hash_array.push(randomBytes(32)); - - return sha256(Buffer.concat(hash_array)); - }, - - get_random_key(entropy: string): PrivateKey { - return PrivateKey.fromBuffer(key_utils.random32ByteBuffer(entropy)); - }, - - browserEntropy(): string { - const entropy = [ - Date.now(), - Math.random(), - entropyCount, - entropyPos, - entropyArray[entropyPos % 101] - ].join(''); - return sha256(Buffer.from(entropy)).toString('hex'); - } -}; diff --git a/src/auth/serializer.ts b/src/auth/serializer.ts index c814320a..773cb802 100644 --- a/src/auth/serializer.ts +++ b/src/auth/serializer.ts @@ -1,6 +1,6 @@ import ByteBuffer from 'bytebuffer'; import Long from 'long'; -import { PublicKey } from './key_public'; +import { PublicKey } from './ecc/src/key_public'; export interface EncryptedMemo { from: PublicKey; diff --git a/src/broadcast/index.ts b/src/broadcast/index.ts index 4cb36e1b..049ce7e3 100644 --- a/src/broadcast/index.ts +++ b/src/broadcast/index.ts @@ -1,6 +1,6 @@ import { Api } from '../api'; import Auth from '../auth'; -import { createOperation, createTransaction, createSignedTransaction, BroadcastOptions } from './helpers'; +import { createOperation, createTransaction, BroadcastOptions } from './helpers'; import { operations } from './operations'; import { camelCase } from '../utils'; import { promisify } from 'util'; @@ -243,10 +243,14 @@ export function setApi(api: any): void { } // Implement the most commonly used methods -broadcastMethods.vote = function(wif: string, voter: string, author: string, permlink: string, weight: number, callback: any) { +broadcastMethods.vote = function(wif: string, voter: string, author: string, permlink: string, weight: number, callback?: any) { // For tests, 'this' may have the api as a property const api = this && this.api ? this.api : steem.api; + if (typeof callback !== 'function') { + callback = undefined; + } + try { const params = { voter, @@ -260,36 +264,60 @@ broadcastMethods.vote = function(wif: string, voter: string, author: string, per extensions: [] }; - broadcast(api, transaction) - .then(result => callback(null, result)) - .catch(error => callback(error)); + if (callback) { + broadcast(api, transaction) + .then(result => callback(null, result)) + .catch(error => callback(error)); + } else { + return broadcast(api, transaction); + } } catch (error) { - callback(error); + if (callback) { + callback(error); + } else { + throw error; + } } }; -broadcastMethods.voteWith = function(options: any, callback: any) { +broadcastMethods.voteWith = function(wif: string, options: any, callback?: any) { // For tests, 'this' may have the api as a property const api = this && this.api ? this.api : steem.api; + if (typeof callback !== 'function') { + callback = undefined; + } + try { const transaction = { operations: [['vote', options]], extensions: [] }; - broadcast(api, transaction) - .then(result => callback(null, result)) - .catch(error => callback(error)); + if (callback) { + broadcast(api, transaction) + .then(result => callback(null, result)) + .catch(error => callback(error)); + } else { + return broadcast(api, transaction); + } } catch (error) { - callback(error); + if (callback) { + callback(error); + } else { + throw error; + } } }; -broadcastMethods.comment = function(wif: string, parentAuthor: string, parentPermlink: string, author: string, permlink: string, title: string, body: string, jsonMetadata: any, callback: any) { +broadcastMethods.comment = function(wif: string, parentAuthor: string, parentPermlink: string, author: string, permlink: string, title: string, body: string, jsonMetadata: any, callback?: any) { // For tests, 'this' may have the api as a property const api = this && this.api ? this.api : steem.api; + if (typeof callback !== 'function') { + callback = undefined; + } + try { const params = { parent_author: parentAuthor, @@ -306,18 +334,30 @@ broadcastMethods.comment = function(wif: string, parentAuthor: string, parentPer extensions: [] }; - broadcast(api, transaction) - .then(result => callback(null, result)) - .catch(error => callback(error)); + if (callback) { + broadcast(api, transaction) + .then(result => callback(null, result)) + .catch(error => callback(error)); + } else { + return broadcast(api, transaction); + } } catch (error) { - callback(error); + if (callback) { + callback(error); + } else { + throw error; + } } }; -broadcastMethods.customJson = function(wif: string, requiredPostingAuths: string[], id: string, customJson: any, callback: any) { +broadcastMethods.customJson = function(wif: string, requiredPostingAuths: string[], id: string, customJson: any, callback?: any) { // For tests, 'this' may have the api as a property const api = this && this.api ? this.api : steem.api; + if (typeof callback !== 'function') { + callback = undefined; + } + try { const params = { required_auths: [], @@ -331,11 +371,19 @@ broadcastMethods.customJson = function(wif: string, requiredPostingAuths: string extensions: [] }; - broadcast(api, transaction) - .then(result => callback(null, result)) - .catch(error => callback(error)); + if (callback) { + broadcast(api, transaction) + .then(result => callback(null, result)) + .catch(error => callback(error)); + } else { + return broadcast(api, transaction); + } } catch (error) { - callback(error); + if (callback) { + callback(error); + } else { + throw error; + } } }; diff --git a/src/crypto/index.ts b/src/crypto/index.ts index e2d6d91b..84951a38 100644 --- a/src/crypto/index.ts +++ b/src/crypto/index.ts @@ -29,13 +29,13 @@ export const generateKeyPair = (): KeyPair => { }; }; -export const sign = (message: string | Buffer, privateKey: string): string => { +export const sign = (_message: string | Buffer, privateKey: string): string => { // Implementation of message signing // This is a placeholder - actual implementation would use proper cryptographic methods return `signature_${privateKey.slice(0, 10)}`; }; -export const verify = (message: string | Buffer, signature: string, publicKey: string): boolean => { +export const verify = (_message: string | Buffer, signature: string, _publicKey: string): boolean => { // Implementation of signature verification // This is a placeholder - actual implementation would use proper cryptographic methods return signature.startsWith('signature_'); diff --git a/src/formatter/index.ts b/src/formatter/index.ts index 913c4caf..1ed96691 100644 --- a/src/formatter/index.ts +++ b/src/formatter/index.ts @@ -1,5 +1,5 @@ import get from 'lodash/get'; -import { PrivateKey } from '../auth/ecc/key_private'; +import { PrivateKey } from '../auth/ecc/src/key_private'; import { Api } from '../api'; export interface Account { @@ -108,10 +108,6 @@ export class Formatter { this.api = api; } - private numberWithCommas(x: string): string { - return x.replace(/\B(?=(\d{3})+(?!\d))/g, ','); - } - private vestingSteem(account: Account, gprops: GlobalProperties): number { const vests = parseFloat(account.vesting_shares.split(' ')[0]); const total_vests = parseFloat(gprops.total_vesting_shares.split(' ')[0]); @@ -300,15 +296,6 @@ export class Formatter { } } -/** - * Add commas as thousands separators to a number string. - * @param x Number string - * @returns String with commas - */ -export function numberWithCommas(x: string): string { - return x.replace(/\B(?=(\d{3})+(?!\d))/g, ','); -} - /** * Calculate vesting STEEM from vesting shares and global properties. * @param account Account object diff --git a/src/global.d.ts b/src/global.d.ts new file mode 100644 index 00000000..86b19498 --- /dev/null +++ b/src/global.d.ts @@ -0,0 +1,8 @@ +declare module 'create-hash'; +declare module 'create-hmac'; +declare module 'secure-random'; +declare module 'lodash/get'; +declare module 'bigi'; +declare module 'bs58'; +declare module 'ecurve'; +declare module 'bytebuffer'; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 2cd2b571..df9d05aa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,7 +7,6 @@ import * as memo from './memo'; import * as operations from './operations'; import * as serializer from './serializer'; import * as utils from './utils'; -import * as types from './serializer/types'; // Create the API instance const api = new Api(); @@ -40,5 +39,6 @@ if (typeof broadcast.setApi === 'function') { broadcast.setApi(api); } -export default steem; +// Export everything as named exports +export { steem }; export * from './crypto'; \ No newline at end of file diff --git a/src/memo/index.ts b/src/memo/index.ts index d7c863a9..8ca332b4 100644 --- a/src/memo/index.ts +++ b/src/memo/index.ts @@ -1,6 +1,6 @@ -import { PrivateKey } from '../auth/key_private'; -import { PublicKey } from '../auth/key_public'; -import { Aes } from '../auth/aes'; +import { PrivateKey } from '../auth/ecc/src/key_private'; +import { PublicKey } from '../auth/ecc/src/key_public'; +import { Aes } from '../auth/ecc/src/aes'; import bs58 from 'bs58'; import ByteBuffer from 'bytebuffer'; import { Serializer, EncryptedMemo } from '../auth/serializer'; diff --git a/src/serializer/types.ts b/src/serializer/types.ts index 34eeaf5d..21b07c5a 100644 --- a/src/serializer/types.ts +++ b/src/serializer/types.ts @@ -75,7 +75,7 @@ export const vote_id = { } }; -export const set = (type: any) => ({ +export const set = (_type: any) => ({ fromObject: (arr: any[]): any[] => { if (!Array.isArray(arr)) { throw new Error('Expected array for set type'); @@ -96,12 +96,14 @@ export const set = (type: any) => ({ return [...arr].sort((a, b) => { if (typeof a === 'number' && typeof b === 'number') return a - b; if (Buffer.isBuffer(a) && Buffer.isBuffer(b)) return a.toString('hex').localeCompare(b.toString('hex')); + if (typeof a === 'string' && typeof b === 'string') return a.localeCompare(b); return a.toString().localeCompare(b.toString()); }); }, toObject: (set: any[]): any[] => [...set].sort((a, b) => { if (typeof a === 'number' && typeof b === 'number') return a - b; if (Buffer.isBuffer(a) && Buffer.isBuffer(b)) return a.toString('hex').localeCompare(b.toString('hex')); + if (typeof a === 'string' && typeof b === 'string') return a.localeCompare(b); return a.toString().localeCompare(b.toString()); }), toHex: (arr: any[]): string => { @@ -120,7 +122,7 @@ export const set = (type: any) => ({ } }); -export const map = (keyType: any, valueType: any) => ({ +export const map = (_keyType: any, _valueType: any) => ({ fromObject: (arr: [any, any][]): [any, any][] => { if (!Array.isArray(arr)) { throw new Error('Expected array for map type'); @@ -142,6 +144,7 @@ export const map = (keyType: any, valueType: any) => ({ const ka = a[0], kb = b[0]; if (typeof ka === 'number' && typeof kb === 'number') return ka - kb; if (Buffer.isBuffer(ka) && Buffer.isBuffer(kb)) return ka.toString('hex').localeCompare(kb.toString('hex')); + if (typeof ka === 'string' && typeof kb === 'string') return ka.localeCompare(kb); return ka.toString().localeCompare(kb.toString()); }); }, @@ -149,6 +152,7 @@ export const map = (keyType: any, valueType: any) => ({ const ka = a[0], kb = b[0]; if (typeof ka === 'number' && typeof kb === 'number') return ka - kb; if (Buffer.isBuffer(ka) && Buffer.isBuffer(kb)) return ka.toString('hex').localeCompare(kb.toString('hex')); + if (typeof ka === 'string' && typeof kb === 'string') return ka.localeCompare(kb); return ka.toString().localeCompare(kb.toString()); }), toHex: (arr: [any, any][]): string => { @@ -256,7 +260,7 @@ export const type_id = { } }; -export const protocol_id_type = (name: string) => ({ +export const protocol_id_type = (_name: string) => ({ toHex: (value: number): string => { const buffer = new ByteBuffer(8, ByteBuffer.LITTLE_ENDIAN); buffer.writeUint64(value); diff --git a/src/types/ecurve.d.ts b/src/types/ecurve.d.ts index be072bf9..d24c1ae5 100644 --- a/src/types/ecurve.d.ts +++ b/src/types/ecurve.d.ts @@ -46,7 +46,7 @@ declare module 'ecurve' { isInfinity(Q: any): boolean; isOnCurve(Q: any): boolean; - pointFromX(isOdd: boolean, x: Point): Point; + pointFromX(isOdd: boolean, x: BigInteger): Point; validate(Q: any): boolean; static getCurveByName(name: string): Curve; diff --git a/test/api.test.ts b/test/api.test.ts index 82da2475..3b360247 100644 --- a/test/api.test.ts +++ b/test/api.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach, vi } from 'vitest'; -import steem from '../src'; +import { steem } from '../src'; import { Api } from '../src/api'; import testPost from './test-post.json'; diff --git a/test/broadcast.test.ts b/test/broadcast.test.ts index 727e167b..5467de43 100644 --- a/test/broadcast.test.ts +++ b/test/broadcast.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, beforeAll, beforeEach, vi, afterAll } from 'vitest'; -import steem from '../src'; +import { steem } from '../src'; import Promise from 'bluebird'; const username = process.env.STEEM_USERNAME || 'guest123'; diff --git a/test/comment.test.ts b/test/comment.test.ts index 9407f39f..54b1b3b3 100644 --- a/test/comment.test.ts +++ b/test/comment.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, beforeAll } from 'vitest'; import { createComment } from '../src/operations'; -import steem from '../src'; +import { steem } from '../src'; import pkg from '../package.json'; const broadcast = steem.broadcast; diff --git a/test/crypto.test.ts b/test/crypto.test.ts index 8fdc159b..f5a036ea 100644 --- a/test/crypto.test.ts +++ b/test/crypto.test.ts @@ -1,8 +1,8 @@ import { describe, it, expect } from 'vitest'; import { getConfig } from '../src/config'; import { PrivateKey, PublicKey } from '../src/auth'; -import { Signature } from '../src/auth/signature'; -import { sha256 } from '../src/auth/hash'; +import { Signature } from '../src/auth/ecc/src/signature'; +import { sha256 } from '../src/auth/ecc/src/hash'; // Set up config prefix to match original getConfig().set('address_prefix', 'STM'); diff --git a/test/hf20-accounts.test.ts b/test/hf20-accounts.test.ts index d95c8f03..02c7a9e9 100644 --- a/test/hf20-accounts.test.ts +++ b/test/hf20-accounts.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import steem from '../src'; +import { steem } from '../src'; const { auth, broadcast, api } = steem; const username = process.env.STEEM_USERNAME || 'guest123'; diff --git a/test/hf21-sps.test.ts b/test/hf21-sps.test.ts index ed802117..96e70beb 100644 --- a/test/hf21-sps.test.ts +++ b/test/hf21-sps.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import steem from '../src'; +import { steem } from '../src'; const { auth, broadcast, api } = steem; const username = process.env.STEEM_USERNAME || 'guest123'; diff --git a/test/memo.test.ts b/test/memo.test.ts index 0f60a584..d9a7133d 100644 --- a/test/memo.test.ts +++ b/test/memo.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest'; import { encode, decode } from '../src/memo'; -import { PrivateKey } from '../src/auth/key_private'; +import { PrivateKey } from '../src/auth/ecc/src/key_private'; describe('steem.auth: memo', () => { const private_key = PrivateKey.fromSeed("") diff --git a/test/reputation.test.ts b/test/reputation.test.ts index 24f37440..9a04b515 100644 --- a/test/reputation.test.ts +++ b/test/reputation.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import steem from '../src'; +import { steem } from '../src'; describe('steem.format.reputation', () => { const reputation = steem.formatter.reputation; diff --git a/test/smt.test.ts b/test/smt.test.ts index 1b9289e8..a522f842 100644 --- a/test/smt.test.ts +++ b/test/smt.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import steem from '../src'; +import { steem } from '../src'; const { auth, broadcast, api, config } = steem; const username = process.env.STEEM_USERNAME || 'guest123';