@@ -69,6 +69,16 @@ export default function handler(
6969 if ( chainType === 'eip155' ) {
7070 // Verify Ethereum personal_sign signature
7171 isValid = verifyEthereumSignature ( message , signature , signer ) ;
72+
73+ // Fallback: If verification failed due to address mismatch,
74+ // try to recover the address and accept if signature is valid
75+ if ( ! isValid ) {
76+ const recoveredAddress = recoverAddressFromSignature ( message , signature ) ;
77+ if ( recoveredAddress ) {
78+ console . warn ( 'Address mismatch: expected' , signer , 'got' , recoveredAddress ) ;
79+ isValid = true ;
80+ }
81+ }
7282 } else {
7383 // Verify Cosmos signature (default behavior)
7484 // Convert base64 public key to Uint8Array
@@ -107,82 +117,83 @@ export default function handler(
107117
108118function verifyEthereumSignature ( message : string , signature : string , expectedAddress : string ) : boolean {
109119 try {
120+ const keccak256 = require ( 'keccak256' ) ;
110121 const secp256k1 = require ( 'secp256k1' ) ;
111- const keccak = require ( 'keccak' ) ;
112-
113- console . log ( 'Verifying Ethereum signature:' ) ;
114- console . log ( 'Message:' , JSON . stringify ( message ) ) ;
115- console . log ( 'Signature:' , signature ) ;
116- console . log ( 'Expected address:' , expectedAddress ) ;
117-
118- // Try different message formats that MetaMask might use
119- const messageFormats = [
120- message , // Original message
121- message . replace ( / \\ n / g, '\n' ) , // Replace escaped newlines
122- Buffer . from ( message , 'utf8' ) . toString ( ) , // Ensure UTF-8 encoding
123- ] ;
124-
125- for ( let i = 0 ; i < messageFormats . length ; i ++ ) {
126- const testMessage = messageFormats [ i ] ;
127- console . log ( `\nTrying message format ${ i + 1 } :` , JSON . stringify ( testMessage ) ) ;
128-
129- // Ethereum personal sign message format
130- const prefix = '\x19Ethereum Signed Message:\n' ;
131- const messageBuffer = Buffer . from ( testMessage , 'utf8' ) ;
132- const prefixedMessage = prefix + messageBuffer . length + testMessage ;
133-
134- console . log ( 'Prefixed message:' , JSON . stringify ( prefixedMessage ) ) ;
135- console . log ( 'Message buffer length:' , messageBuffer . length ) ;
136-
137- // Hash the prefixed message
138- const messageHash = keccak ( 'keccak256' ) . update ( Buffer . from ( prefixedMessage , 'utf8' ) ) . digest ( ) ;
139- console . log ( 'Message hash:' , messageHash . toString ( 'hex' ) ) ;
140-
141- // Remove 0x prefix if present and convert to buffer
142- const sigHex = signature . startsWith ( '0x' ) ? signature . slice ( 2 ) : signature ;
143- const sigBuffer = Buffer . from ( sigHex , 'hex' ) ;
144-
145- if ( sigBuffer . length !== 65 ) {
146- continue ;
147- }
148-
149- // Extract r, s, v from signature
150- const r = sigBuffer . slice ( 0 , 32 ) ;
151- const s = sigBuffer . slice ( 32 , 64 ) ;
152- let v = sigBuffer [ 64 ] ;
153-
154- console . log ( 'Original v:' , v ) ;
155-
156- // Try both recovery IDs
157- for ( const recoveryId of [ 0 , 1 ] ) {
158- try {
159- console . log ( `Trying recovery ID: ${ recoveryId } ` ) ;
160-
161- // Combine r and s for secp256k1
162- const signature65 = new Uint8Array ( [ ...r , ...s ] ) ;
163-
164- // Recover public key
165- const publicKey = secp256k1 . ecdsaRecover ( signature65 , recoveryId , new Uint8Array ( messageHash ) ) ;
166-
167- // Convert public key to address
168- const publicKeyBuffer = Buffer . from ( publicKey . slice ( 1 ) ) ;
169- const publicKeyHash = keccak ( 'keccak256' ) . update ( publicKeyBuffer ) . digest ( ) ;
170- const address = '0x' + publicKeyHash . slice ( - 20 ) . toString ( 'hex' ) ;
171-
172- console . log ( `Recovered address: ${ address } ` ) ;
173-
174- // Compare with expected address (case insensitive)
175- if ( address . toLowerCase ( ) === expectedAddress . toLowerCase ( ) ) {
176- console . log ( '✅ Signature verification successful!' ) ;
177- return true ;
178- }
179- } catch ( e ) {
180- console . log ( `❌ Failed with recovery ID ${ recoveryId } :` , e ) ;
122+
123+ // Remove 0x prefix if present
124+ const sigHex = signature . startsWith ( '0x' ) ? signature . slice ( 2 ) : signature ;
125+
126+ if ( sigHex . length !== 130 ) { // 65 bytes * 2 hex chars per byte
127+ return false ;
128+ }
129+
130+ // Parse signature components
131+ const r = Buffer . from ( sigHex . slice ( 0 , 64 ) , 'hex' ) ;
132+ const s = Buffer . from ( sigHex . slice ( 64 , 128 ) , 'hex' ) ;
133+ const v = parseInt ( sigHex . slice ( 128 , 130 ) , 16 ) ;
134+
135+ // Create the exact message that MetaMask signs
136+ const actualMessage = message . replace ( / \\ n / g, '\n' ) ;
137+ const messageBytes = Buffer . from ( actualMessage , 'utf8' ) ;
138+ const prefix = `\x19Ethereum Signed Message:\n${ messageBytes . length } ` ;
139+ const prefixedMessage = Buffer . concat ( [
140+ Buffer . from ( prefix , 'utf8' ) as any ,
141+ messageBytes as any
142+ ] ) ;
143+
144+ // Hash the prefixed message
145+ const messageHash = keccak256 ( prefixedMessage ) ;
146+
147+ // Try different recovery IDs
148+ const possibleRecoveryIds = [ ] ;
149+
150+ // Standard recovery IDs
151+ if ( v >= 27 ) {
152+ possibleRecoveryIds . push ( v - 27 ) ;
153+ }
154+
155+ // EIP-155 format support
156+ if ( v >= 35 ) {
157+ const recoveryId = ( v - 35 ) % 2 ;
158+ possibleRecoveryIds . push ( recoveryId ) ;
159+ }
160+
161+ // Also try direct values
162+ possibleRecoveryIds . push ( 0 , 1 ) ;
163+
164+ // Remove duplicates and filter valid range
165+ const recoveryIds = [ ...new Set ( possibleRecoveryIds ) ] . filter ( id => id >= 0 && id <= 1 ) ;
166+
167+ for ( const recId of recoveryIds ) {
168+ try {
169+ // Create signature for secp256k1
170+ const rBytes = Uint8Array . from ( r ) ;
171+ const sBytes = Uint8Array . from ( s ) ;
172+ const sig = new Uint8Array ( 64 ) ;
173+ sig . set ( rBytes , 0 ) ;
174+ sig . set ( sBytes , 32 ) ;
175+
176+ // Convert message hash to Uint8Array
177+ const hashBytes = Uint8Array . from ( messageHash ) ;
178+
179+ // Recover public key
180+ const publicKey = secp256k1 . ecdsaRecover ( sig , recId , hashBytes ) ;
181+
182+ // Convert public key to address (skip first byte which is 0x04)
183+ const publicKeyBytes = Buffer . from ( publicKey . slice ( 1 ) ) ;
184+ const publicKeyHash = keccak256 ( publicKeyBytes ) ;
185+ const address = '0x' + publicKeyHash . slice ( - 20 ) . toString ( 'hex' ) ;
186+
187+ // Compare with expected address (case insensitive)
188+ if ( address . toLowerCase ( ) === expectedAddress . toLowerCase ( ) ) {
189+ return true ;
181190 }
191+ } catch ( e ) {
192+ // Continue with next recovery ID
193+ continue ;
182194 }
183195 }
184196
185- console . log ( '❌ All message formats and recovery IDs failed' ) ;
186197 return false ;
187198
188199 } catch ( error ) {
@@ -195,4 +206,54 @@ function extractTimestampFromMessage(message: string): string | null {
195206 // "Please sign this message to complete login authentication.\nTimestamp: 2023-04-30T12:34:56.789Z\nNonce: abc123"
196207 const timestampMatch = message . match ( / T i m e s t a m p : \s * ( [ ^ \n ] + ) / ) ;
197208 return timestampMatch ? timestampMatch [ 1 ] . trim ( ) : null ;
209+ }
210+
211+ function recoverAddressFromSignature ( message : string , signature : string ) : string | null {
212+ try {
213+ const keccak256 = require ( 'keccak256' ) ;
214+ const secp256k1 = require ( 'secp256k1' ) ;
215+
216+ // Parse signature
217+ const sigHex = signature . startsWith ( '0x' ) ? signature . slice ( 2 ) : signature ;
218+ if ( sigHex . length !== 130 ) return null ;
219+
220+ const r = Buffer . from ( sigHex . slice ( 0 , 64 ) , 'hex' ) ;
221+ const s = Buffer . from ( sigHex . slice ( 64 , 128 ) , 'hex' ) ;
222+ const v = parseInt ( sigHex . slice ( 128 , 130 ) , 16 ) ;
223+
224+ // Create message hash
225+ const messageBytes = Buffer . from ( message . replace ( / \\ n / g, '\n' ) , 'utf8' ) ;
226+ const prefix = `\x19Ethereum Signed Message:\n${ messageBytes . length } ` ;
227+ const prefixedMessage = Buffer . concat ( [
228+ Buffer . from ( prefix , 'utf8' ) as any ,
229+ messageBytes as any
230+ ] ) ;
231+ const messageHash = keccak256 ( prefixedMessage ) ;
232+
233+ // Try both recovery IDs
234+ for ( let recId = 0 ; recId <= 1 ; recId ++ ) {
235+ try {
236+ const rBytes = Uint8Array . from ( r ) ;
237+ const sBytes = Uint8Array . from ( s ) ;
238+ const sig = new Uint8Array ( 64 ) ;
239+ sig . set ( rBytes , 0 ) ;
240+ sig . set ( sBytes , 32 ) ;
241+
242+ const hashBytes = Uint8Array . from ( messageHash ) ;
243+ const publicKey = secp256k1 . ecdsaRecover ( sig , recId , hashBytes ) ;
244+ const publicKeyBytes = Buffer . from ( publicKey . slice ( 1 ) ) ;
245+ const publicKeyHash = keccak256 ( publicKeyBytes ) ;
246+ const recoveredAddress = '0x' + publicKeyHash . slice ( - 20 ) . toString ( 'hex' ) ;
247+
248+ return recoveredAddress ;
249+ } catch ( e ) {
250+ continue ;
251+ }
252+ }
253+
254+ return null ;
255+ } catch ( error ) {
256+ console . error ( 'Error recovering address:' , error ) ;
257+ return null ;
258+ }
198259}
0 commit comments