@@ -42,7 +42,7 @@ function doubleSha256(buf) {
4242 * ### Usage:
4343 * ```js
4444 * const signer = new TinySecp256k1({
45- * prefix : '\x18MyApp Signed Message:\n',
45+ * msgPrefix : '\x18MyApp Signed Message:\n',
4646 * privateKey: 'a1b2c3...',
4747 * privateKeyEncoding: 'hex'
4848 * });
@@ -64,18 +64,18 @@ class TinySecp256k1 {
6464 /** @typedef {import('elliptic') } Elliptic */
6565 /** @typedef {import('elliptic').ec } ec */
6666 /** @typedef {import('elliptic').ec.KeyPair } KeyPair */
67- #prefix = '\x18Tinychain Signed Message:\n' ;
67+ #msgPrefix = '\x18Tinychain Signed Message:\n' ;
6868
6969 /**
7070 * Creates an instance of TinySecp256k1.
7171 *
7272 * @param {Object } [options] - Optional parameters for the instance.
73- * @param {string|null } [options.prefix =null] - Message prefix used during message signing.
73+ * @param {string|null } [options.msgPrefix =null] - Message prefix used during message signing.
7474 * @param {string|null } [options.privateKey=null] - String representation of the private key.
7575 * @param {BufferEncoding } [options.privateKeyEncoding='hex'] - Encoding used for the privateKey string.
7676 */
77- constructor ( { prefix = null , privateKey = null , privateKeyEncoding = 'hex' } = { } ) {
78- if ( typeof prefix === 'string' ) this . #prefix = prefix ;
77+ constructor ( { msgPrefix = null , privateKey = null , privateKeyEncoding = 'hex' } = { } ) {
78+ if ( typeof msgPrefix === 'string' ) this . #msgPrefix = msgPrefix ;
7979 this . privateKey = privateKey ? Buffer . from ( privateKey , privateKeyEncoding ) : randomBytes ( 32 ) ;
8080 }
8181
@@ -84,7 +84,7 @@ class TinySecp256k1 {
8484 *
8585 * @returns {Promise<KeyPair> } The elliptic key pair.
8686 */
87- async init ( ) {
87+ async initEc ( ) {
8888 const ec = await this . fetchElliptic ( ) ;
8989 this . keyPair = ec . keyFromPrivate ( this . privateKey ) ;
9090 return this . keyPair ;
@@ -190,7 +190,7 @@ class TinySecp256k1 {
190190 */
191191 signECDSA ( message , encoding = 'utf8' ) {
192192 const msgBuffer = Buffer . isBuffer ( message ) ? message : Buffer . from ( message , encoding ) ;
193- const hash = sha256 ( msgBuffer ) ; // one SHA256 pass
193+ const hash = doubleSha256 ( msgBuffer ) ;
194194 const signature = this . getKeyPair ( ) . sign ( hash , { canonical : true } ) ;
195195 return Buffer . from ( signature . toDER ( ) ) ;
196196 }
@@ -207,60 +207,81 @@ class TinySecp256k1 {
207207 verifyECDSA ( message , signatureBuffer , pubKeyHex , encoding ) {
208208 const ec = this . getEc ( ) ;
209209 const msgBuffer = Buffer . isBuffer ( message ) ? message : Buffer . from ( message , encoding ) ;
210- const hash = sha256 ( msgBuffer ) ;
210+ const hash = doubleSha256 ( msgBuffer ) ;
211211 const key = ec . keyFromPublic ( pubKeyHex , 'hex' ) ;
212212 return key . verify ( hash , signatureBuffer ) ;
213213 }
214214
215215 /**
216- * Recovers the public key from a signed message .
216+ * Normalizes a 65-byte compact ECDSA signature into its r, s, and v components .
217217 *
218- * @param {string| Buffer } message - The original message string or buffer .
219- * @param { Buffer } signature - The signature buffer (must include recovery param) .
220- * @returns { string|null } Recovered public key in hex format or null if invalid .
218+ * @param {Buffer } signature - A 65-byte buffer in the format [r (32) | s (32) | v (1)] .
219+ * @returns { { r: Buffer, s: Buffer, v: number } } The signature components .
220+ * @throws { Error } If the signature length is invalid or recovery param is out of range .
221221 */
222- recoverMessage ( message , signature ) {
223- const ec = this . getEc ( ) ;
224- const msgBuffer = Buffer . isBuffer ( message ) ? message : Buffer . from ( message ) ;
225- const prefix = Buffer . from ( this . #prefix + msgBuffer . length ) ;
226- const fullMessage = Buffer . concat ( [ prefix , msgBuffer ] ) ;
227- const hash = doubleSha256 ( fullMessage ) ; // Bitcoin/Ethereum-style prefixing
222+ #normalizeSignature( signature ) {
223+ if ( signature . length === 65 ) {
224+ let v = signature [ 64 ] ;
225+ if ( v >= 27 ) v -= 27 ;
226+ if ( v < 0 || v > 3 ) throw new Error ( 'Invalid recovery param (v): must be 0, 1, 2, or 3' ) ;
227+ return {
228+ r : signature . subarray ( 0 , 32 ) ,
229+ s : signature . subarray ( 32 , 64 ) ,
230+ v,
231+ } ;
232+ } else throw new Error ( 'Invalid signature length. Expected 65 bytes (r+s+v)' ) ;
233+ }
228234
229- if ( signature . length !== 65 ) {
230- console . warn ( '[recoverMessage] Signature must be 65 bytes (r + s + recovery param).' ) ;
231- return null ;
232- }
235+ /**
236+ * @type {(message: string|Buffer, encoding: BufferEncoding, prefix?: string) => Buffer }
237+ */
238+ #getMessageHash( message , encoding , prefix = '\x18Bitcoin Signed Message:\n' ) {
239+ const msgBuffer = Buffer . isBuffer ( message ) ? message : Buffer . from ( message , encoding ) ;
240+ const msgPrefix = Buffer . from ( prefix + msgBuffer . length ) ;
241+ const fullMessage = Buffer . concat ( [ msgPrefix , msgBuffer ] ) ;
242+ const hash = doubleSha256 ( fullMessage ) ;
243+ return hash ;
244+ }
233245
234- const r = signature . slice ( 0 , 32 ) ;
235- const s = signature . slice ( 32 , 64 ) ;
236- const recoveryParam = signature [ 64 ] ;
246+ /**
247+ * Recovers the public key from a signed message and signature with recovery param.
248+ *
249+ * @param {string|Buffer } message - The original signed message.
250+ * @param {Buffer } signature - A 65-byte compact signature buffer (r + s + v).
251+ * @param {Object } [options] - Options for decoding the message hash.
252+ * @param {BufferEncoding } [options.encoding='hex'] - The encoding of the input message.
253+ * @param {string } [options.prefix=this.#msgPrefix] - Optional prefix used before hashing the message.
254+ * @returns {string } The recovered compressed public key in hex format, or null if recovery fails.
255+ * @throws {Error } If the encoding type is unsupported or signature is invalid.
256+ */
257+ recoverMessage ( message , signature , options = { } ) {
258+ const ec = this . getEc ( ) ;
259+ const { encoding = 'hex' , prefix = this . #msgPrefix } = options ;
260+ const hash = this . #getMessageHash( message , encoding , prefix ) ;
237261
238- try {
239- const pubKey = ec . recoverPubKey ( hash , { r, s } , recoveryParam ) ;
240- return pubKey . encodeCompressed ( 'hex' ) ; // use 'false' for uncompressed
241- } catch ( err ) {
242- console . warn ( '[recoverMessage] Failed to recover public key:' , err ) ;
243- return null ;
244- }
262+ const { r, s, v } = this . #normalizeSignature( signature ) ;
263+ const pubKey = ec . recoverPubKey ( hash , { r, s } , v ) ;
264+ return pubKey . encodeCompressed ( 'hex' ) ;
245265 }
246266
247267 /**
248- * Signs a message and returns a 65-byte recoverable signature .
268+ * Signs a message using ECDSA and includes the recovery param in the result .
249269 *
250- * @param {string|Buffer } message - The original message string or buffer.
251- * @returns {Buffer } The signature with recovery param (65 bytes total).
270+ * @param {string|Buffer } message - The message to sign.
271+ * @param {Object } [options] - Options for the message hashing process.
272+ * @param {BufferEncoding } [options.encoding='hex'] - The encoding used for string messages.
273+ * @param {string } [options.prefix=this.#msgPrefix] - Optional message prefix for the hash.
274+ * @returns {Buffer } A 65-byte recoverable signature (r + s + v).
275+ * @throws {Error } If recovery param is missing or encoding type is unsupported.
252276 */
253- signMessageWithRecovery ( message ) {
254- const msgBuffer = Buffer . isBuffer ( message ) ? message : Buffer . from ( message ) ;
255- const prefix = Buffer . from ( this . #prefix + msgBuffer . length ) ;
256- const fullMessage = Buffer . concat ( [ prefix , msgBuffer ] ) ;
257- const hash = doubleSha256 ( fullMessage ) ; // Same hash style as recoverMessage
258-
259- const { r, s, recoveryParam } = this . getKeyPair ( ) . sign ( hash , { canonical : true } ) ;
277+ signMessage ( message , options = { } ) {
278+ const keyPair = this . getKeyPair ( ) ;
279+ const { encoding = 'hex' , prefix = this . #msgPrefix } = options ;
280+ const hash = this . #getMessageHash( message , encoding , prefix ) ;
260281
261- if ( typeof recoveryParam !== 'number' ) {
282+ const { r, s, recoveryParam } = keyPair . sign ( hash , { canonical : true } ) ;
283+ if ( typeof recoveryParam !== 'number' )
262284 throw new Error ( '[signMessageWithRecovery] Missing recovery param from signature' ) ;
263- }
264285
265286 const rBuf = r . toArrayLike ( Buffer , 'be' , 32 ) ;
266287 const sBuf = s . toArrayLike ( Buffer , 'be' , 32 ) ;
0 commit comments