Home Chi Sono
Servizi ▼
WordPress Sviluppo Web Server & Hosting Assistenza Tecnica Windows Android
Blog ▼
Tutti gli Articoli WordPress Hosting Plesk Assistenza Computer Windows Android A.I.
Contatti

Come Implementare Android Enterprise Work Profile Isolation Hardening: La Mia Procedura Cross-Profile Data Leak Prevention, Encrypted Container Keys e Attestation Service Integration

Come Implementare Android Enterprise Work Profile Isolation Hardening: La Mia Procedura Cross-Profile Data Leak Prevention, Encrypted Container Keys e Attestation Service Integration

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:

  1. Se l'attestazione fallisce 3 volte consecutive, il device viene marcato "Non Conforme"
  2. L'accesso alle app di lavoro è bloccato (Conditional Access via Azure AD)
  3. Viene inviata una notifica all'IT team e all'utente
  4. 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.

Share: