Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions .specs/features/computer-terminal/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -386,21 +386,21 @@ Nenhuma. Q1–Q13 foram respondidas em 29/10 (D12–D24). Q8, Q9, Q10, Q11 e Q13
| CMP-02 | P0 | CMP-T1 | Verified — termlib 0.3.14/fork e cbssh 0.6.6; 126 componentes com POMs/hashes/licenças e 4/4 catracas; build/lint, 2.709 JVM e 20 instrumentados verdes; limites e riscos na [lição](../../project/licoes/2026-10-30-computador-a.md) |
| CMP-03 | P0 | CMP-T1, CMP-T3 | Verified — JNI do CI com SHA fixado; 3 ABIs no APK, ELF/ZIP 16 KB e hashes conferidos, 6/6 catracas; [lição](../../project/licoes/2026-10-30-computador-a.md) |
| CMP-04 | P0 | CMP-T4 | Verified (2026-10-30) |
| CMP-05 | P1-A | CMP-T5, CMP-T11, CMP-T15 | Pending |
| CMP-06 | P1-A | CMP-T5, CMP-T11 | Pending |
| CMP-05 | P1-A | CMP-T5, CMP-T11, CMP-T15 | Parcial — modelo/codec (CMP-T5) verificados; faltam T11 e T15 |
| CMP-06 | P1-A | CMP-T5, CMP-T11 | Parcial — migração pura (CMP-T5) verificada; falta T11 |
| CMP-07 | P1-A | CMP-T11, CMP-T15 | Pending |
| CMP-08 | P1-A | CMP-T9, CMP-T15 | Pending |
| CMP-09 | P1-A | CMP-T5, CMP-T15 | Pending |
| CMP-10 | P1-B | CMP-T7, CMP-T8, CMP-T16 | Pending |
| CMP-11 | P1-B | CMP-T2, CMP-T8, CMP-T16 | Pending |
| CMP-12 | P1-B | CMP-T7, CMP-T16 | Pending |
| CMP-13 | P1-B | CMP-T7, CMP-T8, CMP-T16 | Pending |
| CMP-14 | P1-B | CMP-T2, CMP-T8, CMP-T10, CMP-T16 | Pending |
| CMP-09 | P1-A | CMP-T5, CMP-T15 | Parcial — regra `podeMarcar` (CMP-T5) verificada; falta T15 |
| CMP-10 | P1-B | CMP-T7, CMP-T8, CMP-T16 | Parcial — geração no Keystore/envelope (CMP-T8) implementada; falta T16 |
| CMP-11 | P1-B | CMP-T2, CMP-T8, CMP-T16 | Parcial — sonda e envelope (CMP-T8); falta T2 e T16 |
| CMP-12 | P1-B | CMP-T7, CMP-T16 | Parcial — importação (CMP-T7) verificada; falta T16 |
| CMP-13 | P1-B | CMP-T7, CMP-T8, CMP-T16 | Parcial — pública (CMP-T7/T8); falta T16 |
| CMP-14 | P1-B | CMP-T2, CMP-T8, CMP-T10, CMP-T16 | Parcial — janela de 300 s (CMP-T8); falta T2, T10, T16 |
| CMP-15 | P1-B | CMP-T11, CMP-T16 | Pending |
| CMP-16 | P1-B | CMP-T7, CMP-T8 | Pending |
| CMP-16 | P1-B | CMP-T7, CMP-T8 | Parcial — sentinelas e `SecretFilter` (CMP-T7/T8) verificados |
| CMP-17 | P1-C | CMP-T9 | Pending |
| CMP-18 | P1-C | CMP-T6, CMP-T16 | Pending |
| CMP-19 | P1-C | CMP-T6, CMP-T11 | Pending |
| CMP-18 | P1-C | CMP-T6, CMP-T16 | Parcial — política TOFU (CMP-T6) verificada; falta T16 |
| CMP-19 | P1-C | CMP-T6, CMP-T11 | Parcial — `known_hosts` e rotação (CMP-T6) verificados; falta T11 |
| CMP-20 | P1-C | CMP-T9, CMP-T16 | Pending |
| CMP-21 | P1-C | CMP-T9, CMP-T16 | Pending |
| CMP-22 | P1-C | CMP-T10, CMP-T20 | Pending |
Expand Down Expand Up @@ -443,7 +443,7 @@ Nenhuma. Q1–Q13 foram respondidas em 29/10 (D12–D24). Q8, Q9, Q10, Q11 e Q13
| CMP-59 | P2-I | CMP-T15, CMP-T17 | Pending |
| CMP-60 | P2-I | CMP-T17 | Pending |
| CMP-61 | P2-I | CMP-T15, CMP-T16, CMP-T33 | Pending |
| CMP-62 | P2-I | CMP-T5, CMP-T31 | Pending |
| CMP-62 | P2-I | CMP-T5, CMP-T31 | Parcial — regra de arquitetura `computador/` (CMP-T5) ativa; falta T31 |
| CMP-63 | P2-I | CMP-T4, CMP-T20 | Partial (T4 verificada; T20 pendente) |
| CMP-64 | P2-I | CMP-T18, CMP-T28, CMP-T36 | Pending |
| CMP-65 | P2-I | CMP-T19, CMP-T28, CMP-T36 | Pending |
Expand Down
8 changes: 4 additions & 4 deletions .specs/features/computer-terminal/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ para permitir essa verificação sem reescrever a branch; detalhes na [lição](

### Fase 1A — Modelo e dados (JVM puro)

### CMP-T5 — Hosts: modelo, codec, validação e migração do host único `[P]` — **Status: pendente**
### CMP-T5 — Hosts: modelo, codec, validação e migração do host único `[P]` — **Status: concluída em 2026-10-30**
- **What**: `Host`, `AutenticacaoHost`, `PoliticaAgente`, `ConfigHosts`, limites (50 hosts, nome 1..40 único, usuário 1..64, porta 1..65535), `ValidadorHost` (endereço: nome DNS, IPv4, IPv6 com colchetes; motivo por campo) e `CodecHosts` (`org.json`, `"v":1`, tolerante: nunca lança,
item ruim descartado, campo desconhecido ignorado, ids repetidos viram um). `RegraAgente.podeMarcar(host)` (CMP-09: só autenticação resolvida pelo app). `MigracaoHostUnico.migrar(settingsAntigo) → ConfigHosts?` (função pura; a senha e a chave saem em um `SegredosMigrados` à parte, nunca no host).
- **Where**: `app/src/main/java/com/keyflow/computador/{Modelo,ValidadorHost,CodecHosts,RegraAgente,MigracaoHostUnico}.kt`; testes `CodecHostsTest`, `CodecHostsPropertyTest`, `CodecHostsFuzzTest`, `HostSemSegredoTest`, `MigracaoHostUnicoTest`, `ValidadorHostTest`. Regra no `ArchitectureRulesTest`: `computador/*.kt` (exceto `computador/app/**`) sem `android.*`, compose e `org.connectbot.terminal`.
Expand All @@ -152,7 +152,7 @@ para permitir essa verificação sem reescrever a branch; detalhes na [lição](
IPv6/porta/nome inválidos recusados com o motivo; `podeMarcar` recusa auth "pergunte sempre" e chave com passphrase pedida na hora; migração: `computerSshHost` vazio → nada; preenchido → 1 host "Servidor SSH" com porta, usuário e auth do antigo e **segredos à parte**; idempotente.
- **Tests**: unit. **Gate**: `quick`. **Commit**: `feat(computador): modelo de hosts, codec tolerante e migração do host único`.

### CMP-T6 — known_hosts e política de confiança (TOFU) `[P]` — **Status: pendente**
### CMP-T6 — known_hosts e política de confiança (TOFU) `[P]` — **Status: concluída em 2026-10-30**
- **What**: `ImpressaoDigital` (SHA256 base64 sem padding como o OpenSSH, mais a "arte" aleatória opcional), `RepositorioDeConfianca` (interface) e `PoliticaDeConfianca.avaliar(host, porta, chave, repositorio) → Conhecida | Nova(impressao) | Mudou(antiga, nova)`;
formato do arquivo `known_hosts` compatível com OpenSSH (`[host]:porta algoritmo base64`, sem hash de hostname — limite do cbssh), leitor/escritor tolerante, e `AtualizadorDeChaves` (rotação `hostkeys` só quando a chave autenticada já é conhecida).
- **Where**: `computador/{ImpressaoDigital,PoliticaDeConfianca,KnownHostsArquivo}.kt`; testes `PoliticaDeConfiancaTest`, `KnownHostsArquivoTest`, `KnownHostsPropertyTest`.
Expand All @@ -162,7 +162,7 @@ para permitir essa verificação sem reescrever a branch; detalhes na [lição](
arquivo com linha malformada preserva as outras; remover por host não toca os demais; rotação sem chave conhecida autenticada é recusada.
- **Tests**: unit. **Gate**: `quick`. **Commit**: `feat(computador): known_hosts e política de confiança no primeiro uso`.

### CMP-T7 — Chaves: modelo, importação e assinatura abstrata `[P]` — **Status: pendente**
### CMP-T7 — Chaves: modelo, importação e assinatura abstrata `[P]` — **Status: concluída em 2026-10-30**
- **What**: `ChaveSsh` (id, nome, tipo, impressão, `Protecao {HARDWARE, KEYSTORE_ENVELOPE, SOFTWARE_COFRE}`, `exigeBiometria`, criada em — **sem material secreto**; `toString` mascara), interface `Assinador { chavePublica(): AuthPublicKey; suspend assinar(dados): ByteArray? }` (liga ao `AuthHandler.onSignatureRequest` do cbssh),
`ImportadorDeChave` (OpenSSH e PEM PKCS#8/PKCS#1 pelo `SshKeys.decodePemPrivateKey`; passphrase pedida na hora, nunca guardada; rejeita DSA e RSA < 2048 com o motivo), formatador da chave pública (`ssh-ed25519 AAAA…` ou `ecdsa-sha2-nistp256 AAAA…`, comentário `keyflow@<aparelho>`), `AgenteSegredos.valores()` ampliado para o `SecretFilter`.
Tudo sobre portas (`CofreDeSegredos`, `RelogioDeChaves`), com fakes.
Expand All @@ -173,7 +173,7 @@ para permitir essa verificação sem reescrever a branch; detalhes na [lição](
a passphrase e o texto colado não ficam em nenhum campo depois de `importar()`; o `SecretFilter` recebe os valores guardados.
- **Tests**: unit. **Gate**: `quick`. **Commit**: `feat(computador): modelo de chaves, importação e assinatura abstrata`.

### CMP-T8 — Chaves no Android: Keystore, envelope e biometria — **Status: pendente**
### CMP-T8 — Chaves no Android: Keystore, envelope e biometria — **Status: concluída em 2026-10-30 (T2 pendente; ver a lição)**
- **What**: `ProvedorDeChavesAndroid` sobre o resultado da T2. `gerar(nome, tipo, exigeBiometria)`: **Ed25519** no Keystore (API 33+ e sonda aprovada) com `ProbeEd25519` de produção, ou **fallback** — Ed25519 em software (a semente de 32 bytes gerada pelo Tink do cbssh) guardada **cifrada** por uma chave AES-256-GCM do Keystore (API 24+) no cofre do app, zerada da memória logo após assinar; **ECDSA P-256** (`secp256r1`, `DIGEST_SHA256`) sempre no Keystore, sem envelope (D13).
Importadas entram no envelope. Biometria opcional por chave, janela de **5 minutos** (D14): API 24–29 `setUserAuthenticationValidityDurationSeconds(300)`; API 30+ `setUserAuthenticationParameters(300, AUTH_BIOMETRIC_STRONG)`; duração > 0 **não** usa `CryptoObject`. `setUserAuthenticationRequired(true)`, `setInvalidatedByBiometricEnrollment(true)` com aviso antes de ligar; sem biometria cadastrada a opção fica desligada com o motivo. O prompt autentica o usuário; dentro dos 300 s a assinatura segue sem novo prompt.
Exclusão apaga Keystore e cofre. Fora do backup (`data_extraction_rules.xml`/`backup_rules`).
Expand Down
47 changes: 47 additions & 0 deletions .specs/project/licoes/2026-10-30-computador-b.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# 2026-10-30 — Computador B: hosts, known_hosts e chaves (CMP-T5..T8)

Branch `equipe/computador-b` (base: `equipe/computador-a`, depois `origin/main` com o #87).

## O que entrou
- **T5** `computador/{Modelo,ValidadorHost,CodecHosts,RegraAgente,MigracaoHostUnico}.kt`: codec que nunca lança (fuzz de 10 mil mutações),
`RegraAgente` também aplicada ao decodificar (arquivo editado à mão não liga o agente), migração pura e determinística (ids fixos); um legado
que o validador recusaria **não** é migrado nem apagado (senão o codec o descartaria e o dado se perderia).
- **T6** impressão `SHA256:` e arte iguais ao `ssh-keygen -lvf` (vetores), `known_hosts` no formato OpenSSH (porta 22 sem colchetes),
linhas que o app não entende voltam byte a byte. Mesmo tipo + chave diferente = `Mudou`; tipo ainda desconhecido = `Nova` com `outrosTipos`
(a tela pode alertar). Rotação só acrescenta tipos novos e só se a chave autenticada já era conhecida.
- **T7** importador sem estado (`CharArray` zerados), pré-inspeção do cabeçalho para distinguir senha ausente/errada e recusar DSA antes de decifrar;
RSA < 2048 recusada; material = semente Ed25519 ou OpenSSH sem senha. Públicas conferidas contra `ssh-keygen -y` com chaves falsas versionadas.
- **T8** `computador/app/{ProvedorDeChavesAndroid,EnvelopeDeChave,SondaEd25519,BiometriaAndroid,CofreDoApp}.kt`; `androidx.biometric:1.1.0`
(Apache-2.0; +15 componentes Apache-2.0 no inventário, 141 no total).

## SPEC_DEVIATION
- `org.connectbot.sshlib` também é importado por `chaves/ImportadorDeChave.kt` e `chaves/AssinadorSoftware.kt` (o design previa só o `ConexaoCbssh`);
a regra do `ArchitectureRulesTest` lista os três arquivos. `Assinador` usa `ChavePublicaSsh` própria, não o `AuthPublicKey` do cbssh.
- `Assinador`/provedor recebem o `ChaveSsh` (`assinador(chave)`), não só o id: os metadados vivem na T11.
- Tink entra como dependência direta (`tink-android:1.24.0`, a mesma variante do grafo) para o `Ed25519Sign`.
- O commit da T7 sozinho não compila (a dependência direta do Tink está no `build.gradle.kts`, commitado com a T8).
- **T2 (spike) não foi feita**: a T8 foi escrita com o veredito da sonda de produção, medido só no emulador API 34; a tabela por aparelho segue pendente.
- Protecao: `HARDWARE` = Keystore em TEE/StrongBox, `SOFTWARE_COFRE` = Keystore por software, `KEYSTORE_ENVELOPE` = envelope.

## Lições
- KDoc de Kotlin aninha comentários: `app/**` dentro de `/** … */` abre outro e quebra o arquivo inteiro.
- Biometria não dá para exercitar no emulador sem cadastro: `aplicarBiometriaNoKeystore=false` (só teste) deixa a lógica da janela
testável; a escolha de parâmetros por API é função pura (`PlanoDeAutenticacao`). Cifrar com chave AES que exige autenticação também pede
a janela aberta: criar chave em envelope com biometria pede o prompt antes.
- `pool compilar` sincroniza a árvore no momento em que o nó é obtido: editar arquivos durante a espera entra na execução.

## Gates
Unitários **2.853/2.853** (284 classes), `:app:lintDebug` e `assembleDebugAndroidTest` verdes; instrumentado `ChavesAndroidTest` **16/16** no emulador API 34
(sonda do Ed25519 no Keystore: **Aprovada**, então ed25519 nasceu nativo; o envelope foi forçado por flag). Mutações (uma por task, vermelho confirmado):
`RegraAgente` (Perguntar liberado), `PoliticaDeConfianca` (Mudou→Nova), `ImportadorDeChave` (RSA fraca aceita), `JanelaDeBiometria` (janela que nunca vence).
Os testes foram escritos junto do código; o "vermelho antes" foi substituído pela mutação.

## Correções da revisão do #89 (2 P2)
- **Cofre**: o índice de `CofreDoApp` mora no `CredentialStore` compartilhado, mas a trava era por instância: duas instâncias perdiam uma entrada e o
`SecretFilter` deixava de remover o segredo. Agora a trava é do processo; `CofreDoAppTest` (duas instâncias, gravação lenta do índice) ficou vermelho
com a trava antiga e verde com a nova.
- **Biometria nas APIs 24–29**: duração positiva (300 s) prende a chave ao root SID e `setInvalidatedByBiometricEnrollment(true)` não garante a invalidação ao
cadastrar outra digital. `DetectorDeCadastro` cria uma chave sentinela (AES, duração -1) por chave com biometria; `KeyPermanentlyInvalidatedException` ao
inicializar a cifra = cadastro mudou. A chave vira `INVALIDADA`, `assinar` devolve `null` (`Assinador.invalidada = true`) sem abrir prompt; o material do envelope não é
regenerável (estava cifrado pela chave invalidada), então a pessoa gera/importa outra. Testado com um detector falso (`ChavesAndroidTest`, 19/19).
**Pendente (T21)**: o cadastro real de biometria e a sentinela real em API 24–29 em aparelho físico (o emulador API 34 não cria chave com autenticação sem tela de bloqueio).
8 changes: 6 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,10 @@ dependencies {
// CMP-T1: versões exatas; grafo, hashes e licenças auditados juntos.
implementation("org.connectbot.sshlib:sshlib:0.6.6")
implementation("org.connectbot.sshlib:protocol:0.6.6")
// CMP-T8: BiometricPrompt (Apache-2.0) para liberar as chaves SSH por 5 minutos. A 1.1.0 é a estável mais recente (as 1.2 são alfa).
implementation("androidx.biometric:biometric:1.1.0")
// CMP-T7: o assinador de software usa o Ed25519 do Tink direto (a mesma variante que o cbssh e o cofre já trazem; ver a substituição no fim do arquivo).
implementation("com.google.crypto.tink:tink-android:1.24.0")
// UI
implementation("androidx.core:core-ktx:1.13.1")
implementation("androidx.appcompat:appcompat:1.7.0")
Expand Down Expand Up @@ -740,9 +744,9 @@ val gerarGrafoTerminal = tasks.register("gerarGrafoTerminal") {
.filterIsInstance<ResolvedDependencyResult>().filter {
val id = it.selected.id
(id is ProjectComponentIdentifier && id.projectPath == ":termlib") ||
(id is ModuleComponentIdentifier && id.group == "org.connectbot.sshlib")
(id is ModuleComponentIdentifier && (id.group == "org.connectbot.sshlib" || (id.group == "androidx.biometric" && id.module == "biometric")))
}
check(raizes.size >= 2) { "Não achei termlib e cbssh no grafo do release" }
check(raizes.size >= 3) { "Não achei termlib, cbssh e biometric no grafo do release" }
raizes.forEach { visitar(it.selected) }
val artefatos = config.incoming.artifactView {
componentFilter { it is ModuleComponentIdentifier }
Expand Down
Loading