O aplicativo adota uma estratégia de defesa em profundidade com múltiplas camadas de segurança:
autenticação delegada ao Firebase Auth, autorização via RBAC centralizado (AccessPolicy),
armazenamento seguro de credenciais via Android Keystore e boas práticas de segurança Android
(backup rules, edge-to-edge, verificação de permissões).
O login de produção é delegado ao Firebase Authentication:
suspend fun loginWithFirebase(email: String, password: String): FirebaseLoginResult {
val normalizedEmail = email.trim().lowercase()
if (normalizedEmail.isBlank() || password.isBlank()) {
return FirebaseLoginResult.Error("Informe e-mail e senha.")
}
// signInWithEmailAndPassword via Firebase SDK
firebaseAuth.signInWithEmailAndPassword(normalizedEmail, password).await()
// ...
}Medidas de segurança aplicadas:
- E-mail normalizado (
trim().lowercase()) antes de qualquer verificação, prevenindo bypass por variação de case ou espaços - Validação de campos vazios antes da chamada ao Firebase
- Sessão Firebase encerrada com
firebaseAuth.signOut()se o perfil no Firestore não for encontrado, evitando autenticação parcial - Resultado tipado (
FirebaseLoginResult.Success/FirebaseLoginResult.Error) que nunca expõe exceções brutas à UI
Durante desenvolvimento/demo, um mecanismo de fallback autentica contra credenciais pré-cadastradas com hash SHA-256:
private val hashedCredentials = mutableMapOf(
"professor@sebrae.edu.br" to sha256("prof123"),
"coordenador@sebrae.edu.br" to sha256("coord123"),
"admin@sebrae.edu.br" to sha256("admin123"),
)
fun login(email: String, password: String): Boolean {
val normalizedEmail = email.trim().lowercase()
val expectedHash = hashedCredentials[normalizedEmail]
return if (expectedHash != null && expectedHash == sha256(password)) { ... }
}SHA-256 é implementado via java.security.MessageDigest:
private fun sha256(input: String): String =
MessageDigest.getInstance("SHA-256")
.digest(input.toByteArray(Charsets.UTF_8))
.joinToString("") { "%02x".format(it) }Atenção: o modo de fallback local usa credenciais hardcoded adequadas apenas para desenvolvimento. Em produção, apenas o Firebase Auth deve ser utilizado.
Após autenticação Firebase, o perfil do usuário é buscado na coleção users com
estratégia de fallback em três passos:
- Busca por
document(authUid)— caso padrão - Busca via
whereEqualTo("authUid", firebaseUid)— documentos com campo authUid - Busca via
whereEqualTo("email", email)— fallback por e-mail
Se nenhum perfil for encontrado, o login é abortado e a sessão Firebase é encerrada.
RESPONSAVEL → acesso mínimo: visualiza apenas alunos vinculados via guardian_students
PROFESSOR → leitura de alunos, turmas, cursos, calendário, certificados
ORIENTADOR_EDUCACIONAL → idem PROFESSOR + lançamento de acompanhamento pedagógico
PSICOPEDAGOGO → idem ORIENTADOR + acesso a dados psicológicos e pedagógicos
COORDENADOR → acesso intermediário (criar/editar alunos, cursos, turmas, instrutores)
ADMINISTRADOR → acesso completo (inclui gestão de usuários, configurações avançadas)
Hierarquia: ADMINISTRADOR > COORDENADOR > PSICOPEDAGOGO > ORIENTADOR_EDUCACIONAL > PROFESSOR > RESPONSAVEL
Todas as decisões de autorização passam pelo AccessPolicy.can():
fun can(role: UserRole?, resource: ProtectedResource, action: ProtectedAction): Boolean {
if (role == null) return false // sem sessão = sem acesso
return when (role) {
UserRole.ADMINISTRADOR -> true
UserRole.COORDENADOR -> coordinatorRules(resource, action)
UserRole.PSICOPEDAGOGO -> psicopedagogoRules(resource, action)
UserRole.ORIENTADOR_EDUCACIONAL -> orientadorRules(resource, action)
UserRole.PROFESSOR -> professorRules(resource, action)
UserRole.RESPONSAVEL -> responsavelRules(resource, action)
}
}| Recurso | Ação | RESPONSAVEL | PROFESSOR | ORIENTADOR | PSICOPEDAGOGO | COORDENADOR | ADMINISTRADOR |
|---|---|---|---|---|---|---|---|
| Students | View | ✅* | ✅ | ✅ | ✅ | ✅ | ✅ |
| Students | Create / Update | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| Students | Deactivate / Reactivate | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| Students | LaunchStudentRecord | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Students | Delete | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| StudentMonitoring | View | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| StudentMonitoring | Create / Update | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Courses / Classes | View | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Courses / Classes | Create / Update / Deactivate | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| Teachers | View | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| Teachers | Create / Update / Deactivate | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| Calendar | View | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Calendar | Create | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| Settings | View / ChangeOwnPassword | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Settings | ManualSync | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| Settings | ClearStorage / ResetDatabase | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| Users | CreateCompanyUsers | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| Users | Demais ações | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| Companies | Qualquer ação | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
*
RESPONSAVELvê apenas alunos vinculados a si via tabelaguardian_students.
O AccessPolicy é invocado em dois pontos:
- UI: botões, FABs e ações are conditionally renderizados com base no papel do usuário — evitar exibir ações que o usuário não pode executar
FirebaseDataConnectService: antes de executar mutações no Firestore, o serviço verifica a permissão:
if (!AccessPolicy.can(currentRole, ProtectedResource.Students, ProtectedAction.Create)) {
return Result.Error(SecurityException("Permissão negada"), "Sem permissão para criar aluno.")
}Credenciais de seed para auto-login do Firebase são cifradas com AES-256-GCM via Android Keystore, garantindo que a chave criptográfica nunca saia do hardware seguro:
private const val TRANSFORMATION = "AES/GCM/NoPadding"
private const val ANDROID_KEYSTORE = "AndroidKeyStore"
private const val KEY_ALIAS = "SeedCredentialKey"Fluxo de proteção:
Credencial (plaintext)
│
▼
AES/GCM encrypt ←── chave no Android Keystore (hardware-backed)
│
▼
Base64(ciphertext) + Base64(IV)
│
▼
SharedPreferences (apenas dados cifrados, nunca plaintext)
Para descriptografar:
- A chave é recuperada do Keystore — não pode ser exportada
- IV é armazenado junto ao ciphertext (não é segredo)
- GCM autentica a integridade do dado, detectando adulteração
Geração da chave:
KeyGenParameterSpec.Builder(KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(256)
.build()O AndroidManifest.xml declara apenas a permissão estritamente necessária:
<uses-permission android:name="android.permission.INTERNET" />Nenhuma permissão de localização, câmera, contatos ou armazenamento externo é solicitada, seguindo o princípio do mínimo privilégio para permissões Android.
O AndroidManifest.xml referencia regras customizadas de backup:
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"Estas regras controlam quais dados do aplicativo podem ser incluídos em backups Google/ADB, evitando que dados sensíveis (credenciais, dados de alunos) sejam extraídos via backup.
- O
AuthManagermantém_currentUsercomoMutableStateFlow<AppUser?>—nullindica sessão encerrada - Logout limpa a sessão local E chama
firebaseAuth.signOut():
fun logout() {
_currentUser.value = null
firebaseAuth.signOut()
}- Após logout, o
AppNavHostnavega para login removendo oHOMEdo back-stack (popUpTo(HOME) { inclusive = true }), impedindo acesso via botão voltar
- Toda comunicação com Firebase (Firestore e Auth) usa TLS/HTTPS por padrão via SDK
- Nenhuma URL HTTP insegura é utilizada no código
- Nenhuma chave de API ou segredo está hardcoded no código-fonte; apenas o
google-services.json(não versionado em produção) contém identificadores de projeto
- Room usa parâmetros vinculados em todas as queries (
@Querycom:param), protegendo contra SQL injection - Nenhuma query SQL é construída por concatenação de strings
A tabela app_users armazena passwordHash (SHA-256 hex), nunca a senha em texto plano.
O mesmo algoritmo é usado na verificação local:
val expectedHash = hashedCredentials[normalizedEmail]
expectedHash != null && expectedHash == sha256(password)O FirebaseScreenSyncPlanner aplica least-privilege de dados: cada tela sincroniza
apenas as coleções que precisa, reduzindo exposição de dados desnecessários em trânsito
e limitando o que fica em cache local.
- Remover credenciais hardcoded do fallback local: as senhas
prof123,coord123,admin123nunca devem existir em código de produção — desabilitar completamente oAuthManager.login()local em builds de release comBuildConfig.DEBUG - Adicionar salt ao hash SHA-256: o hash atual sem salt é vulnerável a ataques de
rainbow table — usar PBKDF2 ou bcrypt com salt aleatório para o hash do
passwordHashlocal - Implementar Certificate Pinning: fixar o certificado TLS do Firebase para prevenir
ataques MITM em redes comprometidas, usando
OkHttp CertificatePinnerou Network Security Config - Habilitar ProGuard/R8 em release: o
proguard-rules.proexiste mas a configuração de ofuscação deve ser revisada e ativada para builds de produção, dificultando engenharia reversa - Criptografar banco de dados local com SQLCipher: dados sensíveis de alunos (registros psicológicos, necessidades pedagógicas) armazenados no Room devem ser criptografados em repouso usando SQLCipher para Android
- Implementar detecção de root / emulador: bloquear ou alertar o usuário se o dispositivo estiver com root (use SafetyNet/Integrity API), reduzindo risco de extração de dados pelo sistema de arquivos
- Adicionar timeout de sessão: implementar auto-logout após período de inatividade, especialmente relevante dado que o app gerencia dados sensíveis de alunos
- Revisar regras do Firestore Security Rules: as
Security Rulesdo Firestore devem refletir exatamente a matriz de permissões doAccessPolicy, evitando que requisições diretas à API do Firestore (fora do app) contornem a autorização - Ocultar senha no
google-services.json: garantir quegoogle-services.jsonesteja no.gitignoredo repositório de produção e usar CI/CD secrets para injetá-lo no build - Adicionar verificação de integridade do APK: habilitar Play Integrity API para verificar que o app não foi modificado ou distribuído fora do Play Store