Face Local / Para desenvolver

Do download ao primeiro build.

Documentação de verdade. Permissões, integração e limites — sem letras miúdas.

SDKv0.6.0
Baixar .md
terminal · pacote baixado neste site
pnpm add ./face-local-sdk-0.6.0.tgz
Após liberar o download, mova o .tgz para a pasta do seu app. O comando usa o nome do arquivo, não o endereço privado do download.

Face Local SDK#

English documentation

SDK reutilizável para React Native, com núcleo TypeScript independente da interface, módulo Expo em Kotlin/Swift e adaptador de biblioteca de fotos. O Face Local reúne prova de vida, busca automática de selfies, Face Match e resultado estruturado em um único SDK. Destina-se a integração nos apps do usuário; não está publicado no npm. private: true evita publicação acidental e não impede instalação por pacote local.

Obter o código-fonte#

O pacote pode ser obtido em facelocal.dev/baixar-sdk com pagamento único de US$ 999 pelo Stripe ou usando um cupom válido. Após a confirmação, baixe o arquivo .tgz e guarde o código de recuperação privado para acessar o download novamente em outro navegador.

A documentação e o APK de demonstração continuam públicos. A cobrança é pelo acesso ao código-fonte: não há assinatura, cobrança por verificação ou chave online para executar o SDK no aplicativo. O uso com marca própria é permitido, conforme a licença; a revenda do SDK não é permitida.

O que ele verifica — e o que não verifica#

  1. O motor recebe frames da câmera e executa prova de vida passiva + movimentos.
  2. A imagem da mesma captura aprovada gera a referência facial em memória. Não há API para promover uma selfie da galeria a referência aprovada.
  3. O onboarding busca selfies e compara automaticamente as fotos locais autorizadas.
  4. Retorna positivo ao encontrar um match; negativo após uma busca concluída sem match. Falhas, falta de fotos elegíveis e busca incompleta retornam erro ou inconclusivo, nunca uma aprovação ou um falso negativo.

Retorno JSON com imagens e critérios da decisão (0.5+)#

Para integrar o resultado em outro app, use runFaceLocalVerification, exportado por @workspace/face-local-sdk/expo. Ele executa o onboarding automático e retorna VerificationResult, com schemaVersion: "1.0":

  • status, approved e reason: aprovação, reprovação, inconclusivo, cancelamento ou erro;
  • photos.liveness e photos.compared: cópias locais temporárias das imagens efetivamente usadas, nunca uma nova selfie ou um arquivo escolhido como referência;
  • faceMatch: similaridade original, limiar aplicado e índice de 0 a 100 — não acurácia nem probabilidade de identidade;
  • exif: Make, Model e DateTimeOriginal lidos dos mesmos bytes analisados pelo nativo;
  • deviceFingerprint: perfil do modelo usado na captura, não identificador único;
  • decision: motor, prova de vida, requisitos, contagens e motivos de exclusão.

A imagem comparada é a primeira que atingiu o limiar; sem match, é a melhor comparação concluída, se existir. Em cancelamento ou erro, nenhuma foto é entregue. As cópias exigem descarte explícito, possuem expiração e nunca alteram a galeria.

Documentação completa:

  1. Integrar, cancelar e descartar as imagens.
  2. Contrato JSON, significado de todos os campos e limitações.

O fluxo antigo runFaceLocalOnboarding continua disponível, sem exportação de imagens. Atualizar apenas o JavaScript não basta para a nova API: recompile o aplicativo nativo. Expo Go e navegador não executam biometria nem simulam aprovação.

Estrutura#

  • Importação principal: tipos, política, FaceLocalVerifier, runVerification, validação de EXIF.
  • /expo: createExpoEngine, photoPermission, managePhotoSelection, readLocalPhotos, runFaceLocalVerification, releaseFaceLocalVerification.
  • android/: implementação Android do Face Local, com CameraX, ponte Expo, prova de vida e comparação local.
  • ios/: implementação iOS do Face Local, com AVFoundation/SwiftUI, ponte Expo, ImageIO e Vision.
  • app.plugin.js: permissões, backups Android e isolamento de rede no release Android.
  • tests/: testes determinísticos do fluxo. Os dublês de teste não entram no pacote.

Não copie as telas do app para reutilizar o SDK.

A implementação Android inicializa MMKV para a configuração nativa, mas não cadastra embeddings nem imagens nele. O comportamento de armazenamento das bibliotecas nativas ainda deve ser verificado em aparelho.

Compatibilidade-alvo#

ComponenteCompatibilidade
ExpoSDK 57
React Native0.86
AndroidAPI 26+; aparelho físico ARM
iOS16.4+; iPhone físico
Face Local para AndroidKotlin, CameraX e Expo Modules
Face Local para iOSSwift, AVFoundation e Expo Modules

O SDK depende de Expo Modules, não do Expo Go. Um projeto React Native sem Expo pode instalar Expo Modules, seguir a configuração nativa e usar o mesmo adaptador. Outros ambientes/versões exigem adaptação e validação; não são declarados compatíveis automaticamente.

Levar para outro projeto#

Na raiz deste workspace:

sh
pnpm --filter @workspace/face-local-sdk build
pnpm --filter @workspace/face-local-sdk pack --pack-destination ../../generated-artifacts

Copie o .tgz resultante para seu outro projeto. Instale-o por caminho local:

sh
pnpm add ./face-local-sdk-0.6.0.tgz
pnpm exec expo install expo-modules-core expo-media-library expo-image-picker

O nome de importação continua @workspace/face-local-sdk; não é necessário um registro privado. Alternativamente, mantenha o pacote como workspace compartilhado.

Nomes de modelo no EXIF e no Android#

O nome comercial escrito pela câmera pode diferir de Build.MODEL. O módulo nativo resolve somente equivalências explícitas verificadas; não usa o nome editável do celular nem aprende o modelo do aparelho a partir da galeria.

O catálogo inicial cobre os códigos completos 24117RN76E, 24117RN76G, 24117RN76L e 24117RN76O, fabricante Xiaomi e dispositivo tanzanite, como Redmi Note 14, conforme a lista oficial de aparelhos do Google. Não é um catálogo universal de todos os Androids. Outros códigos mantêm a comparação literal; 5G, Pro, Pro+ e códigos regionais desconhecidos não são aceitos por semelhança.

Quando há uma equivalência, CaptureDevice.model contém o nome resolvido e hardwareModel preserva o código real do sistema. Ambos permanecem vinculados à referência de liveness. O filtro aceita o nome verificado ou o código exato daquele aparelho, nunca o código de outra variante. A leitura nativa revalida essa regra nos bytes usados para a comparação. Nenhum desses campos identifica uma unidade física nem autentica o EXIF.

Diagnóstico sem fotos#

ScanReport.device informa o modelo usado na captura. summarizeVerification(result) produz somente modelo/código do aparelho, contagens, motivos de descarte e quantidades de campos ausentes. Não inclui fotos, nomes de arquivos, URIs, datas EXIF, localização, sessão, embeddings ou scores.

O helper não grava nem transmite nada. O app oferece compartilhamento opcional, com confirmação e escolha de destino pelo usuário, apenas em resultados inconclusivos. Nunca use JSON.stringify(result) para esse fim: o resultado completo contém informações das fotos locais.

Acrescente a configuração ao app.json do app integrador, preservando seus plugins existentes:

json
{
  "expo": {
    "plugins": [
      ["expo-media-library", {
        "granularPermissions": ["photo"],
        "isAccessMediaLocationEnabled": false,
        "photosPermission": "Permita acesso a todas as fotos para encontrar seu rosto neste aparelho."
      }],
      ["expo-image-picker", {
        "cameraPermission": "Usar a câmera para prova de vida local.",
        "microphonePermission": false
      }],
      "@workspace/face-local-sdk"
    ]
  }
}

Atenção: o plugin desabilita backup do app Android e remove a permissão INTERNET do release inteiro, não apenas do motor biométrico. Isso é adequado ao app de testes offline. Se seu outro app depende de rede, avalie essa política antes de integrá-lo: manter rede exige auditoria do motor e não permite prometer isolamento de rede pelo sistema operacional. Não remova a proteção silenciosamente.

No iPhone não existe uma permissão geral equivalente a INTERNET do Android: ATS limita transportes inseguros, mas não bloqueia todo HTTPS.

Gere e compile os projetos nativos após instalar:

sh
pnpm exec expo prebuild
# Android físico:
pnpm exec expo run:android --variant release --device
# iPhone físico, em um Mac com Xcode e CocoaPods:
pnpm exec expo run:ios --configuration Release --device

Não execute prebuild --clean em um app existente com alterações nativas manuais sem antes revisá-las e salvá-las.

API de uso#

Na versão 0.3, um motor deve informar fabricante/modelo exatos em capabilities.currentDevice e no device do recibo de captura. Não substitua esses campos por nome personalizado, marca genérica, valor digitado pelo usuário, IDFV ou UUID. Adaptações de outros motores também devem reler o EXIF nativamente antes de inferir sobre cada imagem. Modelos desconhecidos são recusados.

ts
import { FaceLocalVerifier, requireFullPhotoAccess, type Consent } from "@workspace/face-local-sdk";
import {
  createExpoEngine, photoPermission, readLocalPhotos,
} from "@workspace/face-local-sdk/expo";
import { requestCameraPermissionsAsync } from "expo-image-picker";

const engine = createExpoEngine();
const verifier = new FaceLocalVerifier(engine, {
  matchThreshold: 0.83,
  requireCaptureMetadata: true,
  maxPhotos: 100,
  captureTimeoutSeconds: 15,
});

if (!verifier.capabilities.available) {
  throw new Error(verifier.capabilities.reason);
}

// O aceite inicial explica o uso do rosto para encontrar as fotos no aparelho.
// Comece com photoLibrary: false; só autorize fotos depois do retorno real do SO.
const consent: Consent = consentFromYourUI;
if (!consent.camera || !consent.biometricProcessing) {
  throw new Error("Consentimento necessário.");
}
const camera = await requestCameraPermissionsAsync();
if (!camera.granted) throw new Error("A câmera precisa ser autorizada.");

try {
  const receipt = await verifier.capture(consent);
  // receipt é metadado de uma sessão opaca. Não contém imagem nem embedding.
  const libraryPermission = await photoPermission(true);
  requireFullPhotoAccess(libraryPermission);
  verifier.authorizePhotoLibrary({
    photoLibrary: libraryPermission.granted,
    biometricProcessing: consent.biometricProcessing,
    version: consent.version,
  });
  const library = await readLocalPhotos({
    limit: 100,
    signal: abortController.signal,
    onProgress: count => updateLibraryProgress(count),
  });
  // Comparação automática. Nunca usar uma foto da biblioteca como referência.
  const report = await verifier.scan(library.photos, progress => {
    updateProgress(progress.processed, progress.total);
  });
  showReportInYourUI(report);
} finally {
  await verifier.revokeConsent();
}

consentFromYourUI, abortController, updateLibraryProgress, updateProgress e showReportInYourUI representam responsabilidades da interface integradora, não funções exportadas pelo SDK. Veja o app Face Local para uma implementação completa.

Onboarding automático sem exportar imagens (API anterior)#

ts
import { runFaceLocalOnboarding } from "@workspace/face-local-sdk/expo";
import { requestCameraPermissionsAsync } from "expo-image-picker";

const result = await runFaceLocalOnboarding({
  // Chamar somente após o aceite visível para usar câmera e rosto na comparação.
  consent: { camera: true, biometricProcessing: true, photoLibrary: false, version: "1" },
  signal: abortController.signal,
  policy: { maxPhotos: 100, matchThreshold: 0.83, requireCaptureMetadata: true },
  requestCameraAccess: async () => {
    // Apenas autorização da câmera; nenhum seletor ou captura estática é aberto.
    if (!(await requestCameraPermissionsAsync()).granted) throw new Error("Câmera não autorizada.");
  },
  onPhase: phase => updatePhaseInYourUI(phase),
  onSearchProgress: count => updateLibraryProgress(count),
  onProgress: progress => updateProgress(progress.processed, progress.total),
});
// result.outcome: "positive" | "negative" | "inconclusive"
// result.reason explica o motivo; result.report contém as comparações efetivamente feitas.

updatePhaseInYourUI também é um callback da interface integradora. A sequência é permissão da câmera → liveness → permissão das fotos → descoberta → Face Match. Não há escolha manual de imagens no app. A opção de acesso limitado pertence ao sistema operacional e não pode ser removida ou ignorada pelo aplicativo. O Face Local exige acesso completo: com acesso limitado, orienta a liberar todas as fotos nos Ajustes e encerra a sessão sem buscar ou comparar imagens. Não acrescente botões de buscar, comparar ou confirmar o início da busca após a captura. O adaptador Expo usa o retorno real da permissão de fotos para continuar sozinho. No núcleo independente, requestPhotoAccess deve retornar { granted, accessPrivileges: "all" | "limited" | "none", canAskAgain? }. Desde 0.4, granted: true sem escopo explícito e o retorno antigo void não autorizam a busca, mesmo com consentimento inicial para fotos. Recusa, cancelamento ou expiração impedem qualquer leitura posterior; uma permissão que chega após cancelar é ignorada.

No iPhone, a coleção Selfies é identificada pelo subtipo do PhotoKit, sem depender do idioma. Nomes de álbuns são pistas adicionais, especialmente no Android. A busca prioriza essas coleções e depois percorre as demais fotos autorizadas, sem duplicatas. Uma imagem com uma face é candidata; isso não comprova que foi tirada com a câmera frontal. Nenhum classificador de origem infalível é alegado.

O teste usa o limite configurado (máximo 1.000 fotos) e referência de 5 minutos. Para ao primeiro match. Se restarem fotos além do limite e nenhum match tiver sido encontrado, retorna inconclusive, não negative. Fotos na nuvem, sem face, com múltiplas faces ou rejeitadas pelo filtro EXIF não recebem um score inventado. O negativo se refere às fotos locais elegíveis e autorizadas, não a todo conteúdo que possa existir no aparelho ou na nuvem.

Proteção local da captura#

  • Apenas câmera nativa; a ponte rejeita opções extras, inclusive referências, imagens, frames e scores. A captura anterior aceita apenas timeoutSeconds. A captura com exportação acrescenta somente prazo de retenção e limiar de match; nenhum parâmetro permite importar ou substituir uma imagem de referência.
  • A aprovação exige sequência recente de frames com timestamps crescentes, câmera frontal integrada declarada pelo sistema e gate de uso único.
  • No iOS, um relay observa o output AVFoundation antes do processamento biométrico. Se não for possível verificar esse fluxo, a captura falha, sem fallback.
  • Builds de depuração, simuladores e sinais conhecidos de root/jailbreak, depurador ou bibliotecas de instrumentação são bloqueados. Use Release.
  • Android usa janela segura, proteção de toques obscurecidos e bloqueio de overlays no Android 12+. iOS cancela com captura de tela ativa ou perda de foco.
  • Bloqueios de segurança abortam a sessão inteira; resultados parciais não aprovam.
  • O recibo deve incluir source: "native-camera". Não é atestado criptográfico: não o aceite como prova confiável enviada a um servidor por um cliente hostil.

São camadas locais, não imunidade contra injeção. Root oculto, hooks desconhecidos, HAL/driver adulterado e deepfakes adaptativos podem contornar verificações locais. Não há Play Integrity/App Attest validado externamente, assinatura própria de produção, auditoria completa dos binários nativos ou certificação de resistência a injeção. O APK de avaliação continua com assinatura de teste. O iOS foi testado em iPhone. Ao integrar o SDK ou alterar a captura nativa, valide o fluxo no seu app, especialmente o relay de câmera. Consulte o modelo de ameaças e o protocolo físico do projeto.

Cancelamento e ciclo de vida#

  • Cancele a leitura de fotos com AbortController.abort().
  • Use await verifier.clear() para invalidar a sessão e aguardar callbacks antigos.
  • Use await verifier.revokeConsent() ao encerrar, revogar ou sair do app.
  • Não inicie uma segunda operação antes de a primeira ser encerrada.
  • Mantenha uma instância ativa por processo; o adaptador nativo possui uma única referência.
  • No Android, a câmera nativa é outra Activity. Não a confunda com saída do app. O app de exemplo ignora esse evento durante captura; a própria câmera cancela se perder o foco. No iOS, o módulo também limpa dados ao entrar em background.
  • Não persista receipt, fotos selecionadas, EXIF, resultados ou embeddings em AsyncStorage, analytics, logs, crash reports ou servidores.
  • A API JSON é uma opção explícita para obter até duas cópias JPEG em cache privado, por 30–300 segundos após a exportação. Não muda a regra de manter templates somente em memória. Veja o ciclo de vida, limites de expiração e responsabilidade sobre Base64 em INTEGRACAO.md.

Resultados#

EstadoSignificado
matchUma face e similaridade ≥ limiar
differentUma face e similaridade abaixo do limiar
inconclusiveZero ou múltiplas faces; não é um não-match
skippedNuvem, URI proibida ou EXIF incompleto segundo a política
errorArquivo ou resultado não pôde ser processado

similarity é uma pontuação do motor, não uma porcentagem de certeza. report.nativeAttempts conta chamadas ao motor nativo, inclusive tentativas sem face ou com falha. report.compared conta somente resultados com uma face e similaridade válida; é esse número que aparece no app como “fotos comparadas”. Fotos descartadas pelo filtro de metadados não entram nessa contagem. Falhas de leitura (unreadable-photos) e do motor (comparison-failed) são inconclusivas e têm mensagens distintas, nunca são um falso resultado negativo. O limiar padrão 0,83 é um valor inicial e não foi calibrado para seus aparelhos/fotos. Após scan, a referência é liberada mesmo se ocorrer erro. Expiração/cancelamento impedem emissão de um relatório parcial como concluído.

Biblioteca e EXIF#

  • Exige acesso completo (accessPrivileges: "all") e respeita a recusa do usuário: não força permissões nem tenta acessar fotos fora da autorização.
  • iOS: getAssetInfoAsync(..., { shouldDownloadFromNetwork: false }).
  • Android: enumera sem resolveWithFullInfo, abre a URI local do MediaStore e lê apenas Make/Model/DateTimeOriginal por código nativo. Não usa getAssetInfoAsync: a versão instalada do Expo também solicita GPS nessa chamada, provocando falhas sem ACCESS_MEDIA_LOCATION mesmo com acesso completo à galeria.
  • Não pede localização, MANAGE_EXTERNAL_STORAGE nem baixa originais da nuvem.
  • Confere o escopo novamente após a descoberta e no nativo durante as comparações; uma revogação aborta o resultado e libera a referência.
  • Nunca passa URI ph:// ou URL remota ao renderizador/matcher como fallback.
  • Android: somente arquivos locais e MediaStore; provedores remotos são recusados.
  • Descarta GPS e todos os campos EXIF exceto fabricante, modelo e data original.
  • O filtro exige esses três campos, não apenas a existência de algum EXIF.
  • Fabricante e modelo devem corresponder exatamente aos da captura, normalizando somente espaços e caixa. Não há aproximação por família, prefixo, Pro/Max ou geração.
  • O Android usa Build.MANUFACTURER/Build.MODEL; o iOS usa o mapeamento local do identificador de hardware do DeviceKit 5.9.0. Modelo desconhecido não recebe fallback.
  • Cada captura traz receipt.device; o núcleo confere esse valor com capabilities.currentDevice. Os dois adaptadores nativos conferem novamente o EXIF dos próprios bytes locais antes de detectar rostos ou comparar, sem confiar no metadado enviado pelo JavaScript. Arquivos acima de 32 MiB são recusados.
  • requireCaptureMetadata: false agora é uma política inválida. Não há opção na interface para desabilitar o filtro. Modelos com nomes EXIF não correspondentes serão excluídos, mesmo que sejam fotos legítimas; nunca se presume a equivalência.
  • Esse filtro não é um fingerprint individual: aparelhos iguais compartilham o mesmo modelo e EXIF pode ser editado. Não prova origem, titularidade ou identidade.
  • Bibliotecas grandes são processadas em lotes com limite explícito.
  • Cópias editadas/compartilhadas podem perder metadados. Isso não indica fraude.

Trocar o motor#

Implemente BiometricEngine com capabilities, captureReference, compare e clear. A captura deve ser passiva + ativa, com uma única face, referência vinculada à captura aprovada e expiração real no código nativo. Não preencha pontuações fictícias nem use comparação genérica de strings de embedding.

O adaptador atual chama evaluateFaceSimiByFeature no Android e evaluateSimilarity(f1:f2:) no iPhone. Os formatos internos das bibliotecas nativas são opacos.

Para suportar a API JSON com imagens, implemente também EvidenceEngine, valide EXIF nos mesmos bytes usados na inferência e gere as cópias a partir das imagens já analisadas, não reabrindo a galeria. Não anuncie evidenceVersion: 1 sem vínculo de sessão, expiração, hash das cópias e limpeza comprovável.

Licença e verificação#

Você pode integrar o Face Local em aplicativos com sua própria marca (white-label). Não é permitido vender ou revender o SDK, inclusive modificado, reempacotado ou apresentado sob outro nome. Essa restrição não impede a comercialização do seu aplicativo que utiliza o SDK.

Consulte a licença de uso do Face Local, incluída no pacote. As novas condições não alteram as permissões de versões anteriormente distribuídas. Não há certificação de anti-spoofing, prova forense ou identidade civil.

Execute pnpm --filter @workspace/face-local-sdk test. Consulte docs/TESTES-EM-APARELHO.md no workspace para os testes físicos necessários.