Timevault Migration & Backups
Security-focused WordPress plugin for encrypted backups, selective exports, restores, and site migrations, with async jobs, audit logs, REST API, privacy-by-design defaults, and optional S3/Google Drive storage.
by Pedro Agostini · github.com/pedroagostini/timevault-migration
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/pedroagostini/timevault-migration/archive/refs/heads/main.zipReadme
Timevault Migration & Backups
Plugin WordPress privado (uso interno de agência — não será publicado no WP.org) para backup, exportação, importação e migração de sites, com segurança e LGPD como parte da arquitetura.
Documento mestre do projeto (arquitetura, requisitos de segurança, roteiro P0–P7): CLAUDE.md
Requisitos
- PHP 8.1+
- WordPress 6.2+
- Composer
Setup de desenvolvimento
composer install # autoload PSR-4 + Action Scheduler + phpcs/phpunit + wp-phpunit
composer lint # WordPress Coding Standards
composer test # PHPUnit (precisa do banco de testes — ver abaixo)
O plugin funciona sem
composer install(há um autoloader PSR-4 de fallback), mas a fila de jobs (Action Scheduler) só fica disponível com as dependências instaladas.
Testes (PHPUnit + WP test suite)
Os testes usam wp-phpunit (test suite do WordPress via Composer, sem svn) contra um banco
descartável timevault_tests. Configuração em tests/wp-tests-config.php
(aceita override por variáveis de ambiente: WP_CORE_DIR, WP_TESTS_DB_*, WP_PHP_BINARY).
# 1. crie o banco de testes uma vez
mysql -u root -e "CREATE DATABASE IF NOT EXISTS timevault_tests"
# 2. rode (php precisa estar no PATH; WP_CORE_DIR aponta para um WordPress local)
WP_CORE_DIR=/caminho/para/wordpress/ vendor/bin/phpunit
Cobertura (67 testes) com foco na superfície mais crítica (import/restore): PathGuard
(zip-slip/traversal), SqlImporter (tokenizer, construções proibidas, tabelas próprias, dump
corrompido), ArchiveInspector (checksum inválido, ZIP malicioso, manifest), EncryptionService
(round-trip, adulteração, truncamento), LocalAdapter + guard de SDK ausente, Anonymizer
(determinismo), BackupManager (run_now + integridade), ImportManager (validação + checksum) e
instalação automática da chave no wp-config.php.
Deploy: a pasta do plugin no servidor deve se chamar timevault (slug oficial).
Configuração de criptografia (wp-config.php)
Na ativação, o plugin tenta gerar uma chave forte automaticamente e gravar no wp-config.php,
antes do carregamento do wp-settings.php:
define( 'TIMEVAULT_ENCRYPTION_KEY', '<chave gerada automaticamente>' );
Se o wp-config.php não estiver gravável, a ativação continua, mas a chave precisa ser adicionada
manualmente:
// Chave de criptografia dos backups (base64 de 32 bytes aleatórios).
// Gere com: php -r "echo base64_encode(random_bytes(32)), PHP_EOL;"
define( 'TIMEVAULT_ENCRYPTION_KEY', '<chave>' );
Configuração opcional recomendada:
// Opcional, recomendado: diretório de backups FORA do webroot.
define( 'TIMEVAULT_BACKUP_DIR', '/caminho/fora/do/webroot/timevault' );
A chave nunca é armazenada no banco. O banco guarda apenas o status da tentativa automática
quando a escrita no wp-config.php falha. Sem TIMEVAULT_BACKUP_DIR, o plugin usa
wp-content/timevault-<sufixo aleatório>/ protegido por .htaccess (Apache/LiteSpeed),
web.config (IIS) e index.php vazio.
Nginx não lê .htaccess — adicione ao server block:
location ~ ^/wp-content/timevault- {
deny all;
}
Configuração opcional
// wp-config.php não tem: notificação por e-mail é uma option do site.
// Vazia (padrão) = desligada. Defina um e-mail para receber conclusão/falha:
update_option( 'timevault_notify_email', 'ops@agencia.com.br' );
API REST (/wp-json/timevault/v1)
Todos os endpoints exigem a capability manage_timevault (+ nonce X-WP-Nonce em auth por cookie).
| Endpoint | Método | Função |
|---|---|---|
/status |
GET | Saúde do ambiente (chave, fila, hardening) |
/overview |
GET | Agregado do dashboard (último backup, totais, espaço, jobs, retenção, agendamento) |
/schedule |
GET/POST | Lê/define o backup automático (frequência + quantos manter) |
/backups |
GET | Histórico paginado (per_page, page) |
/backups |
POST | Agenda backup (type: full|db; files_scope: wp-content|full) → 202 + uuid |
/backups/{uuid} |
GET | Status de um backup (polling de progresso) |
/exports |
POST | Agenda export seletivo (tables[], include_uploads, anonymize) → 202 + uuid |
/exports/tables |
GET | Tabelas disponíveis para export seletivo |
/import |
POST | Importa um pacote enviado (multipart package) — valida e registra p/ restaurar |
/backups/{uuid}/download-token |
POST | Emite token HMAC de 5 min para download |
/download?token= |
GET | Baixa o backup (auth pelo token assinado; checksum verificado antes) |
/destinations |
GET | Destinos configurados (credenciais nunca retornadas) |
/destinations/{s3\|gdrive} |
POST/DELETE | Configura/remove destino externo (opt-in) |
/restore/prepare |
POST | Valida o pacote e emite token de confirmação (rate-limited) |
/restore/confirm |
POST | Confirma (frase RESTORE + token) e agenda o restore (rate-limited) |
/restores |
GET | Histórico de restaurações |
/restores/{uuid} |
GET | Status de um restore (polling) |
/privacy/processing-record |
GET | Registro de operações de tratamento (LGPD) |
/privacy/retention |
GET/POST | Lê/define a política de retenção |
/privacy/retention/run |
POST | Executa a varredura de retenção agora |
Privacidade / LGPD (P5)
- Anonimização opt-in de exports staging/dev: com
"anonymize": truenoPOST /exports, dados pessoais (email, nome, URL, telefone, endereço emwp_users/wp_usermeta/wp_comments) são pseudonimizados na origem (transformador de linha no dumper, nunca reescrevendo SQL). O mascaramento é determinístico (HMAC por site) — o mesmo valor mascara igual, preservando joins e unicidade, mas o dado real nunca sai do site. O manifest registraprivacy.anonymized. - Retenção automática: política configurável (
max_age_days,max_count, pisomin_keep) aplicada por uma varredura diária (Action Scheduler). Backups expirados têm o artefato apagado e o registro marcadoexpired(mantido para prestação de contas). Nunca deleta abaixo do piso. - Registro de operações de tratamento gerado automaticamente: dados pessoais tocados, finalidades, destinos (com região p/ transferência internacional, Art. 33), retenção e postura de segurança — pronto para anexar à documentação de compliance da agência.
- Sem telemetria: nada é enviado para fora do site sem opt-in explícito e configurável.
Restauração / migração (P2 — camada mais crítica)
O restore roda num pipeline assíncrono de 6 etapas
(safety_backup → validate → extract → restore_db → restore_files → finalize)
com salvaguardas não-negociáveis, nessa ordem:
- Backup de segurança automático: um backup full do estado atual é feito até o fim (síncrono) antes de qualquer sobrescrita. Se falhar, o restore aborta sem tocar no site.
- Validação: o SHA-256 do artefato é verificado antes de processar os bytes; então
decifra (autenticado) e cada entrada do ZIP é validada (PathGuard: rejeita
../, caminhos absolutos, drive letters, backslash, NUL, fora dos prefixos); o manifest é lido como JSON (nuncaunserialize). Guarda também contra zip-bomb (tamanho/razão de compressão). - Extração: cada entrada é streamada para um alvo revalidado dentro de um staging
(nunca
ZipArchive::extractTosobre nomes hostis). - Restore de SQL: um importador tokeniza o dump respeitando strings/comentários, classifica
cada statement contra uma whitelist e executa um a um via
$wpdb->query()— nuncaeval, nunca multi-statement. Construções perigosas (INTO OUTFILE,LOAD DATA,LOAD_FILE,DROP DATABASE,GRANT…) abortam o restore. As tabelas operacionais do próprio Timevault (audit log, registro de backups/restores) são preservadas para não perder a trilha de auditoria nem a referência ao backup de segurança. As options internas (localização do diretório de backup, versão de schema) e o estado "plugin ativo" também são preservados. - Dupla confirmação + rate limiting são exigidos na camada REST antes de agendar: o cliente
precisa do token de
/restore/preparee ecoar a frase exataRESTOREem/restore/confirm. Toda tentativa é auditada.
Fluxo do cliente:
POST /restore/prepare { backup_uuid } -> { summary, confirm_token, confirm_phrase }
POST /restore/confirm { token, confirm:"RESTORE" } -> 202 { restore_uuid }
GET /restores/{uuid} -> status (polling)
Destinos externos (opt-in)
Nenhum destino externo é ativado por padrão. Para usar S3 ou Google Drive:
- Instale o SDK correspondente (autoload condicional — só onde for usar):
composer require aws/aws-sdk-phpoucomposer require google/apiclient - Configure via
POST /destinations/s3(bucket, prefix dedicado, region, credentials{access_key, secret_key}) ouPOST /destinations/gdrive(folder_id dedicado, region, credentials = JSON da service account — escopo mínimodrive.file). - Credenciais são cifradas com a
TIMEVAULT_ENCRYPTION_KEYantes de tocar o banco — sem a chave configurada, o plugin recusa salvá-las. A região de cada destino fica registrada no log de auditoria (LGPD Art. 33). - Agende backups com
"storage": "s3"(ou"gdrive") noPOST /backups.
O pipeline roda em 3 etapas assíncronas via Action Scheduler (dump_db → package → finalize);
o resultado é um timevault-{tipo}-{data}-{uuid8}.zip[.enc] no diretório protegido, com
SHA-256 registrado no banco e manifest JSON embutido.
Criptografia em repouso
Com TIMEVAULT_ENCRYPTION_KEY definida, todo backup é cifrado por streaming: libsodium
secretstream (XChaCha20-Poly1305) quando disponível — autenticação por chunk com proteção
nativa contra truncamento/reordenação — e fallback OpenSSL AES-256-GCM em chunks com AAD.
O checksum SHA-256 é calculado sobre o artefato final (pós-criptografia), permitindo validar
integridade antes de qualquer restore sem decifrar.
Estrutura (camadas)
timevault.php Bootstrap: constantes, autoload, hooks de ativação
src/
├── Plugin.php Container de serviços + boot
├── Activation/ Activator (tabelas via dbDelta, capability,
│ hardening do diretório) e Deactivator
├── Admin/ AdminMenu — dashboard (placeholder até P6)
├── Rest/ AbstractController (permission_callback real)
│ + StatusController (GET /timevault/v1/status)
├── Core/
│ ├── AuditLog.php Log de auditoria append-only (funcional)
│ ├── EncryptionService.php Gestão de chave (cifras em P1)
│ ├── BackupManager.php Contrato — implementação em P1
│ ├── ExportManager.php Contrato — implementação em P1
│ ├── ImportManager.php Contrato — implementação em P2 (camada mais crítica)
│ └── PrivacyService.php Contrato — implementação em P5 (LGPD)
├── Storage/ StorageAdapterInterface (Strategy) + LocalAdapter
├── Queue/ JobQueue — wrapper do Action Scheduler
└── Support/ Capabilities (manage_timevault) e Paths (hardening)
Convenções
| Item | Valor |
|---|---|
| Slug / pasta | timevault |
| Namespace PHP | Timevault\ |
| Prefixo funções/tabelas/options | timevault_ |
| Capability | manage_timevault |
| Text domain | timevault |
| Grupo Action Scheduler | timevault |
| Namespace REST | timevault/v1 |
Decisões de segurança já aplicadas no scaffolding
- Capability dedicada
manage_timevault(não reaproveitamanage_options). - Todo endpoint REST herda de
AbstractControllercompermission_callbackreal — nunca__return_true. - Diretório de backup com sufixo aleatório não enumerável + arquivos de negação de acesso; suporte a diretório fora do webroot via constante.
- Log de auditoria append-only (a classe não expõe update/delete), com redação automática de chaves sensíveis (
password,token,secret…) e IP pseudonimizado (hash com salt) — nunca segredos em logs. - Chave de criptografia exclusivamente em
wp-config.php, nunca no banco. Update URI: falseno header — impede takeover por colisão de slug no WP.org.- Desinstalação conservadora: dados só são removidos com opt-in explícito; arquivos de backup nunca são apagados automaticamente.
- Nenhuma chamada de rede externa por padrão (privacy by design); destinos externos (P3) serão sempre opt-in.
Roadmap
P0 scaffolding ✅ → P1 backup/export → P3 storage adapters → P2 import/restore → P5 LGPD → P7 testes → P4 auditoria de segurança → P6 UI/UX. Detalhes em CLAUDE.md.