@@ -69,6 +69,16 @@ export default function handler(
69
69
if ( chainType === 'eip155' ) {
70
70
// Verify Ethereum personal_sign signature
71
71
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
+ }
72
82
} else {
73
83
// Verify Cosmos signature (default behavior)
74
84
// Convert base64 public key to Uint8Array
@@ -107,82 +117,83 @@ export default function handler(
107
117
108
118
function verifyEthereumSignature ( message : string , signature : string , expectedAddress : string ) : boolean {
109
119
try {
120
+ const keccak256 = require ( 'keccak256' ) ;
110
121
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 ;
181
190
}
191
+ } catch ( e ) {
192
+ // Continue with next recovery ID
193
+ continue ;
182
194
}
183
195
}
184
196
185
- console . log ( '❌ All message formats and recovery IDs failed' ) ;
186
197
return false ;
187
198
188
199
} catch ( error ) {
@@ -195,4 +206,54 @@ function extractTimestampFromMessage(message: string): string | null {
195
206
// "Please sign this message to complete login authentication.\nTimestamp: 2023-04-30T12:34:56.789Z\nNonce: abc123"
196
207
const timestampMatch = message . match ( / T i m e s t a m p : \s * ( [ ^ \n ] + ) / ) ;
197
208
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
+ }
198
259
}
0 commit comments