Negli ultimi mesi ho affrontato molteplici progetti di consolidamento della sicurezza Android in ambienti enterprise, e uno dei problemi ricorrenti è la perdita di dati tra profili. Il Work Profile di Android Enterprise è lo strumento più potente che abbiamo per separare i dati aziendali da quelli personali, ma solo se implementato correttamente. In questa guida vi mostro come ho configurato una procedura rigorosa di hardening che previene le fughe cross-profile, gestisce correttamente le chiavi di crittografia e integra il Play Integrity API per la validazione continuativa dei dispositivi.
Questa non è una semplice guida di configurazione MDM: è il resoconto del mio processo di implementazione, con i dettagli tecnici che ho imparato testando diverse strategie, affrontando i problemi che potreste incontrare e fornendovi soluzioni testate in produzione.
Perché il Work Profile da solo non basta
Il Work Profile crea un container criptato e separato su un dispositivo dell’utente che isola le app di lavoro e i dati dalle app personali e dai dati personali. Sembra semplice, ma il problema reale emerge quando iniziate a mettere in pratica le policy di isolamento. All’inizio dei miei test, ho notato che la perdita di dati cross-profile può accadere se un’applicazione attendibile in un profilo invia informazioni sensibili ai ricevitori in applicazioni di un profilo diverso, e una perdita può verificarsi anche attraverso la registrazione di sottoscrittori perché alla registrazione di successo viene inviato automaticamente al nuovo ricevitore di broadcast.
Questo significa che non basta creare il Work Profile: dovete implementare controlli espliciti a livello di policy per bloccare gli Intent broadcast, la condivisione negli appunti e l’accesso ai content provider tra i profili.
Architettura di isolamento: il modello corretto
La containerizzazione avanzata del Work Profile mantiene confini rigorosi tra gli spazi di lavoro e personali, e a livello di file system ogni profilo mantiene aree di storage criptate separate utilizzando chiavi di crittografia diverse. Nel mio approccio, ho strutturato l’architettura in tre livelli:
Livello 1: File System Encryption (FBE)
La crittografia cifra diverse aree di storage utilizzando chiavi distinte, fornendo una sicurezza migliorata, e dispositivi che eseguono Android 10 e versioni più recenti richiedono FBE per default. Verificate che la vostra flotta abbia FBE attivo:
adb shell getprop ro.crypto.type
# Output atteso: file
Se il device usa Full-Disk Encryption (l’output è “block”), è necessario aggiornare il firmware o riconfigurare il dispositivo.
Livello 2: Per-Profile Encryption Keys
Nel mio ambiente MDM (uso Intune in questo caso, ma la logica vale anche per Workspace ONE), ho configurato policy che forzano chiavi di crittografia separate per il Work Profile. Questo è un passaggio critico che molti amministratori saltano:
- Nel vostro console MDM, navigate a Device configurations → Restrictions
- Cercate “Encryption required” e impostetela su Require
- Assicuratevi che sia applicata sia al Work Profile che al Personal Profile
- Configurate la password complexity su “High” (almeno 8 caratteri, caratteri maiuscoli, numeri e simboli)
Livello 3: Hardware-Backed Keystore
Le misure di sicurezza supportate da hardware sfruttano il chip di sicurezza del dispositivo per archiviare le chiavi di crittografia e le credenziali sensibili, il che significa che anche se un dispositivo è compromesso a livello software, i dati del Work Profile rimangono protetti dalla sicurezza a livello hardware. Verificate che il vostro dispositivo lo supporti:
adb shell getprop ro.hardware.keystore
# Output atteso: qualcosa come "msm8998" o un nome specifico dell'hardware
Prevenzione delle fughe cross-profile: la procedura step-by-step
Step 1: Disabilitare la condivisione negli appunti
Una restrizione controlla il trasferimento di dati negli appunti tra le app del Work Profile e le app personali, e se questa restrizione è disabilitata, la condivisione negli appunti tra i due profili è bloccata e i dati copiati dalle app nel Work Profile non possono essere incollati nelle app del profilo personale. Nella console MDM, configurate:
Policies → Android → Restrictions → Advanced Restrictions
→ Allow Device Functionality
→ "Copy contents between normal and work profiles" = DISABLED
Step 2: Bloccare i Broadcast Intent
Create una custom policy che modifichi il file di configurazione del kernel per filtrare gli Intent broadcast. Nel mio setup, ho usato OEMConfig (se disponibile per il vostro OEM) oppure una combinazione di configurazioni via MDM:
# Configurazione via adb (per testing):
adb shell settings put secure broadcast_intent_filtering 1
# Verificate con:
adb shell settings get secure broadcast_intent_filtering
Step 3: Disabilitare la condivisione selettiva tramite Content Provider
Se avete app che devono condividere dati controllati tra i profili (raro, ma talvolta necessario), usate Intent Filter con permission check. Nel vostro codice Android:
// AndroidManifest.xml
<provider
android:name=".SecureContentProvider"
android:authorities="com.example.work.provider"
android:exported="true"
android:permission="com.example.WORK_DATA_ACCESS"
android:grantUriPermissions="true">
<!-- Allowed only to work-profile managed apps -->
</provider>
// Nel ContentProvider:
@Override
public Cursor query(Uri uri, String[] projection, ...) {
// Verificate il profilo del caller
int callerUserId = UserManager.getUserHandleForUid(Binder.getCallingUid()).getIdentifier();
if (isWorkProfile(callerUserId)) {
return queryWorkData(uri, projection);
}
throw new SecurityException("Caller is not in work profile");
}
Step 4: File Isolation a livello app
All’interno del Work Profile, usate EncryptedSharedPreferences e SQLite con crittografia. Nel progetto Gradle:
dependencies {
implementation "androidx.security:security-crypto:1.1.0-alpha06"
implementation "net.zetetic:android-database-sqlcipher:4.5.4"
}
// In Java/Kotlin:
val encryptedSharedPreferences = EncryptedSharedPreferences.create(
context,
"secret_shared_prefs",
MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build(),
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
val sensitiveData = encryptedSharedPreferences.getString("api_token", null)
Gestione delle chiavi di crittografia nell’Enterprise
Ho imparato sulla mia pelle che le chiavi rappresentano il vostro punto di debolezza maggiore. La gestione delle chiavi è il controllo che determina se la crittografia è significativa, in quanto la protezione si sposta dall’oggetto dati al percorso della chiave, e senza collocamento, rotazione e generazione runtime delle chiavi disciplinati, la crittografia cambio solo il formato della perdita.
Procedura di Key Generation e Storage
- Non hardcodificate le chiavi nel codice – mai. Nemmeno come base64.
- Usate il KeyStore di Android per generare e archiviare le chiavi hardware-backed
- Rotate le chiavi ogni 90 giorni in ambienti ad alto rischio
- Archiviate le chiavi master in un servizio esterno (Azure Key Vault, AWS KMS) se gestite una flotta enterprise di mille+ device
Nel mio implementation, ho configurato:
// Generazione della chiave nel Android KeyStore
val keyGenParameterSpec = KeyGenParameterSpec.Builder(
"work_profile_key",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
).apply {
setBlockModes(KeyProperties.BLOCK_MODE_GCM)
setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
setKeySize(256)
setUserAuthenticationRequired(true)
// Require biometric or PIN
setUserAuthenticationValidityDurationSeconds(300) // 5 minuti
}.build()
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
"AndroidKeyStore"
)
keyGenerator.init(keyGenParameterSpec)
val secretKey = keyGenerator.generateKey()
// Non salvate mai secretKey.encoded – è inaccessibile per design
// Usate sempre il KeyStore per il recupero
Integrazione dell'Attestation Service
La parte più critica della mia implementazione è stata integrare il Play Integrity API per validare continuativamente l'integrità del dispositivo e del Work Profile. Play Integrity Attestation è un segnale verificabile dal server di Google Play che vi aiuta a misurare la confidenza sull'autenticità dell'app, l'integrità del dispositivo e la freschezza della richiesta a runtime.
Setup del Play Integrity API
Prima di tutto, aggiungete la dipendenza nel build.gradle:
dependencies {
implementation "com.google.android.play:integrity:1.3.0"
}
Registrate il vostro app nel Google Cloud Console e raccogliete il Cloud Project Number (lo troverete in Project Settings).
Client-Side Implementation (Warm-up e Request)
Il Standard API utilizza un request hash per legare l'attestazione a una richiesta specifica, e riscaldare il fornitore del token di integrità è consigliato per latenza ridotta, inizializzando una richiesta con il builder PrepareIntegrityTokenRequest e impostando il Cloud Project Number.
Nel mio codice Kotlin:
import com.google.android.play.core.integrity.IntegrityManagerFactory
import com.google.android.play.core.integrity.StandardIntegrityManager
import kotlinx.coroutines.tasks.await
class IntegrityClient(
private val context: Context,
private val cloudProjectNumber: Long
) {
private val integrityManager = IntegrityManagerFactory.createStandard(context)
private var tokenProvider: StandardIntegrityManager.StandardIntegrityTokenProvider? = null
suspend fun warmUp() {
try {
val request = StandardIntegrityManager.PrepareIntegrityTokenRequest
.builder()
.setCloudProjectNumber(cloudProjectNumber)
.build()
tokenProvider = integrityManager.prepareIntegrityToken(request).await()
} catch (e: Exception) {
Log.e("IntegrityClient", "Warm-up failed", e)
}
}
suspend fun requestAttestation(requestHash: String): String? {
if (tokenProvider == null) warmUp()
return try {
val request = StandardIntegrityManager.StandardIntegrityTokenRequest
.builder()
.setRequestHash(requestHash)
.build()
val response = tokenProvider?.request(request)?.await()
response?.token()
} catch (e: Exception) {
Log.e("IntegrityClient", "Token request failed", e)
null
}
}
}
// Utilizzo nel vostro Activity/Fragment:
val integrityClient = IntegrityClient(context, CLOUD_PROJECT_NUMBER)
integrityClient.warmUp()
val requestHash = generateRequestHash(sensitiveAction)
val token = integrityClient.requestAttestation(requestHash)
if (token != null) {
sendToBackendForVerification(token)
}
Backend Verification (Server-Side)
Il token deve essere verificato nel vostro backend. Nel mio setup uso Node.js con la libreria ufficiale di Google:
import { IntegrityTokenResponse } from '@google-play/integrity';
const { google } = require('googleapis');
const integrityService = google.integritytoken({
version: 'v1',
auth: new google.auth.GoogleAuth({
keyFile: '/path/to/service-account-key.json',
scopes: ['https://www.googleapis.com/auth/playintegrity']
})
});
async function verifyIntegrityToken(encodedToken: string, expectedRequestHash: string) {
try {
const response = await integrityService.decodeIntegrityToken({
integrityToken: encodedToken
});
const verdict = response.data;
// Verificate i campi cruciali
if (verdict.requestDetails.requestHash !== expectedRequestHash) {
console.error('Request hash mismatch');
return false;
}
// Controllate il device integrity
const deviceIntegrity = verdict.deviceIntegrity.deviceRecognitionVerdict;
if (deviceIntegrity !== 'RECOGNIZED') {
console.warn(`Device integrity: ${deviceIntegrity}`);
// Decidete se rifiutare in base alla vostra policy
}
// Verificate l'app integrity
const appIntegrity = verdict.appIntegrity;
if (appIntegrity.appRecognitionVerdict !== 'RECOGNIZED') {
console.error('App not recognized');
return false;
}
// Controllate la freschezza della richiesta
const tokenTimestamp = new Date(verdict.requestDetails.requestPackageRelease.requestTime);
const now = new Date();
const ageMs = now.getTime() - tokenTimestamp.getTime();
if (ageMs > 600000) { // 10 minuti
console.error('Token too old');
return false;
}
return true;
} catch (error) {
console.error('Token verification failed:', error);
return false;
}
}
Continuous Attestation Policy
Nel mio flusso di lavoro, ho implementato un controllo di attestazione ricorrente che si esegue ogni volta che l'app accede a dati sensibili. Non è il più performante, ma garantisce che un dispositivo compromesso dopo il primo accesso venga rilevato entro minuti.
// Nel vostro ViewModel o Repository:
val attestationInterval = 5 * 60 * 1000 // Ogni 5 minuti
LaunchEffect(Unit) {
while (isActive) {
val requestHash = computeContextHash("sensitive_action")
val token = integrityClient.requestAttestation(requestHash)
if (token != null) {
val isValid = backendClient.verifyToken(token)
if (!isValid) {
// Blocca l'accesso, logout, eventualmente remote wipe
sessionManager.terminateSession()
}
}
delay(attestationInterval.toLong())
}
}
Policy MDM Consolidate
Ecco il set di policy che ho applicato nella mia implementazione end-to-end in Intune:
Policy Name
Setting
Value
Work Profile Encryption
Encryption required
Require
Cross-Profile Copy/Paste
Allow copy/paste between profiles
Block
Notifications
Show work notifications on lock screen
Hide notification content
VPN
Always-on VPN
Enabled (work profile only)
Screenshot
Allow screenshots in work profile
Block
Backup
Allow backup to cloud
Block for work profile
USB Debugging
USB debugging
Block
Monitoraggio e Incident Response
Nel mio ambiente, ho configurato un endpoint in Intune che monitora i certificati di attestazione falliti. Se la console MDM implementa l'API SafetyNet attestation, controlla lo stato di salute del dispositivo e se un device è trovato compromesso verranno intraprese azioni necessarie per ulteriore protezione, e un device manipolato sarà contrassegnato come non conforme dall'MDM.
Nel mio playbook di incident response:
- Se l'attestazione fallisce 3 volte consecutive, il device viene marcato "Non Conforme"
- L'accesso alle app di lavoro è bloccato (Conditional Access via Azure AD)
- Viene inviata una notifica all'IT team e all'utente
- Se dopo 48 ore l'attestazione non passa, il Work Profile viene rimosso remotamente
Problemi affrontati e soluzioni
Problema: Play Integrity API timeout su reti lente
All'inizio, i device in connessioni 3G avevano timeout frequenti. Ho aggiunto retry con backoff esponenziale e timeout fallback per la biometria locale:
suspend fun requestWithRetry(
requestHash: String,
maxRetries: Int = 3
): String? {
var attempt = 0
var delay = 1000L
while (attempt < maxRetries) {
return try {
withTimeoutOrNull(8000L) { // 8 secondi timeout
tokenProvider?.request(
StandardIntegrityManager.StandardIntegrityTokenRequest
.builder()
.setRequestHash(requestHash)
.build()
).await()?.token()
}
} catch (e: Exception) {
attempt++
delay(delay)
delay *= 2
null
}
}
return null // Fallback a local biometric check
}
Problema: EncryptedSharedPreferences non sincronizza tra istanze
Se l'app ha più processi, le SharedPreferences potrebbero non sincronizzarsi. Ho usato di ROOM con crittografia al posto:
@Entity(tableName = "work_secrets")
data class WorkSecret(
@PrimaryKey val id: String,
val encryptedValue: String, // Crittografato da SQLCipher
val timestamp: Long
)
// Database con crittografia
val database = Room.databaseBuilder(
context,
WorkDatabase::class.java,
"work-db.db"
)
.openHelperFactory(SupportSQLiteOpenHelper.Configuration.builder(context)
.name("work-db.db")
.callback(object : SupportSQLiteOpenHelper.Callback(1) {}
)
.build())
.build()
FAQ
Il Work Profile è sufficiente per bloccare completamente l'accesso ai dati personali da parte delle app di lavoro?
No, il Work Profile è isolamento a livello kernel, ma richiede configurazione policy corretta. Isolando solo gli strumenti di lavoro e i dati si riduce il rischio, e il patching su larga scala, la visibilità continua e i cambiamenti di configurazione proattivi aiutano a raggiungere la conformità normativa totale. Dovete disabilitare esplicitamente copy/paste, broadcast filtering e backup cloud.
Quale MDM è il migliore per implementare questa procedura?
Android Enterprise fornisce Work Profile, configurazioni gestite e zero-touch enrollment, ed è quasi universale per i deployment enterprise di Android negli Stati Uniti. Intune, Workspace ONE, MobileIron e Hexnode supportano tutti i controlli descritti. La scelta dipende dall'ecosistema IT esistente (Microsoft 365, VMware, etc.).
Come gestisco le chiavi se ho mille device distribuiti globalmente?
Non usate key management decentralizzato. Implementate un Key Management Service centralizzato: Azure Key Vault (se Intune), AWS KMS (se AWS), oppure un server interno con accesso via VPN. Le chiavi rimangono sul device solo in memoria; il key derivation avviene sempre dal KMS. Rotate le chiavi master ogni 6 mesi.
Posso usare questa procedura su BYOD o solo su COPE?
Evitate di affidarvi ad app di isolamento installate dall'utente – mancano dell'enforcement a livello kernel e non possono prevenire perdite negli appunti, sfondamento delle notifiche o interferenza dei servizi in background. Su BYOD, il Work Profile nativo è l'unica opzione validata. Non usate containerizzazione third-party.
Se implemento Play Integrity API, mi espongo a dipendenza da Google?
Sì. Se Google Play Services è disabilitato o offline, l'attestazione fallisce. Nel mio setup, ho un fallback a biometrica locale + local hash check. Inoltre, Google raccomanda ai prodotti che si affidano a SafetyNet Attestation API di pianificare workaround di emergenza nel caso in cui l'API fallisca temporaneamente. Lo stesso vale per Play Integrity.
Conclusione
Implementare un vero hardening del Work Profile di Android Enterprise non è triviale, ma è essenziale per qualsiasi organizzazione che gestisce dati sensibili. Ho strutturato la procedura in tre pilastri: isolamento a livello file system, gestione rigorosa delle chiavi e validazione continua tramite attestazione.
Le fughe cross-profile non sono un rischio teorico – le ho viste in ambienti reali dove policy incomplete permettevano agli Intent broadcast di sfuggire. Disabilitate la condivisione negli appunti, filtrate gli Intent, usate EncryptedSharedPreferences per ogni dato sensibile e integrate Play Integrity API per rilevare dispositivi compromessi.
Se implementate questa procedura nel vostro ambiente, iniziate con i vostri utenti pilota, monitorate i log di attestazione per la prima settimana e regolate i timeout e le policy in base ai vostri pattern di rete. Lasciate un commento qui sotto con le vostre domande o i vostri scenari specifici – sono sempre interessato a sentire come affrontate questi problemi in produzione.