diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index 0cc77a0..2f7cbc6 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -100,7 +100,26 @@ export class AuthService { } try { const keypair = Keypair.fromPublicKey(dto.wallet); - const isValid = keypair.verify(Buffer.from(dto.nonce), Buffer.from(dto.signature, 'base64')); + + let isValid = false; + + // First attempt: raw Ed25519 signature (mobile clients) + try { + isValid = keypair.verify(Buffer.from(dto.nonce), Buffer.from(dto.signature, 'base64')); + } catch (e) { + isValid = false; + } + + // If raw verification failed, try SEP-0043 (browser wallets like Freighter) + if (!isValid) { + try { + const sepMessage = 'Stellar Signing Key: ' + dto.nonce; + isValid = keypair.verify(Buffer.from(sepMessage), Buffer.from(dto.signature, 'base64')); + } catch (e) { + isValid = false; + } + } + if (!isValid) { throw new UnauthorizedException({ code: 'AUTH_SIGNATURE_INVALID', message: 'Invalid signature.' }); } diff --git a/src/modules/auth/dto/verify-request.dto.ts b/src/modules/auth/dto/verify-request.dto.ts index 6f1ccfe..1057b1b 100644 --- a/src/modules/auth/dto/verify-request.dto.ts +++ b/src/modules/auth/dto/verify-request.dto.ts @@ -1,4 +1,4 @@ -import { IsString, IsNotEmpty, Matches, Length } from 'class-validator'; +import { IsString, IsNotEmpty, Matches, Length, IsOptional, IsIn } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; /** @@ -43,4 +43,15 @@ export class VerifyRequestDto { @IsString() @IsNotEmpty({ message: 'Signature is required' }) signature: string; + + @ApiProperty({ + description: "Signature type — 'raw' for raw Ed25519 or 'sep0043' for browser wallets", + example: 'raw', + required: false, + enum: ['raw', 'sep0043'], + }) + @IsOptional() + @IsString() + @IsIn(['raw', 'sep0043']) + signatureType?: 'raw' | 'sep0043' = 'raw'; } diff --git a/test/unit/modules/auth/auth.service.spec.ts b/test/unit/modules/auth/auth.service.spec.ts index 7d89525..71ffc37 100644 --- a/test/unit/modules/auth/auth.service.spec.ts +++ b/test/unit/modules/auth/auth.service.spec.ts @@ -243,6 +243,21 @@ describe('AuthService', () => { ); }); + it('should verify using SEP-0043 if raw verification fails', async () => { + const { mockKeypair } = setupMocks({ signatureValid: false }); + // First call (raw) returns false, second call (sep0043) should return true + mockKeypair.verify.mockImplementationOnce(() => false).mockImplementationOnce(() => true); + + await expect(service.verifySignature(validDto)).resolves.toBeUndefined(); + + expect(Keypair.fromPublicKey).toHaveBeenCalledWith(validWallet); + expect(mockKeypair.verify).toHaveBeenCalledWith(Buffer.from(validNonce), Buffer.from(validSignature, 'base64')); + expect(mockKeypair.verify).toHaveBeenCalledWith( + Buffer.from('Stellar Signing Key: ' + validNonce), + Buffer.from(validSignature, 'base64'), + ); + }); + it('should mark nonce as used after successful verification', async () => { const { } = setupMocks(); await service.verifySignature(validDto);