Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
a249933
feat(computador): modelo de hosts, codec tolerante e migração do host…
yan-vidal Oct 30, 2026
8452745
feat(computador): known_hosts e política de confiança no primeiro uso
yan-vidal Oct 30, 2026
27e60d7
feat(computador): modelo de chaves, importação e assinatura abstrata
yan-vidal Oct 30, 2026
32cddb6
feat(computador): chaves no Keystore com envelope e biometria
yan-vidal Oct 30, 2026
1f56746
Merge remote-tracking branch 'origin/main' into equipe/computador-b
yan-vidal Oct 30, 2026
07beff9
fix(computador): Base64 próprio, porque java.util.Base64 exige API 26…
yan-vidal Oct 30, 2026
f624e16
test(computador): asset da chave de teste vem do APK de teste; T8 con…
yan-vidal Oct 30, 2026
7e814b9
fix(computador): índice do cofre com trava do processo e detector de …
yan-vidal Oct 31, 2026
8acd6cc
docs(computador): lição das correções da revisão do #89
yan-vidal Oct 31, 2026
f172ae2
feat(computador): conexão SSH pelo cbssh com verificação de servidor …
yan-vidal Oct 31, 2026
56b5296
feat(computador): sessões, gerenciador, reconexão e buffer de texto
yan-vidal Oct 31, 2026
7a3ba94
feat(computador): persistência de hosts, chaves e known_hosts e migra…
yan-vidal Oct 31, 2026
a422cfe
feat(computador): serviço em primeiro plano para manter as sessões
yan-vidal Oct 31, 2026
f784caa
docs(computador): status e lição das tasks CMP-T9 a T12
yan-vidal Oct 31, 2026
d95dd2f
test(computador): o teste do serviço concede POST_NOTIFICATIONS para …
yan-vidal Oct 31, 2026
349c42f
fix(computador): o serviço só se derruba sem sessão viva e sem comand…
yan-vidal Oct 31, 2026
001b296
fix(computador): a opção da tela apagada é um estado único lido por q…
yan-vidal Oct 31, 2026
bb94fb5
test(computador): o teste do serviço diz em que etapa esgotou o tempo
yan-vidal Oct 31, 2026
26ead81
test(computador): o WakeLock é conferido só entre os ativos do dumpsys
yan-vidal Oct 31, 2026
fc4e578
fix(computador): a sentinela existente é consultada em qualquer API e…
yan-vidal Oct 31, 2026
49e226c
Merge equipe/computador-b em equipe/computador-c
yan-vidal Oct 31, 2026
44032b8
wip
yan-vidal Oct 31, 2026
8f63161
wip: itens do #92
yan-vidal Oct 31, 2026
21dfc74
fix(computador): leitura incerta da marca de invalidação bloqueia a c…
yan-vidal Oct 31, 2026
7f8bfa4
Merge equipe/computador-b em equipe/computador-c
yan-vidal Oct 31, 2026
46977cf
docs(computador): lição da rodada de revisão do #92
yan-vidal Oct 31, 2026
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
38 changes: 19 additions & 19 deletions .specs/features/computer-terminal/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -386,38 +386,38 @@ 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-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-16 | P1-B | CMP-T7, CMP-T8 | Parcial — sentinelas e `SecretFilter` (CMP-T7/T8) verificados |
| CMP-17 | P1-C | CMP-T9 | Parcial — adaptador cbssh (CMP-T9) verificado contra o servidor de teste; falta a tela |
| CMP-18 | P1-C | CMP-T6, CMP-T16 | Parcial — política TOFU (T6) e verificação suspensa na conexão (T9); falta a tela (T16) |
| CMP-19 | P1-C | CMP-T6, CMP-T11 | Parcial — `known_hosts` em arquivo atômico e rotação (T6, T11); exclusão em cascata verificada |
| 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 |
| CMP-22 | P1-C | CMP-T10, CMP-T20 | Parcial — reconexão, backoff, biometria e buffer (CMP-T10) verificados; falta a tela |
| CMP-23 | P1-C | CMP-T9, CMP-T13 | Pending |
| CMP-24 | P1-D | CMP-T13, CMP-T17 | Pending |
| CMP-25 | P1-D | CMP-T13 | Pending |
| CMP-26 | P1-D | CMP-T14 | Pending |
| CMP-27 | P1-D | CMP-T14 | Pending |
| CMP-28 | P1-D | CMP-T13 | Pending |
| CMP-29 | P1-D | CMP-T10, CMP-T13 | Pending |
| CMP-29 | P1-D | CMP-T10, CMP-T13 | Parcial — limites 8/3 e ociosidade do agente (CMP-T10) verificados |
| CMP-30 | P1-D | CMP-T13 | Pending |
| CMP-31 | P1-D | CMP-T14, CMP-T17 | Pending |
| CMP-32 | P1-E | CMP-T12, CMP-T20 | Pending |
| CMP-33 | P1-E | CMP-T12, CMP-T19 | Pending |
| CMP-34 | P1-E | CMP-T10, CMP-T12 | Pending |
| CMP-32 | P1-E | CMP-T12, CMP-T20 | Parcial — serviço `specialUse`, notificação só com contagem (CMP-T12) verificados |
| CMP-33 | P1-E | CMP-T12, CMP-T19 | Parcial — manifesto, `property` e checklist da Play (CMP-T12); declaração no Console é [YAN] |
| CMP-34 | P1-E | CMP-T10, CMP-T12 | Parcial — serviço sobe com a 1ª sessão e cai com a última (CMP-T12); falta a tela |
| CMP-35 | P1-F | CMP-T13 | Pending |
| CMP-36 | P1-F | CMP-T10, CMP-T13 | Pending |
| CMP-36 | P1-F | CMP-T10, CMP-T13 | Parcial — buffer só em memória (CMP-T10) verificado; falta o emulador (T13) |
| CMP-37 | P2-G | CMP-T22 | Pending |
| CMP-38 | P2-G | CMP-T23 | Pending |
| CMP-39 | P2-G | CMP-T23 | Pending |
Expand All @@ -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
16 changes: 8 additions & 8 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 All @@ -186,7 +186,7 @@ para permitir essa verificação sem reescrever a branch; detalhes na [lição](

### Fase 1B — Conexão e sessão

### CMP-T9 — Camada SSH: a porta `ConexaoSsh` e o adaptador cbssh — **Status: pendente**
### CMP-T9 — Camada SSH: a porta `ConexaoSsh` e o adaptador cbssh — **Status: concluída em 2026-10-31**
- **What**: interface `ConexaoSsh` (`conectar`, `verificarServidor`, `autenticar`, `abrirShell(pty)`, `redimensionar`, `escrever`, `saida: Flow<ByteArray>`, `fechar`, `eventos`) e `ConexaoCbssh`: `SshClient` com `HostKeyVerifier` (usa a `PoliticaDeConfianca` e **suspende** até o usuário decidir),
`AuthHandler` (`onPublicKeysNeeded` → `Assinador.chavePublica`, `onSignatureRequest` → `Assinador.assinar`, `onKeyboardInteractivePrompt` → ponte para a UI, `password` → ponte), política criptográfica **padrão** do cbssh (sem algoritmos legados), `keepAliveIntervalMs`, `requestPty(TERM=xterm-256color, cols, rows, px)`, `resizeTerminal`.
`MapaDeErros`: exceções do cbssh → `CausaSsh` (tabela do `spec.md` › Erros). **Modo teste de conexão** (`testar()`): conecta, verifica, autentica e fecha **sem** abrir shell.
Expand All @@ -197,7 +197,7 @@ para permitir essa verificação sem reescrever a branch; detalhes na [lição](
`Mudou` encerra com `CHAVE_DO_SERVIDOR_MUDOU` sem chamar o `AuthHandler`; `resize` chega ao servidor como `window-change`; keepalive: servidor que para de responder derruba em ≤ 3 intervalos; `testar()` não pede o shell; nenhum segredo em mensagem de exceção (sentinela); cancelar a coroutine fecha o socket.
- **Tests**: unit/integração JVM. **Gate**: `quick`. **Commit**: `feat(computador): conexão SSH pelo cbssh com verificação de servidor e erros mapeados`.

### CMP-T10 — Sessão, gerenciador, reconexão e buffer — **Status: pendente**
### CMP-T10 — Sessão, gerenciador, reconexão e buffer — **Status: concluída em 2026-10-31**
- **What**: `SessaoTerminal` (máquina de estados `Conectando · AguardandoServidor · Autenticando · Aberta · Reconectando · Encerrada(motivo) · Falhou(causa)`; `dono: USUARIO | AGENTE`, `compartilhada`), `GerenciadorDeSessoes` (abrir/fechar, **8 sessões no total, 3 do agente**, ociosidade só do agente, 10 min — D21; a sessão do usuário não fecha por ociosidade), `Reconexao`
(backoff 1·2·4·8·16·30 s, no máximo 10 tentativas ou 10 min; só com rede; chave com biometria **fora da janela de 5 minutos** emite `PrecisaDoUsuario` e não abre o prompt sozinha; **dentro** da janela reconecta sem novo prompt — D14), `BufferDeTexto` (anel de linhas de texto puro alimentado pelo mesmo fluxo de bytes, para o agente e para busca; **só em memória**) e `PortaDoEmulador` (interface que a tela implementa).
- **Where**: `computador/sessao/*.kt`; testes `SessaoTerminalTest`, `GerenciadorDeSessoesTest`, `ReconexaoTest`, `BufferDeTextoTest` (relógio virtual).
Expand All @@ -209,7 +209,7 @@ para permitir essa verificação sem reescrever a branch; detalhes na [lição](

### Fase 1C — Integração Android

### CMP-T11 — Persistência: hosts, confiança, chaves e a migração — **Status: pendente**
### CMP-T11 — Persistência: hosts, confiança, chaves e a migração — **Status: concluída em 2026-10-31**
- **What**: chave `computer_hosts_json` no DataStore `keyflow_settings` (padrão `bars_config_json`), `computer_keys_json` (metadados de chave, sem segredo), arquivo `known_hosts` em `filesDir` (fora do backup); `hostsFlow()`, `hostsOnce()` (leitura leve, sem cofre), `atualizarHosts(f)` atômico sobre o valor atual;
**migração** do host único no primeiro uso (T5): cria o host, move senha → segredo do host e chave privada → `ChaveSsh` importada, **apaga** `COMPUTER_SSH_PASSWORD`/`COMPUTER_SSH_KEY` do cofre e deixa `computerSsh*` ignorados no `Settings` (campos mantidos só para ler o legado; removidos numa task futura). Excluir host/chave com os efeitos em cascata (CMP-07, CMP-15).
- **Where**: `data/SettingsRepository.kt` (ou `computador/app/RepositorioComputador.kt` sobre o mesmo DataStore), `data/SettingsCredentialKeys.kt`, `res/xml/` regras de backup; teste instrumentado `RepositorioComputadorTest`.
Expand All @@ -219,7 +219,7 @@ para permitir essa verificação sem reescrever a branch; detalhes na [lição](
excluir host fecha as sessões dele (via fake do gerenciador), apaga a entrada do `known_hosts` e a senha lembrada e **mantém** a chave; excluir chave em uso pede a lista de hosts afetados e os deixa "sem chave"; `data_extraction_rules` exclui `known_hosts` e os segredos.
- **Tests**: instrumentado. **Gate**: `pool instrumentar com.keyflow.computador.app.RepositorioComputadorTest`. **Commit**: `feat(computador): persistência de hosts, chaves e known_hosts e migração do host único`.

### CMP-T12 — Serviço em primeiro plano `specialUse` e notificação — **Status: pendente**
### CMP-T12 — Serviço em primeiro plano `specialUse` e notificação — **Status: concluída em 2026-10-31**
- **What**: `SessoesService` (FGS `specialUse`, `FOREGROUND_SERVICE_SPECIAL_USE`, `<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE">` com o texto do `design.md` §7), canal "Sessões do Computador", notificação com a contagem, **Desconectar todas** e (com sessão do agente) **Parar o agente**;
o gerenciador a liga ao abrir a 1ª sessão (com o app visível) e a derruba ao fechar a última sessão (a ociosidade de 10 min fecha só a sessão do agente, D21, e não a do usuário); sem `POST_NOTIFICATIONS` o serviço funciona igual; processo morto pelo sistema → sessões marcadas `EncerradaPeloSistema` na reabertura.
Opção "Manter conectado com a tela apagada" (D22), desligada por padrão: desligada, o serviço não adquire `WakeLock`; ligada, adquire `PARTIAL_WAKE_LOCK` enquanto houver sessão do usuário. `WAKE_LOCK` é permissão normal e o lock só existe com a opção ligada.
Expand Down
Loading