Mobile
Desenvolvimento Mobile Solana: Tutorial React Native e MWA

Desenvolvimento Mobile Solana: Tutorial React Native e MWA

Assinando Mensagens

Assinando Mensagens

Nem toda interação precisa de uma transação. Às vezes você só precisa provar que um usuário controla uma carteira, sem escrever nada na blockchain ou gastar lamports.

A assinatura de mensagens permite fluxos de autenticação, atestações off-chain e provas criptográficas de propriedade. O Sign In With Solana (SIWS) padroniza isso para login web3, substituindo email/senha por assinaturas de carteira.

Assinaturas de mensagens provam propriedade da carteira sem tocar na blockchain. São gratuitas, instantâneas e verificáveis por qualquer pessoa.

Nesta lição, você criará um hook useSignMessage e aprenderá como implementar Sign In With Solana para autenticação.

Como a Assinatura de Mensagens Funciona

Quando você assina uma transação, está autorizando uma mudança de estado na Solana. Quando assina uma mensagem, está criando uma prova criptográfica que diz "esta carteira aprovou este conteúdo exato."

A assinatura é determinística: a mesma carteira assinando a mesma mensagem sempre produzirá a mesma assinatura. Qualquer pessoa com a chave pública pode verificar que a assinatura foi feita pela chave privada correspondente, sem nunca ver essa chave privada.

text
Mensagem + Chave Privada → Assinatura
Mensagem + Assinatura + Chave Pública → Verdadeiro/Falso (verificação)

Isso é criptografia Ed25519, o mesmo esquema que a Solana usa para assinaturas de transações.

Assinatura Básica de Mensagens

O método signMessages recebe um array de payloads (arrays de bytes) e retorna assinaturas:

typescript
import { transact } from '@solana-mobile/mobile-wallet-adapter-protocol-web3js';
import { toByteArray } from 'react-native-quick-base64';

async function signMessage(message: string): Promise<Uint8Array> {
  return await transact(async (wallet) => {
    const authResult = await wallet.authorize({
      identity: APP_IDENTITY,
      chain: 'solana:mainnet',
    });
    
    // Converter string para bytes
    const messageBytes = new TextEncoder().encode(message);
    
    // Assinar a mensagem
    const signatures = await wallet.signMessages({
      addresses: [authResult.accounts[0].address],
      payloads: [messageBytes],
    });
    
    // signatures[0] é Uint8Array (64 bytes)
    return signatures[0];
  });
}

Pontos importantes:

  • addresses: Quais conta(s) devem assinar

  • payloads: Os bytes brutos para assinar

  • Retorna: Array de assinaturas correspondendo aos payloads

A carteira mostra aos usuários o conteúdo da mensagem (decodificado como UTF-8 se possível) e pede aprovação.

Assinando Múltiplas Mensagens

Você pode assinar múltiplas mensagens em uma solicitação:

typescript
const signatures = await wallet.signMessages({
  addresses: [account.address, account.address],
  payloads: [messageBytes1, messageBytes2],
});

// signatures[0] corresponde a messageBytes1
// signatures[1] corresponde a messageBytes2

Verificando Assinaturas

Após receber uma assinatura, você (ou seu backend) pode verificá-la:

typescript
import { sign } from 'tweetnacl';
import { PublicKey } from '@solana/web3.js';

function verifySignature(
  message: Uint8Array,
  signature: Uint8Array,
  publicKey: PublicKey
): boolean {
  return sign.detached.verify(
    message,
    signature,
    publicKey.toBytes()
  );
}

// Uso
const message = new TextEncoder().encode('Olá, Solana!');
const signature = await signMessage('Olá, Solana!');
const publicKey = /* chave pública do usuário da autorização */;

const isValid = verifySignature(message, signature, publicKey);
console.log('Assinatura válida:', isValid); // true

Importante: Você deve verificar contra exatamente os mesmos bytes que foram assinados. Se você codificar a mensagem de forma diferente (codificação diferente, espaços em branco, quebras de linha), a verificação falhará.

Sign In With Solana

Sign In With Solana (SIWS) é um padrão para autenticação baseada em carteira. É como "Fazer login com Google" mas usando sua carteira Solana em vez de um provedor OAuth.

O MWA 2.0 suporta SIWS diretamente no método authorize:

typescript
await transact(async (wallet) => {
  const authResult = await wallet.authorize({
    identity: APP_IDENTITY,
    chain: 'solana:mainnet',
    sign_in_payload: {
      domain: 'mydapp.com',
      statement: 'Entrar no Meu dApp',
      uri: 'https://mydapp.com',
      version: '1',
      nonce: generateSecureNonce(), // Gerado pelo servidor
      issuedAt: new Date().toISOString(),
    },
  });
  
  if (authResult.sign_in_result) {
    const { address, signature, signedMessage } = authResult.sign_in_result;
    
    // Enviar para o backend para verificação
    await verifySignInOnServer(address, signature, signedMessage);
  }
});

Por que SIWS?

  • Sem senhas: Usuários autenticam com sua carteira

  • Prova criptográfica: Impossível de forjar

  • Descentralizado: Sem dependência de provedor OAuth

  • Multi-plataforma: A mesma carteira funciona em qualquer lugar

Formato da Mensagem SIWS

O sign_in_payload torna-se uma mensagem legível mostrada ao usuário:

text
mydapp.com quer que você entre com sua conta Solana:
7F9k...Xyz

Entrar no Meu dApp

URI: https://mydapp.com
Versão: 1
Nonce: abc123
Emitido em: 2024-01-15T10:30:00Z

Esta mensagem é o que o usuário assina. A carteira a exibe claramente para que os usuários saibam o que estão aprovando.

Campos do Payload SIWS

typescript
type SignInPayload = {
  domain: string;      // Seu domínio (ex., "mydapp.com")
  statement?: string;  // Descrição legível
  uri: string;         // URI completa (ex., "https://mydapp.com")
  version: string;     // Versão SIWS, atualmente "1"
  nonce: string;       // String aleatória gerada pelo servidor
  issuedAt: string;    // Timestamp ISO 8601
  expirationTime?: string;  // Quando este login expira
  notBefore?: string;       // Não aceitar antes deste horário
  requestId?: string;       // Seu identificador de sessão
  resources?: string[];     // URIs que o usuário concorda em acessar
};

O nonce é crítico; previne ataques de replay. Seu servidor gera um nonce único, o usuário o assina, e seu servidor verifica que a assinatura inclui exatamente esse nonce.

Verificação no Backend

Nunca confie em verificação do lado do cliente para autenticação. Sempre verifique assinaturas SIWS no seu backend.

Exemplo Node.js

typescript
import { verifySignIn } from '@solana/wallet-standard-util';
import type { SolanaSignInInput, SolanaSignInOutput } from '@solana/wallet-standard-features';

export function verifySIWS(
  input: SolanaSignInInput,
  output: SolanaSignInOutput
): boolean {
  // Converter para o formato esperado por verifySignIn
  const serializedOutput = {
    account: {
      publicKey: new Uint8Array(output.account.publicKey),
      ...output.account,
    },
    signature: new Uint8Array(output.signature),
    signedMessage: new Uint8Array(output.signedMessage),
  };
  
  return verifySignIn(input, serializedOutput);
}

// Exemplo de rota Express
app.post('/api/auth/siws', async (req, res) => {
  const { input, output } = req.body;
  
  // Verificar se o nonce corresponde ao que emitimos
  const expectedNonce = await redis.get(`siws:nonce:${req.sessionID}`);
  if (input.nonce !== expectedNonce) {
    return res.status(400).json({ error: 'Nonce inválido' });
  }
  
  // Verificar a assinatura
  const isValid = verifySIWS(input, output);
  if (!isValid) {
    return res.status(401).json({ error: 'Assinatura inválida' });
  }
  
  // Assinatura válida - criar sessão
  const publicKey = output.account.address;
  req.session.wallet = publicKey;
  
  // Limpar o nonce usado
  await redis.del(`siws:nonce:${req.sessionID}`);
  
  res.json({ success: true, publicKey });
});

Geração de Nonce

typescript
import crypto from 'crypto';

export function generateSecureNonce(): string {
  return crypto.randomBytes(32).toString('base64url');
}

// Armazenar nonce antes de enviar ao cliente
app.get('/api/auth/nonce', async (req, res) => {
  const nonce = generateSecureNonce();
  await redis.setex(`siws:nonce:${req.sessionID}`, 300, nonce); // 5 min expiração
  res.json({ nonce });
});

SIWS Sem Suporte da Carteira

Se uma carteira não suporta o parâmetro sign_in_payload, você pode implementar SIWS manualmente usando signMessages:

typescript
import { createSignInMessage, SolanaSignInInput } from '@solana/wallet-standard-util';

async function manualSignIn(): Promise<{ input: SolanaSignInInput; output: any }> {
  // Buscar nonce do servidor
  const { nonce } = await fetch('/api/auth/nonce').then(r => r.json());
  
  const input: SolanaSignInInput = {
    domain: 'mydapp.com',
    statement: 'Entrar no Meu dApp',
    uri: 'https://mydapp.com',
    version: '1',
    nonce,
    issuedAt: new Date().toISOString(),
  };
  
  return await transact(async (wallet) => {
    const authResult = await wallet.authorize({
      identity: APP_IDENTITY,
      chain: 'solana:mainnet',
    });
    
    const account = authResult.accounts[0];
    const publicKey = new PublicKey(toByteArray(account.address));
    
    // Criar a mensagem SIWS padrão
    const messageBytes = createSignInMessage({
      ...input,
      address: publicKey.toBase58(),
    });
    
    // Assiná-la
    const [signature] = await wallet.signMessages({
      addresses: [account.address],
      payloads: [messageBytes],
    });
    
    return {
      input,
      output: {
        account: {
          publicKey: publicKey.toBytes(),
          address: publicKey.toBase58(),
        },
        signature,
        signedMessage: messageBytes,
      },
    };
  });
}

Casos de Uso

Autenticação

Substitua usuário/senha por assinaturas de carteira:

typescript
async function login(): Promise<void> {
  const { input, output } = await signInWithSolana();
  
  const response = await fetch('/api/auth/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ input, output }),
  });
  
  if (response.ok) {
    const { token } = await response.json();
    await AsyncStorage.setItem('auth_token', token);
  }
}

Atestados Off-Chain

Prove concordância com termos de serviço sem uma transação:

typescript
const message = `Eu concordo com os Termos de Serviço do MyApp.
Hash do Documento: ${tosHash}
Timestamp: ${Date.now()}`;

const signature = await signMessage(message);

// Armazenar assinatura como prova de concordância
await fetch('/api/tos/agree', {
  method: 'POST',
  body: JSON.stringify({
    wallet: publicKey.toBase58(),
    signature: Buffer.from(signature).toString('base64'),
    message,
  }),
});

Acesso a Conteúdo Restrito

Prove propriedade de NFT sem revelar todas as posses da carteira:

typescript
const message = `Verificar propriedade de NFT para acesso:
Coleção: ${collectionAddress}
Timestamp: ${Date.now()}`;

const signature = await signMessage(message);

// Backend verifica se a carteira possui um NFT da coleção
const { hasAccess } = await fetch('/api/verify-access', {
  method: 'POST',
  body: JSON.stringify({
    wallet: publicKey.toBase58(),
    signature: Buffer.from(signature).toString('base64'),
    message,
  }),
}).then(r => r.json());

Considerações de Segurança

Sempre Use Nonces

Sem um nonce, assinaturas podem ser reproduzidas:

  1. Usuário entra no seu app

  2. Atacante captura a assinatura

  3. Atacante a reproduz para se passar pelo usuário

Nonces tornam cada assinatura única e de uso único.

Vinculação de Domínio

O campo domain do SIWS deve corresponder ao seu domínio real. Carteiras podem alertar usuários se o domínio parecer suspeito.

Clareza da Mensagem

Usuários veem a mensagem antes de assinar. Torne claro o que estão aprovando:

typescript
// Ruim: vago, poderia ser qualquer coisa
const message = 'confirmar ação';

// Bom: específico, inclui contexto
const message = `Entrar no MyApp
ID da Solicitação: ${requestId}
Isso não concede acesso aos seus fundos.`;

Validação de Timestamp

Inclua e valide timestamps para evitar que assinaturas antigas sejam reutilizadas:

typescript
function isSignatureExpired(issuedAt: string, maxAgeMs: number = 300000): boolean {
  const issued = new Date(issuedAt).getTime();
  return Date.now() - issued > maxAgeMs;
}

A assinatura de mensagens é a base da autenticação baseada em carteira. Na próxima lição, construiremos um AuthorizationProvider para gerenciar todo este estado em sua aplicação.

Blueshift © 2026Commit: 1b88646