Comment sécuriser une application universelle Windows ?

Que ce soit lors du développement d'une application métier ou d'une application grand public, il est parfois nécessaire de protéger les données.

Le modèle d'application Windows universelle (UWP) permet un premier niveau de protection car les applications téléchargées depuis le Windows Store sont stockées et exécutées au sein d'une sandbox (bac à sable), c'est-à-dire dans un conteneur inaccessible (en théorie) depuis une autre application. Cependant, il est parfois nécessaire d'ajouter des couches de protection supplémentaires afin de garantir la sécurité la plus optimale possible notamment au niveau stockage des données mais aussi au niveau échange des données avec les serveurs.

Pour cela, il est recommandé d'utiliser respectivement la Data Protection API & la vérification de la chaîne de confiance des certificats. Nous allons voir ces points en détail.

Data Protection API

L'API de protection des données ou DPAPI permet de protéger les données via la classe : DataProtection (Windows.Security.Cryptography.DataProtection). Ce mécanisme de protection natif permettra de sécuriser les données qui sont stockées sur le téléphone : par exemple un champ dans une base de données, toute la base de données ou même un flux JSON mis en cache.

Voici le code qui permet de chiffrer une chaîne:

public async Task<string> EncryptString(string message)
{
    if (string.IsNullOrEmpty(message))
    {
      return message;
    }

    DataProtectionProvider dataProtectionProvider = new DataProtectionProvider(ProtectionDescriptor);
    IBuffer messageBuffer = CryptographicBuffer.ConvertStringToBinary(message, BinaryStringEncoding.Utf8);
    IBuffer protectedBuffer = await dataProtectionProvider.ProtectAsync(messageBuffer);
    return Convert.ToBase64String(protectedBuffer.ToArray(0, (int)protectedBuffer.Length));
}

Si nous considérons ce fichier JSON :

{"myObject":{"property":"value"}}

Nous aurons en sortie :

MIIB8wYJKoZIhvcNAQcDoIIB5DCCAeACAQIxggF6ooIBdgIBBDCCATgEggEGAQAAANCMnd8BFdERjHoAwE/Cl+sBAAAATn0+x/2Ma0+bjZcB89pN3QAAAAACAAAAAAAQZgAAAAEAACAAAAAgNCCUBS+koCValLW5srpKR+wA2DpxMzY2z/sUxSJ3pwAAAAAOgAAAAAIAACAAAADXt9zg+6BjSt+8/PvEP17XrkhFu8dhBQZKwGBbHLXHdDAAAACaygjoN1zEvuNoMa+uTDv7F7PDX7cWou7KQ4KDZXAiGrDA4WBbXOHMi7EFbGz8ycJAAAAAZk6S60rWc4z84ENTE0/AAoUWkwnAYQYIb7/kD7k3jh9MHwDRAtS19pyKRtIpmOiC/UBLfhWMwOF2b8sr2ZkiajAsBgkrBgEEAYI3SgEwHwYKKwYBBAGCN0oBCDARMA8wDQwFTE9DQUwMBHVzZXIwCwYJYIZIAWUDBAEtBCh9ccbnHWiFkE6bQmUMc07QJWw9U8CKmzcS/DPzRY2h4MV5pKMGXE3QMF0GCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMQGOz8mIBwsA9lWYSAgEQgDDHxsv1yYxc3gsAXP3UbykhMthYr+K007x6gitakG/H8os6uvvKvqZHcllaD/b0emQ=

Comme vous pouvez le voir notre JSON a bien été chiffré, et il pourra seulement être décrypté par l'application.

Lorsque nous utilisons DPAPI, il est nécessaire de préciser le scope. Sur un Windows Phone le scope sera "LOCAL=user", c'est à dire que la protection sera faite par utilisateur. Sur tablette, le scope peut être "LOCAL=machine", c'est à dire que le chiffrement sera fait cette fois-ci au niveau de la machine, et que potentiellement n'importe quel utilisateur pourra déchiffrer les données.

Pour déchiffrer, il suffira d'appeler la méthode UnProtectAsync de la DPAPI :

public async Task<string> DecryptString(string encryptedMessage)
{
    if (string.IsNullOrEmpty(encryptedMessage))
    {
       return encryptedMessage;
    }

    DataProtectionProvider dataProtectionProvider = new DataProtectionProvider(ProtectionDescriptor);
    IBuffer messageBuffer = Convert.FromBase64String(encryptedMessage).AsBuffer();
    IBuffer unprotectedBuffer = await (dataProtectionProvider.UnprotectAsync(messageBuffer));
    return CryptographicBuffer.ConvertBinaryToString(BinaryStringEncoding.Utf8, unprotectedBuffer);
}

Toutefois le stockage pouvant se faire sur carte SD, les données sont potentiellement exposées et cela peut-être considéré comme une faille de sécurité

Les Responsables la Sécurité (RSSI) peuvent donc ne pas recommander l'usage de DPAPI. Il faudra donc renforcer ou remplacer le chiffrement DPAPI avec un autre système de chiffrement à clef.

L'utilisation d'une clé est elle aussi problématique car nécessite de stocker cette même clef. Elle est en général codée en dur dans les sources de l'application et le développeur tente l'obfuscation (complexifier le code pour ralentir la retro-ingénierie). L'autre possibilité est l'attribution d'une clef de chiffrement depuis un serveur mais cela déplace le problème au niveau du stockage des données et peut poser des soucis aux applications offline.

Il serait intéressant d'avoir un stockage inviolable (sécurisé, isolé) de cette clef. C'est là qu'intervient la Password Vault.

La Password Vault

La Password Vault est un conteneur sécurisé, qui est habituellement utilisé pour sauvegarder les identifiants d'un utilisateur (nom d'utilisateur, mot de passe). Il repose lui aussi sur DPAPI à la différence que le conteneur est sandboxé par application. Ainsi aucune application ne peut accéder aux informations d'une autre.

Pour notre solution de chiffrement maison, nous allons détourner l'utilisation de la Password Vault, afin d'y stocker notre clef de chiffrement.

Le principe est donc dans un premier temps de créer notre Password Vault :

PasswordVault passwordVault = new PasswordVault();
passwordVault.Add(new PasswordCredential { UserName = "key", Resource = "crypt", Password = Guid.NewGuid().ToString() });

Nous utilisons ici un Guid qui me permet générer une clef unique qui ne sera pas retrouvable par un tiers et un algorithme de chiffrement symétrique :

private IBuffer GetMD5Hash(string key)
{
   IBuffer buffUtf8Msg = CryptographicBuffer.ConvertStringToBinary(key, BinaryStringEncoding.Utf8);
   HashAlgorithmProvider hashAlgorithmProvider = HashAlgorithmProvider.OpenAlgorithm(HashAlgorithmNames.Md5);
   IBuffer buffHash = hashAlgorithmProvider.HashData(buffUtf8Msg);
   if (buffHash.Length != hashAlgorithmProvider.HashLength)
   {
     throw new Exception("There was an error creating the hash");
   }
   return buffHash;
}

public string Encrypt(string toEncrypt, string key)
{
   var keyHash = GetMD5Hash(key);

   var toDecryptBuffer = CryptographicBuffer.ConvertStringToBinary(toEncrypt, BinaryStringEncoding.Utf8);
   var symmetricKeyAlgorithm = SymmetricKeyAlgorithmProvider.OpenAlgorithm(SymmetricAlgorithmNames.AesEcbPkcs7);

   var symetricKey = symmetricKeyAlgorithm.CreateSymmetricKey(keyHash);
   var buffEncrypted = CryptographicEngine.Encrypt(symetricKey, toDecryptBuffer, null);

   var strEncrypted = CryptographicBuffer.EncodeToBase64String(buffEncrypted);

   return strEncrypted;
}

public string Decrypt(string toDecrypt, string key)
{
   var keyHash = GetMD5Hash(key);

   IBuffer toDecryptBuffer = CryptographicBuffer.DecodeFromBase64String(toDecrypt);

   SymmetricKeyAlgorithmProvider symmetricKeyAlgorithm = SymmetricKeyAlgorithmProvider.OpenAlgorithm(SymmetricAlgorithmNames.AesEcbPkcs7);
   var symetricKey = symmetricKeyAlgorithm.CreateSymmetricKey(keyHash);

   var buffDecrypted = CryptographicEngine.Decrypt(symetricKey, toDecryptBuffer, null);
   string strDecrypted = CryptographicBuffer.ConvertBinaryToString(BinaryStringEncoding.Utf8, buffDecrypted);

   return strDecrypted;
}

Maintenant, nous avons notre propre manière de chiffrer (grâce à notre clef), et nous pouvons soit l'utiliser à la place de DPAPI soit en supplément : nous parlons alors de Surchiffrement (Multiple Encryption)

Prévenir une attaque Man In The Middle (MIM)

Comme le décrit, Rémi, dans son brillant article (Develop a secured Android application) : il y a 2 manières d'opérer une attaque MIM :

  1. Le hacker arrive à se trouver sur le même réseau que l'utilisateur et intercepte tous les paquets et appels réseaux (grâce notamment à un proxy)
  2. Le hacker arrive à se faire passer pour le serveur qui répond à sa place aux appels WebService.

Pour éviter cela, il faut faire du Certificate Pinning & valider la Chain of Trust (chaîne de confiance).

Le Certificate Pinning consiste à vérifier la clé publique (ou le hash) des certificats chiffrant les données venant du serveur. Il est possible de ne vérifier que le certificat serveur (parfois Self-Signed) ou l'un des certificats intermédiaires (niveau organisation ou autorité de certification).

Microsoft met à disposition la classe Certificate (https://msdn.microsoft.com/en-us/library/windows.security.cryptography.certificates.certificate.aspx) pour nous aider à comparer les certificats serveur avec une version embarquée dans le code.

private async Task CheckCertificateValidity(HttpResponseMessage responseMessage)
{
   // Retrieve Intermediate certificate
   var receivedCertificates = responseMessage.RequestMessage.TransportInformation.ServerIntermediateCertificates;
   var receivedCertificateToCheck = receivedCertificates[0];

   // To retrieve Server certificate use the following line
   receivedCertificateToCheck = responseMessage.RequestMessage.TransportInformation.ServerCertificate
 
   // Compare certificate with the stored information
   var storedCertificate = new Certificate(CryptographicBuffer.ConvertStringToBinary(storedCertificateStr, BinaryStringEncoding.Utf8));
   if (!receivedCertificateToCheck.GetHashValue().SequenceEqual(storedCertificate.GetHashValue()))
   {
      throw new SecurityException("Unauthorized certificate");
   }

   // Validate Certificate subject for trusted url
   var receivedServerCertificate = responseMessage.RequestMessage.TransportInformation.ServerCertificate;
   if (!receivedServerCertificate.Subject.Equals(baseUri.DnsSafeHost)) 
   {
      throw new SecurityException("Invalid certificate"); 
   }
}

La vérification de la chaîne de confiance est facilitée par l'utilisation de la classe CertificateChain.

private async Task CheckCertificateChain(HttpResponseMessage responseMessage)
{
   // Validate Certificate Chain
   var chain = await responseMessage.RequestMessage.TransportInformation.ServerCertificate.BuildChainAsync(null);
   var validationResult = chain.Validate(new ChainValidationParameters()
   {
     CertificateChainPolicy = CertificateChainPolicy.Ssl,
     ServerDnsName = new HostName(responseMessage.RequestMessage.RequestUri.DnsSafeHost)
   });
   if (validationResult != ChainValidationResult.Success)
   {
      throw new SecurityException("Invalid chain of trust (" + validationResult.ToString() + ")");
   }
}

Vous pouvez vérifier que cela fonctionne en mettant un proxy (par exemple : Charles Proxy, WireShark, Fiddler, etc.) qui simulera une attaque MIM et provoquera une SecurityException dans votre application.

Clef stockée dans l'application: Obfuscation or not Obfuscation

Lorsque vous créez une application universelle (Windows 10), le binaire généré est un binaire natif et non plus un binaire ".NET". La décompilation vers du C# devient donc quasi impossible. Le code assembleur par centaine de pages est loin d'être lisible mais les chaînes de caractères restent généralement en clair.

Si vous devez stocker des clefs dans votre application, les pratiques d'obfuscation restent nécessaires pour un niveau élevé de sécurité (notamment le chiffrement des chaînes et le tampering detection…). Vous trouverez plus d'informations sur la nécessité de l'obfuscation dans cet article : what's it mean for obfuscation and dotfuscator in particular ?

Conclusion

Dans cet article, nous avons montré comment sécuriser une application Windows Universelle aussi bien au niveau du stockage des données qu'au niveau des échanges avec les Web Services. L'implémentation de ce dernier niveau n'est possible que si l'architecture serveur repose sur HTTPS et protégera l'application des attaques de type MIM.