Runtime: Primitive générale de bas niveau pour les chiffrements (AES-GCM étant le premier)

Créé le 29 août 2017  ·  143Commentaires  ·  Source: dotnet/runtime

Raisonnement

Il existe un besoin général pour un certain nombre de chiffrements pour le chiffrement. Le mélange d'interfaces et de classes d'aujourd'hui est devenu un peu décousu. De plus, les chiffrements de style AEAD ne sont pas pris en charge car ils doivent pouvoir fournir des informations d'authentification supplémentaires. Les conceptions actuelles sont également sujettes à l'allocation et celles-ci sont difficiles à éviter en raison des tableaux de retour.

API proposée

Une classe de base abstraite à usage général qui sera implémentée par des classes concrètes. Cela permettra l'expansion et aussi en ayant une classe plutôt que des méthodes statiques, nous avons la possibilité de créer des méthodes d'extension ainsi que de maintenir l'état entre les appels. L'API doit permettre le recyclage de la classe pour permettre des allocations inférieures (n'ayant pas besoin d'une nouvelle instance à chaque fois, et pour attraper, par exemple, des clés non gérées). En raison de la nature souvent non gérée des ressources qui sont suivies, la classe doit implémenter IDisposable

public abstract class Cipher : IDisposable
{
    public virtual int TagSize { get; }
    public virtual int IVSize { get; }
    public virtual int BlockSize { get; }
    public virtual bool SupportsAssociatedData { get; }

    public abstract void Init(ReadOnlySpan<byte> key, ReadOnlySpan<byte> iv);
    public abstract void Init(ReadOnlySpan<byte> iv);
    public abstract int Update(ReadOnlySpan<byte> input, Span<byte> output);
    public abstract int Finish(ReadOnlySpan<byte> input, Span<byte> output);
    public abstract void AddAssociatedData(ReadOnlySpan<byte> associatedData);
    public abstract int GetTag(Span<byte> span);
    public abstract void SetTag(ReadOnlySpan<byte> tagSpan);
}

Exemple d'utilisation

(la source d'entrée/sortie est un flux basé sur une étendue mythique comme la source IO)

using (var cipher = new AesGcmCipher(bitsize: 256))
{
    cipher.Init(myKey, nonce);
    while (!inputSource.EOF)
    {
        var inputSpan = inputSource.ReadSpan(cipher.BlockSize);
        cipher.Update(inputSpan);
        outputSource.Write(inputSpan);
    }
    cipher.AddAssociatedData(extraInformation);
    cipher.Finish(finalBlockData);
    cipher.GetTag(tagData);
}

Comportement de l'API

  1. Si la balise get est appelée avant la fin, un [type d'exception ?] doit être généré et l'état interne doit être défini sur invalide
  2. Si la balise n'est pas valide à la fin pour le déchiffrement, cela devrait être une exception levée
  3. Une fois terminé est appelé, un appel à autre chose qu'une des méthodes Init lancera
  4. Une fois Init appelé, un deuxième appel sans "finir" lancera
  5. Si le type attend une clé fournie (une instance "new'd" droite) si l'appel initial "Init" n'a qu'un IV, il lancera
  6. Si le type a été généré, disons à partir d'une clé basée sur le magasin et que vous essayez de changer la clé via Init et pas seulement le IV, il lancera
  7. Si get tag n'est pas appelé avant dispose ou Init, une exception doit-elle être levée ? Pour empêcher l'utilisateur de ne pas récupérer le tag par accident ?

Référence dotnet/corefx#7023

Mises à jour

  1. Changé nonce en IV.
  2. Section de comportement ajoutée
  3. Suppression des cas d'étendue d'entrée/sortie unique de la finition et de la mise à jour, ils peuvent simplement être des méthodes d'extension
  4. Changement d'un certain nombre de plages en readonlyspan comme suggéré par @bartonjs
  5. Suppression de Reset, Init avec IV doit être utilisé à la place
api-suggestion area-System.Security

Commentaire le plus utile

@bartonjs , vous ignorez littéralement toutes les analyses techniques et logiques et utilisez à la place les dates des projets Go et libsodium comme indicateur faible de la véritable analyse ?

Non, j'utilise les conseils de cryptographes professionnels qui disent que c'est extrêmement dangereux et que nous devrions éviter de diffuser AEAD. Ensuite, j'utilise les informations de l'équipe CNG selon lesquelles "beaucoup de gens disent qu'ils le veulent en théorie, mais en pratique, presque personne ne le fait" (je ne sais pas dans quelle mesure il s'agit de télémétrie par rapport à l'anecdotique des demandes d'assistance sur le terrain). Le fait que d'autres bibliothèques aient opté pour la voie unique _renforce_ simplement la décision.

Pourquoi la demande démontrée jusqu'à présent sur GitHub est-elle insuffisante ?

Quelques scénarios ont été évoqués. Le traitement des tampons fragmentés pourrait probablement être résolu en acceptant ReadOnlySequence , s'il semble qu'il y ait suffisamment de scénario pour justifier de compliquer l'API au lieu de demander à l'appelant de réassembler les données.

Les fichiers volumineux sont un problème, mais les fichiers volumineux sont déjà un problème puisque GCM a une coupure à un peu moins de 64 Go, ce qui n'est "pas si gros" (d'accord, c'est assez gros, mais ce n'est pas le "whoa, c'est gros" qui c'était le cas). Les fichiers mappés en mémoire permettraient d'utiliser des étendues (jusqu'à 2 ^ 31-1) sans nécessiter 2 Go de RAM. Nous avons donc réduit de quelques bits le maximum... cela se produirait probablement avec le temps de toute façon.

Vous vous rendez compte que le choix d'une interface sans streaming pour AEAD exclut toutes les implémentations de ce type, n'est-ce pas ?

Je suis de plus en plus convaincu que @GrabYourPitchforks avait raison (https://github.com/dotnet/corefx/issues/23629#issuecomment-334638891) qu'il n'y a probablement pas d'interface unificatrice sensée. GCM _nécessitant_ un nonce/IV et SIV _interdisant_ cela signifie que l'initialisation d'un mode/algorithme AEAD nécessite déjà une connaissance de ce qui va se passer... il n'y a pas vraiment de notion "abstraite" pour AEAD. SIV dicte où va "la balise". GCM/CCM ne le font pas. SIV est tag-first, par spec.

SIV ne peut pas commencer à chiffrer tant qu'il n'a pas toutes les données. Ainsi, son cryptage en continu va soit lancer (ce qui signifie que vous devez savoir ne pas l'appeler) ou mettre en mémoire tampon (ce qui pourrait entraîner un temps de fonctionnement n ^ 2). CCM ne peut pas démarrer tant que la longueur n'est pas connue ; mais CNG n'autorise pas un indice de pré-chiffrement sur la longueur, donc c'est dans le même bateau.

Nous ne devrions pas concevoir un nouveau composant où il est plus facile de faire la mauvaise chose que la bonne chose par défaut. Le décryptage en continu rend très facile et tentant de câbler une classe Stream (à la manière de votre proposition de le faire avec CryptoStream), ce qui permet d'obtenir très facilement un bogue de validation des données avant que la balise ne soit vérifiée, ce qui annule presque entièrement le bénéfice de AE . ( IGcmDecryptor => CryptoStream => StreamReader => XmlReader => "attendez, ce n'est pas du XML légal..." => oracle de texte chiffré adaptatif) .

Ça va droit au but... la demande des clients.

Comme je l'ai malheureusement entendu bien trop de fois dans ma vie : je suis désolé, mais vous n'êtes pas le client que nous avons en tête. Je concéderai que vous savez peut-être comment faire GCM en toute sécurité. Vous savez qu'il ne faut diffuser que vers un fichier/tampon/etc volatile jusqu'à la vérification des balises. Vous savez ce que signifie la gestion nonce et vous connaissez les risques de se tromper. Vous savez qu'il faut faire attention aux tailles de flux et passer à un nouveau segment GCM après 2^36-64 octets. Vous savez qu'après que tout soit dit et fait, c'est votre bogue si vous vous trompez.

Le client que j'ai en tête, en revanche, est quelqu'un qui sait "je dois chiffrer ça" parce que son patron le lui a dit. Et ils savent que lorsqu'ils recherchent comment effectuer le cryptage, certains tutoriels disent "toujours utiliser AE" et mentionnent GCM. Ensuite, ils trouvent un tutoriel "chiffrement dans .NET" qui utilise CryptoStream. Ils connectent ensuite le pipeline, n'ayant aucune idée qu'ils viennent de faire la même chose qu'en choisissant SSLv2... coché une case en théorie, mais pas vraiment en pratique. Et quand ils le font, ce bogue appartient à tous ceux qui savaient mieux, mais que la mauvaise chose soit trop facile à faire.

Tous les 143 commentaires

Commentaires rapides sur l'API proposée (en essayant d'être utile) :

  • Span<T> n'est pas dans NetStandard2 .
  • "Nonce" est très spécifique à l'implémentation - c'est-à-dire. sent le GCM. Cependant, même les documents GCM (ex. NIST SP800-38D) l'appellent "IV", ce qui - dans le cas de GCM - se trouve être un nonce. Cependant, dans le cas d'autres implémentations AEAD, l'IV ne doit pas nécessairement être un nonce (par exemple, répéter IV sous CBC + HMAC n'est pas catastrophique).
  • Streaming AEAD devrait soit fonctionner de manière transparente avec CryptoStream , soit fournir son propre "AEADCryptoStream" qui est tout aussi facile à diffuser en entrée/sortie que CryptoStream.
  • Les implémentations de l'API AEAD doivent être autorisées à effectuer une dérivation de clé interne basée sur AAD (Associated Data). L'utilisation de l'AAD uniquement pour le calcul/la vérification des balises est trop restrictive et empêche un modèle AEAD plus fort.
  • Les méthodes "Get*" doivent retourner quelque chose (GetTag). S'ils sont vides, ils doivent définir quelque chose/changer d'état.
  • Essayer d'obtenir une balise avant de "terminer" est probablement une mauvaise idée, donc "IsFinished" pourrait être utile.
  • Les personnes qui ont conçu ICryptoTransform ont pensé à la réutilisation, à la prise en charge de plusieurs blocs et à des tailles de blocs d'entrée/sortie de tailles différentes. Ces préoccupations ne sont pas prises en compte.

Comme preuve de la validité de l'API AEAD, la première implémentation d'une telle API proposée ne devrait pas être AES-GCM, mais l'AES-CBC classique/par défaut avec une balise HMAC. La simple raison en est que n'importe qui peut créer aujourd'hui une implémentation AES-CBC + HMAC AEAD, avec des classes .NET simples, existantes et bien connues. Faisons d'abord travailler un ancien [AES-CBC + HMAC] ennuyeux sur les nouvelles API AEAD, car il est facile pour tout le monde de MVP et de tester.

Le problème de nommage nonce / IV était quelque chose pour lequel j'étais indécis, heureux d'un changement de IV, donc ça va changer.

Quant aux méthodes Get renvoyant quelque chose, cela évite toute allocation. Il pourrait y avoir une surcharge Get() qui renvoie quelque chose. Peut-être que cela nécessite un changement de nom, mais je suis assez marié à l'idée que l'ensemble de l'API doit être essentiellement sans allocation.

En ce qui concerne les flux, etc., cela ne me dérange pas trop car ce sont des API de niveau supérieur qui peuvent facilement être construites à partir des primitives de niveau inférieur.

L'obtention d'une balise avant d'avoir terminé ne devrait pas être autorisée, mais vous devez savoir quand vous avez appelé finish, donc je ne suis pas sûr que cela devrait être dans l'API, mais cela devrait être un comportement défini, j'ai donc mis à jour la conception de l'API pour inclure une section de comportement afin que nous puissions capturer tous les autres qui sont pensés.

En ce qui concerne le chiffrement, je ne pense pas qu'un chiffrement spécifique devrait être la seule cible, afin de prouver qu'une nouvelle API à usage général doit correspondre à un nombre. AES GCM et CBC devraient tous deux être couverts.

(tous les commentaires sur le sujet, bons ou mauvais, sont toujours utiles !)

  • Classe ou interface ?
  • Comment, le cas échéant, les classes SymmetricAlgorithm actuelles interagissent-elles avec cela ?
  • Comment cela serait-il utilisé pour les clés persistantes, comme TripleDESCng et AesCng peuvent le faire ?
  • Beaucoup de ces durées semblent pouvoir être ReadOnlySpan.

@Drawaes merci d'avoir lancé le bal sur cette API. Quelques réflexions :

  1. La génération et la vérification des balises sont une partie très importante de cette API, car une mauvaise utilisation des balises peut aller à l'encontre de l'objectif. Si possible, j'aimerais voir des balises intégrées dans les opérations d'initialisation et de finition pour s'assurer qu'elles ne peuvent pas être ignorées accidentellement. Cela implique probablement que chiffrer et déchiffrer ne doivent pas utiliser les mêmes méthodes d'initialisation et de finalisation.
  2. J'ai des sentiments mitigés quant à la sortie de blocs pendant le décryptage avant d'arriver à la fin, car les données ne sont pas fiables tant que la balise n'a pas été vérifiée (ce qui ne peut être fait tant que toutes les données n'ont pas été traitées). Nous devrons évaluer ce compromis très soigneusement.
  3. La réinitialisation est-elle nécessaire ? Devrait terminer juste réinitialiser? Nous le faisons sur des hachages incrémentiels (mais ils n'ont pas besoin de nouveaux IV)

@bartonjs

  1. La classe, comme on l'a souvent vu dans la BCL avec une interface qu'on ne peut pas étendre par la suite sans tout casser. Une interface est comme un chiot pour la vie... à moins que les méthodes d'interface par défaut puissent être considérées comme une solution à ce problème.
    De plus, une classe scellée à partir d'un type abstrait est en fait plus rapide (à partir de maintenant) car le jitt peut dévirtualiser les méthodes maintenant ... Donc c'est fondamentalement gratuit. la répartition de l'interface n'est pas aussi bonne (toujours bonne mais pas aussi bonne)
  2. Je ne sais pas comment voudriez-vous que cela fonctionne ? Je m'intéresse peu aux choses actuelles car c'est tellement déroutant que je corrigerais directement tous les algos modernes raisonnables (laissez 3DES dans les autres classes :) Mais je n'ai pas toutes les réponses, alors avez-vous d'autres réflexions à ce sujet?
  3. Les clés persistantes devraient être faciles. Créez une méthode d'extension sur la méthode de clé ou le magasin de persistance.
MyKeyStore.GetCipher();

Il n'est pas initialisé. Il est jetable afin que toutes les références puissent être supprimées par un modèle jetable normal. S'ils essaient de définir la clé, lancez une exception d'opération non valide.

Oui aux étendues en lecture seule que je réglerai lorsque je ne suis pas sur le tube de mon téléphone.

@morganbr pas de problème... Je veux juste que ça se produise plus que tout ;)

  1. Pouvez-vous donner un extrait de code sur la façon dont vous voyez cela fonctionner? Pas sûr que ce soit le cas, mais le code apporte toujours de la clarté
  2. C'est dommage mais il faut vraiment cracher les blocs tôt. Avec hmac et le hachage, vous n'en avez pas, mais vous n'avez pas de données intermédiaires, juste l'état. Donc, dans ce cas, vous devrez mettre en mémoire tampon une quantité inconnue de données. Examinons l'exemple des pipelines et TLS. Nous pouvons écrire 16 ko de texte en clair, mais les tampons du pipeline ont aujourd'hui la taille d'une page de 4 ko. Nous voudrions donc au mieux chiffrer/déchiffrer 4*4k. Si vous ne me donnez pas la réponse jusqu'à la fin, vous devez allouer de la mémoire interne pour stocker tout cela, puis je suppose que je la jette quand j'obtiens le résultat ? Ou allez-vous l'essuyer. Et si je déchiffrais 10 Mo et que vous conserviez cette mémoire après que je doive maintenant m'inquiéter de l'utilisation de la mémoire latente.
  3. Pas à 100% sur le truc init/reset (pas vos idées, ma forme actuelle d'API) ça ne me va pas bien donc je suis ouvert à une nouvelle suggestion !

J'ai peu d'intérêt pour les choses actuelles car c'est tellement déroutant que je corrigerais directement tous les algos modernes raisonnables (laissez 3DES dans les autres classes :)

Le problème serait que les formats de conteneur comme EnvelopedCms (ou EncryptedXml) peuvent avoir besoin de fonctionner avec 3DES-CBC, AES-CBC, etc. -256 n'aurait probablement pas l'impression de faire des choses anciennes et cruelles.

Si ce n'est censé être que pour AE, cela devrait être reflété quelque part dans le nom. En ce moment, c'est un "chiffrement" générique (que j'allais/vais, à un moment donné, m'asseoir avec un dictionnaire/glossaire et découvrir s'il y a un mot pour "un algorithme de chiffrement dans un mode de fonctionnement", puisque je pense que "cipher" == "algorithme", donc "Aes").

:) Je soulignais simplement que ce n'est pas mon sujet ou que cela m'intéresse beaucoup, donc je suis prêt à m'en remettre à vous et à la communauté sur ce sujet, je n'ai pas réfléchi aux implications de cela.


Après les avoir parcourus rapidement, une option est de leur permettre de prendre une instance du "Cipher" ou de ce qu'on appelle la classe. Cela pourrait ne pas être fait lors de la première vague, mais pourrait rapidement être suivi. Si l'API est super efficace, je ne vois aucune raison pour qu'ils fassent leur propre chose en interne et c'est exactement le cas d'utilisation de cette API.

En tant que barre latérale sur la dénomination ... Je dois admettre que c'est difficile cependant
Openssl = chiffre
Rubis = chiffre
Go = package de chiffrement avec des interfaces de type canard pour AEAD, etc.
Java = chiffre

Maintenant, je suis pour être différent mais... il y a une tendance. Si quelque chose de mieux est possible, c'est cool.

Peut-être "BlockModeCipher" ... ?

J'ai fait quelques changements, je changerai de nom si un meilleur nom est décidé.

Lorsque j'ai commencé à essayer de répondre aux questions, j'ai réalisé que l'API manquait déjà de différenciation de cryptage/décryptage, donc dans votre exemple, elle ne sait pas s'il faut crypter ou décrypter les données. Obtenir cela pourrait ajouter de la clarté.

Je pourrais imaginer plusieurs façons dont l'API pourrait appliquer l'utilisation appropriée des balises (en supposant qu'il s'agit d'une API AEAD, pas seulement d'un cryptage symétrique puisque nous avons déjà SymmetricAlgorithm/ICryptoTransform/CryptoStream). Ne les considérez pas comme prescriptifs, juste comme un exemple d'application du marquage.
Par méthode :

class Cipher
{
   void InitializeEncryption(ReadOnlySpan<byte> key, ReadOnlySpan<byte> iv);
   // Ensures decryptors get a tag
   void InitializeDecryption(ReadOnlySpan<byte> key, ReadOnlySpan<byte> iv, ReadOnlySpan<byte> tag);
   // Ensure encryptors produce a tag
    void FinishEncryption(ReadOnlySpan<byte> input, Span<byte> output, Span<byte> tag);
   // Throws if tag didn't verify
   void FinishDecryption(ReadOnlySpan<byte> input, Span<byte> output);
   // Update and properties are unchanged, but GetTag and SetTag are gone
}

Par classe :

class Cipher
{
    // Has properties and update, but Initialize and Finish aren't present
}
class Encryptor : Cipher
{
    void Initialize(ReadOnlySpan<byte> key, ReadOnlySpan<byte> iv);
    void Finish(ReadOnlySpan<byte> input, Span<byte> output, Span<byte> tag);
}
class Decryptor : Cipher
{
   void Initialize(ReadOnlySpan<byte> key, ReadOnlySpan<byte> iv, ReadOnlySpan<byte> tag);
   // Throws if tag didn't verify
   void Finish(ReadOnlySpan<byte> input, Span<byte> output);
}
class AesGCMEncryptor : Encryptor {}
class AesGCMDecryptor : Decryptor {}
}

Cela dit, s'il ne se met pas en mémoire tampon lors du décryptage, est-il pratique de s'assurer que le décryptage est réellement terminé et que la balise est vérifiée ? La mise à jour peut-elle en quelque sorte comprendre qu'il est temps de vérifier ? Est-ce quelque chose que Dispose devrait faire ? (La disposition est un peu dangereuse car vous avez peut-être déjà fait confiance aux données au moment où vous disposez de l'objet)

En ce qui concerne la dénomination, notre précédent est SymmetricAlgorithm et AsymmetricAlgorithm. Si cela est destiné à AEAD, certaines idées pourraient être AuthenticatedSymmetricAlgorithm ou AuthenticatedEncryptionAlgorithm.

Quelques réflexions et idées d'API :

public interface IAEADConfig
{
    // size of the input block (plaintext)
    int BlockSize { get; }

    // size of the output per input-block;
    // typically a multiple of BlockSize or equal to BlockSize.
    int FeedbackSize { get; }

    // IV size; CAESAR completition uses a fixed-length IV
    int IVSize { get; }

    // CAESAR competition uses a fixed-length key
    int KeySize { get; }

    // CAESAR competition states that typical AEAD ciphers have a constant gap between plaintext length
    // and ciphertext length, but the requirement is to have a constant *limit* on the gap.
    int MaxTagSize { get; }

    // allows for AE-only algorithms
    bool IsAdditionalDataSupported { get; }
}

public interface ICryptoAEADTransform : ICryptoTransform
{
    // new AEAD-specific ICryptoTransform interface will allow CryptoStream implementation
    // to distinguish AEAD transforms.
    // AEAD decryptor transforms should throw on auth failure, but current CryptoStream
    // logic swallows exceptions.
    // Alternatively, we can create a new AEAD_Auth_Failed exception class, and
    // CryptoTransform is modified to catch that specific exception.
}

public interface IAEADAlgorithm : IDisposable, IAEADConfig
{
    // separates object creation from initialization/keying; allows for unkeyed factories
    void Initialize(ArraySegment<byte> key);

    void Encrypt(
        ArraySegment<byte> iv, // readonly; covered by authentication
        ArraySegment<byte> plaintext, // readonly; covered by authentication
        ref ArraySegment<byte> ciphertext, // must be of at least [plaintext_length + MaxTagSize] length. iv is not part of ciphertext.
        ArraySegment<byte> additionalData = default(ArraySegment<byte>) // readonly; optional; covered by authentication
        ); // no failures expected under normal operation - abnormal failures will throw

    bool Decrypt(
        ArraySegment<byte> iv, // readonly
        ArraySegment<byte> ciphertext, // readonly
        ref ArraySegment<byte> plaintext, // must be of at least [ciphertext_length - MaxTagSize] length.
        ArraySegment<byte> additionalData = default(ArraySegment<byte>), // readonly; optional
        bool isAuthenticateOnly = false // supports Authentication-only mode
        );// auth failures expected under normal operation - return false on auth failure; throw on abnormal failure; true on success

    /*  Notes:
        * Array.LongLength should be used instead of Array.Length to accomodate byte arrays longer than 2^32.
        * Ciphertext/Plaintext produced by Encrypt()/Decrypt() must be determined *only* by method inputs (combined with a key).
          - (ie. if randomness or other hidden inputs are needed, they must be a part of iv)
        * Encrypt()/Decrypt() are allowed to write to ciphertext/plaintext segments under all conditions (failure/success/abnormal)
          - some implementations might be more stringent than others, and ex. not leak decrypted plaintext on auth failures
    */

    ICryptoAEADTransform CreateEncryptor(
        ArraySegment<byte> key,
        ArraySegment<byte> iv,
        ArraySegment<byte> additionalData = default(ArraySegment<byte>)
        );

    ICryptoAEADTransform CreateDecryptor(
        ArraySegment<byte> key,
        ArraySegment<byte> iv,
        ArraySegment<byte> additionalData = default(ArraySegment<byte>)
        );

    // Streaming AEAD can be done with good-old CryptoStream
    // (possibly modified to be AEAD-aware).
}

@sdrapkin , merci pour vos réflexions. Où doivent aller les balises dans votre API ? Font-ils implicitement partie du texte chiffré ? Si tel est le cas, cela implique un protocole que certains algorithmes n'ont pas réellement. J'aimerais comprendre les protocoles dont les gens pourraient se soucier pour voir si le placement implicite des balises est acceptable ou s'il doit être transporté séparément.

@morganbr J'ai également remarqué le problème de cryptage/décryptage, mais je n'avais pas eu le temps ce soir de le résoudre, si heureux pour votre conception. Je préfère les méthodes à la classe car elles permettent un meilleur recyclage agressif (les tampons pour les clés et les IV peuvent s'additionner).

Comme pour un contrôle avant la disposition. Il n'est malheureusement pas possible de dire la fin d'une opération.

Les interfaces @sdrapkin , je dirais, sont interdites en raison du problème de version mentionné précédemment. À moins que nous ne comptions sur l'implantation par défaut à l'avenir. De plus, la répartition de l'interface est plus lente. Les segments de tableau sont également les nôtres, car span est la primitive inférieure la plus polyvalente. Cependant, des méthodes d'extension pourraient être ajoutées pour que le segment de tableau s'étende s'il y avait une demande ultérieure.

Certaines de vos propriétés sont intéressantes, elles seront donc mises à jour lorsque je serai sur un ordinateur plutôt que sur mon téléphone.

Bonne rétroaction tout au long!

@morganbr Les balises font partie du texte chiffré. Ceci est calqué sur l' API CAESAR (qui inclut AES-GCM).

@Drawaes J'ai utilisé des interfaces pour illustrer uniquement des pensées - je suis parfaitement d'accord avec les méthodes/classes statiques. Enverguren'existe pas. Je me fiche de ce qui peut arriver ou non - ce n'est pas dans NetStandard2, et ce n'est pas dans le .NET normal que les projets sérieux utilisent réellement (ouais, ouais, je sais que c'est dans Core, mais Core est un jouet pour le moment). Je serais heureux de considérer personnellement Spanquand je le vois - jusque-là ArraySegmentest l'API NetStandard la plus proche actuellement livrée.

Je vais approfondir l'API CAESAR qui est utile.

Quant à Span, il est livré autour de la période 2.1, je crois, ce qui est, espérons-le, le même moment où la première implémentation de cette API serait livrée (ou du moins le plus tôt possible).

Si vous jetez un coup d'œil au package nuget actuel de la version préliminaire, il prend en charge jusqu'à la norme .net 1.0 et il n'est pas prévu de modifier cela lors de la sortie.

Peut-être que @stephentoub peut le confirmer alors qu'il travaille pour ajouter des API basées sur Span dans le cadre en ce moment même.

(Nuget pour Span)[https://www.nuget.org/packages/System.Memory/4.4.0-preview2-25405-01]

Je dirais donc que c'est le seul vrai choix pour une toute nouvelle API. Ensuite, des méthodes d'extension, etc. peuvent être ajoutées pour prendre un ArraySegment si vous le souhaitez et s'il est suffisamment utile, il peut être ajouté au framework, mais il est trivial de transformer un ArraySegment en Span, mais l'autre méthode nécessite de copier des données.

Le problème que je vois avec cette API ci-dessus est que ce sera un désastre pour les performances sur toutes les données "fragmentées". Par exemple, le trafic réseau, si un seul bloc authentifié est divisé en plusieurs lectures à partir d'un flux existant, je dois tout mettre en mémoire tampon dans un seul [insérer la structure de données] et chiffrer/déchiffrer tout à la fois. Les défaites toutes les tentatives de faire n'importe quel type de copie zéro sur ces données.

Les frameworks de mise en réseau tels que ceux fournis par Pipelines parviennent à éviter presque toutes les copies, mais s'ils rencontrent un type de crypto dans cette API, tout cela disparaît.

L'objet de configuration séparé (ou sac) a en fait été impliqué dans une discussion récente sur une autre API que j'ai eue. Je ne m'y oppose pas en principe car s'il grandit à l'avenir, cela peut devenir un gâchis d'avoir un grand nombre de propriétés sur l'objet principal.

Deux ou trois choses me sont venues à l'esprit.

  • La proposition actuelle appelle TagSize une valeur de sortie (enfin, une propriété get-only). Mais pour GCM et CCM, il s'agit d'une entrée pour le chiffrement (et déductible du déchiffrement puisque vous avez fourni la balise réelle).
  • La proposition suppose que l'entrée et la sortie peuvent se produire en même temps, et au coup par coup.

    • IIRC CCM ne peut pas effectuer de cryptage en continu (la longueur du texte en clair est une entrée dans la première étape de l'algorithme).

    • Les modes rembourrés retardent le décryptage par (au moins un) bloc, car jusqu'à ce que Final soit appelé, ils ne savent pas si plus de données arrivent / si le bloc actuel a besoin que le rembourrage soit supprimé

  • Certains algorithmes peuvent considérer que l'élément AD est requis au début de l'opération, ce qui le rend plus proche d'un paramètre Init/ctor que d'une association à liaison tardive.

Je ne sais pas si les formats de conteneur (EnvelopedCms, EncryptedXml) auraient besoin d'extraire la clé, ou s'il leur appartiendrait simplement de la générer et de s'en souvenir (aussi longtemps qu'ils en auraient besoin pour l'écrire).

(Apparemment, je n'ai pas appuyé sur le bouton "commentaire" hier, donc il ne reconnaîtra rien après "j'ai apporté quelques modifications" à 1910Z)

La vraie taille de balise devrait être variable. D'accord.

Si nous regardons juste le cryptage pour l'instant, pour simplifier le cas d'utilisation. Vous avez raison de dire que certains chiffrements ne renverront rien, moins ou plus. Il y a une question générale sur ce qui se passe si vous ne fournissez pas un tampon suffisamment grand.

Sur les nouvelles interfaces TextEncoding utilisant span, il y avait une suggestion d'avoir le type de retour un enum pour définir s'il y avait suffisamment d'espace pour sortir ou non, et la taille réellement écrite dans un paramètre "out" à la place. C'est une possibilité.

Dans le cas de CCM, je dirais simplement qu'il ne renvoie rien et qu'il devra mettre en mémoire tampon interne jusqu'à ce que vous appeliez finish, auquel cas il voudrait vider tout le lot. Rien ne vous empêche d'appeler finish comme premier appel si vous avez toutes les données dans un seul bloc (auquel cas il pourrait y avoir un meilleur nom). Ou il est possible de jeter si vous essayez une mise à jour sur ces chiffrements. CNG renvoie une erreur de taille invalide si vous essayez de faire une continuation sur CCM par exemple.

En ce qui concerne le moment où la balise est définie sur le déchiffrement, vous ne le savez souvent pas tant que vous n'avez pas lu l'intégralité du paquet. Si nous prenons TLS comme exemple, nous devrons peut-être lire des paquets réseau de 8 * 2k pour accéder à la balise à la fin. d'un bloc de 16k. Nous devons donc maintenant mettre en mémoire tampon l'intégralité des 16k avant de pouvoir commencer le déchiffrement et il n'y a aucune chance de chevauchement (je ne dis pas que cela serait utilisé pour TLS juste qu'un processus lié IO est courant pour ces types de chiffrements, que ce soit un disque ou réseau).

@Drawaes re. flux fragmentés et limites de mise en mémoire tampon :
Vous devez choisir vos batailles. Vous ne pourrez pas créer une API unificatrice alignée sur chaque objectif agréable à avoir dans le monde AE ​​- et il existe de nombreux objectifs de ce type. Ex. il y a un streaming AEAD décent en morceaux dans Inferno , mais ce n'est pas du tout une norme, et une telle norme n'existe pas. À un niveau supérieur, l'objectif est "canaux sécurisés" (voir this , this et this ).

Cependant, nous devons penser plus petit pour le moment. Chunking/buffer-limiting ne sont même pas sur le radar pour les efforts de normalisation (section " AEADs with large plaintexts ")..

Les opérations de chiffrement/déchiffrement concernent fondamentalement les transformations. Ces transformations nécessitent des tampons et ne sont pas en place (les tampons de sortie doivent être plus grands que les tampons d'entrée - au moins pour la transformation Encrypt).

La RFC 5116 pourrait également être intéressante.

@Drawaes , intéressant que vous évoquiez TLS. Je dirais que SSLStream (si elle utilisait cette API) ne doit pas renvoyer de résultats non authentifiés à une application car l'application n'aura aucun moyen de se défendre.

Bien sûr, mais c'est le problème de SSLStreams. J'ai prototypé cette chose exacte (TLS géré au niveau du protocole appelant à CNG et OpenSSL pour les bits de chiffrement) au-dessus des pipelines. La logique était assez simple, les données chiffrées entrent, déchiffrent le tampon en place, s'attachent à la sortie et répètent jusqu'à ce que vous arriviez à la balise. Au tag appel terminé...

S'il jette fermer le pipeline. Si ce n'est pas le cas, rincez, permettant à l'étape suivante de travailler soit sur le même fil, soit via dispatch.

Ma preuve de concept n'était pas prête pour les heures de grande écoute, mais en l'utilisant et en évitant beaucoup de copies, etc., elle a montré une augmentation des performances très décente ;)

Le problème avec toute mise en réseau est que les éléments du pipeline commencent à allouer leurs propres tampons et à ne pas utiliser autant que possible ceux qui traversent déjà le système.

La crypte d'OpenSsl et CNG ont cette même méthode Update, Update, Finish. Finish peut afficher la balise comme indiqué. Les mises à jour doivent être en taille de bloc (pour CNG) et pour OpenSsl, la mise en mémoire tampon est minimale pour atteindre une taille de bloc.

Comme ce sont des primitives, je ne suis pas sûr que nous attendions d'eux des fonctionnalités de niveau supérieur. Si nous concevions une API de niveau "utilisateur" plutôt que des primitives pour les construire, je dirais alors que la génération de clés, la construction IV et des morceaux authentifiés entiers devraient tous être implémentés, donc je suppose que cela dépend du niveau cible de cette API. est vraiment.

Mauvais bouton

@blowdart , qui a eu des idées intéressantes sur la gestion des nonces.

La gestion des nonce est donc essentiellement un problème d'utilisateur et est spécifique à leur configuration.

Alors ... faites-en une exigence. Vous devez brancher la gestion nonce ... et ne pas fournir d'implémentation par défaut, ni aucune implémentation du tout. Ceci, plutôt qu'un simple

cipher.Init(myKey, nonce);

oblige les utilisateurs à faire un geste précis pour comprendre les risques.

L'idée de @blowdart pourrait aider à résoudre à la fois les problèmes de gestion nonce et les différences entre les algorithmes. Je conviens qu'il est probablement important de ne pas avoir d'implémentation intégrée pour s'assurer que les utilisateurs comprennent que la gestion des nonces est un problème qu'ils doivent résoudre. À quoi ressemble quelque chose comme ça?

interface INonceProvider
{
    public void GetNextNonce(Span<byte> writeNonceHere);
}

class AesGcmCipher : Cipher
{
    public AesGcmCipher(ReadOnlySpan<byte> key, INonceProvider nonceProvider);
}

// Enables platform-specific hardware keys
class AesGcmCng : Cipher
{
    public AesGcmCng(CngKey key, INonceProvider nonceProvider);
}

// Example of AEAD that might not need a nonce
class AesCBCHmac : Cipher
{
    public AesCBC(ReadOnlySpan<byte> key)
}

class Cipher
{
    // As above, but doesn't take keys, IVs, or nonces
}

Mais à quoi sert le INonceProvider ? C'est juste une interface/un type supplémentaire, si l'Init prend juste un none et doit être appelé avant de commencer n'importe quel bloc, n'est-ce pas la même chose sans un extra/interface ?

De plus, je ne suis pas un expert en cryptographie, mais AES ne nécessite-t-il pas d'IV (ce qui n'est pas un nonce mais doit être fourni par l'utilisateur ?)

C'est juste une interface/type supplémentaire

C'est un peu le point. Il dit essentiellement que _nonce management_ est un problème, pas seulement en passant dans un tableau d'octets mis à zéro ou même aléatoire. Cela pourrait également aider à empêcher une réutilisation par inadvertance si les gens interprètent GetNextNonce comme signifiant "renvoyez quelque chose de différent de ce que vous avez fait la dernière fois".

Il est également utile de ne pas en avoir besoin pour les algorithmes qui n'ont pas de problèmes de gestion nonce (comme AES SIV ou peut-être AES+CBC+HMAC).

Les exigences exactes IV / nonce varient en fonction du mode. Par example:

  • AES ECB ne nécessite pas de nonce ou IV
  • AES GCM nécessite un nom nonce 96 bits qui ne doit jamais être réutilisé ou la sécurité de la clé est rompue. Une faible entropie convient tant que le nonce n'est pas réutilisé.
  • AES CBC nécessite un IV 128 bits qui doit être aléatoire. Si l'IV se répète, il révèle seulement si le même message a déjà été envoyé.
  • AES SIV n'a pas besoin d'un IV explicite puisqu'il le dérive d'autres entrées.

AES CBC a besoin d'IV, n'est-ce pas ? Alors allez-vous avoir un InitializationVectorProvider ? Ce n'est pas un nonce mais
nonce like et la réutilisation du dernier bloc a conduit à une attaque tls car le iv peut être prédit. Vous ne pouvez explicitement pas utiliser, disons, un nonce séquentiel pour CBC.

Ouais mais un IV n'est pas un nonce donc vous ne pouvez pas avoir le terme fournisseur nomce

Je ne voulais pas dire qu'AES CBC n'a pas besoin d'IV - c'est le cas. Je voulais juste spéculer sur certains schémas qui dérivent l'IV d'autres données.

Bien sûr, je suppose que mon point est que je l'aime généralement ... Je peux regrouper le fournisseur;) mais appelez-le un fournisseur iv ou ayez 2 interfaces pour être clair sur l'intention

@morganbr Les usines INonceProvider passées dans les constructeurs de chiffrement sont une mauvaise conception. Il passe complètement à côté du fait que _nonce_ n'existe pas par lui-même : la contrainte "_...utilisé une fois_" a un _contexte_. Dans le cas des modes CTR et GCM (qui utilise CTR), le _contexte_ du _nonce_ est la _clé_. C'est à dire. _nonce provider_ doit retourner un nonce qui n'est utilisé qu'une seule fois dans un contexte d'une _key_ spécifique.

Étant donné que le INonceProvider de votre API proposée n'est pas sensible aux clés, il ne peut pas générer de nonces corrects (autrement que via le caractère aléatoire, ce qui n'est pas ce qu'est un nonce, même si l'espace de bits était suffisamment grand pour que le caractère aléatoire statistique fonctionne sans encombre).

Je ne suis pas tout à fait sûr de ce que ce fil de discussion vise à atteindre. Diverses idées de conception de cryptage authentifié sont discutées... ok. Qu'en est-il des interfaces de chiffrement authentifié déjà intégrées dans .NET Core, en particulier dans son API ASP.NET ? Il y a IAuthenticatedEncryptor , etc. Toutes ces fonctionnalités sont déjà implémentées, extensibles et livrées dans le cadre de .NET Core aujourd'hui. Je ne dis pas que DataProtection crypto est parfait, mais est-il prévu de les ignorer ? Les changer ? Assimiler ou refactoriser ?

DataProtection crypto a été construit par @GrabYourPitchforks (Levi Broderick). Il connaît le sujet, et son opinion/contribution/rétroaction serait des plus précieuses pour cette communauté. J'apprécie les divertissements sur le thème de la cryptographie autant que quiconque, mais si quelqu'un veut s'intéresser sérieusement à la conception de l'API crypto, alors de vrais experts qui font déjà partie de l'équipe MS devraient être engagés.

@sdrapkin , les fournisseurs nonce devant être conscients des clés sont un bon point. Je me demande s'il existe un moyen raisonnable de modifier ces API pour appliquer cela.

DataProtection est une bonne API, mais c'est une construction de niveau supérieur. Il encapsule la génération et la gestion des clés, les IV et le protocole de sortie. Si quelqu'un a besoin d'utiliser (par exemple) GCM dans une implémentation d'un protocole différent, DataProtection ne rend pas cela possible.

L'équipe de cryptographie .NET comprend @bartonjs , @blowdart et moi-même. Bien sûr, si @GrabYourPitchforks veut intervenir, il est plus que bienvenu.

Je suis d'accord avec @morganbr en ce sens que c'est censé être une primitive de bas niveau (en fait, c'est dit dans le titre). Alors que la protection des données, etc. est conçue pour être utilisée directement dans le code utilisateur et réduit la possibilité de se tirer une balle dans le pied, la façon dont je vois cette primitive est de permettre au framework et aux bibliothèques de construire des constructions de niveau supérieur sur une base commune.

Avec cette pensée à l'esprit, le fournisseur est d'accord s'il doit être fourni à chaque fois qu'une clé est fournie. Cela le rend un peu désordonné, laissez-moi vous expliquer l'utilisation de TLS (c'est une utilisation bien connue des modes de bloc AES pour le trafic réseau, c'est tout).

Je reçois une "trame" (peut-être plus de 2 + TU avec le MTU ~ 1500 d'Internet). Il contient le nonce (ou une partie du nonce avec 4 octets laissés "cachés") Je dois ensuite définir cette valeur sur un "fournisseur" de shell, puis appeler décrypter et passer par mon cycle de décryptage des tampons pour obtenir un seul texte brut .

Si tu es content de ça, je peux vivre avec. Je suis impatient de faire avancer les choses, si désireux de mettre à jour la conception ci-dessus en quelque chose sur lequel nous pouvons nous mettre d'accord.

Merci d'avoir bifurqué la discussion, d'avoir du temps libre pour sauter dessus. @Drawaes , pouvez-vous confirmer/mettre à jour le meilleur article en tant qu'étalon-or/cible de cette conversation en évolution ? Sinon, pouvez-vous le mettre à jour ?

Je vois que la proposition actuelle a un problème fatal, puis d'autres problèmes liés au fait d'être trop bavard.

// current proposed usage
using (var cipher = new AesGcmCipher(bitsize: 256))
{
    cipher.Init(myKey, nonce);
    while (!inputSource.EOF)
    {
        var inputSpan = inputSource.ReadSpan(cipher.BlockSize);
        cipher.Update(inputSpan);
        outputSource.Write(inputSpan);
    }
    cipher.AddAssociatedData(extraInformation); // <= fatal, one can't just do this
    cipher.Finish(finalBlockData);
    cipher.GetTag(tagData);
}

Si vous examinez une véritable primitive AEAD, les données de confidentialité et les données authentifiées sont mélangées. Voir ceci pour Auth Data 1 et CipherText1. Cela continue bien sûr pour plusieurs blocs, pas seulement 1.highlighted

Puisque tout le monde est un mème, je ne peux pas résister, désolé :)
Can't resist

De plus, l'API semble bavarde avec new, init, update etc. Je proposerais le modèle de ce programmeur

// proposed, see detailed comments below
using (var cipher = new AesGcmCipher(myKey, iv, aad)) // 1
{
    // 2
    while (!inputSource.EOF) 
    {
        var inputSpan = inputSource.ReadSpan(16411); // 3
        var outSpan = cipher.Encrypt(inputSpan); // 4
        outputSource.Write(outSpan); 
    }    
    var tag = cipher.Finish(finalBlockData); // 5
}
  1. Typiquement AAD << texte en clair, j'ai donc vu cipher.Init(mykey, nonce, aad); où l'intégralité de l'AAD est transmise en tant que tampon, puis le chiffrement écrase le reste du flux potentiellement gigaoctet +. (par exemple , le paramètre CipherModeInfo de BCryptEncrypts ). De plus, la taille de myKey établit déjà AES128, 192, 256, pas besoin d'un autre paramètre.
  2. Init devient une API facultative au cas où l'appelant souhaite réutiliser la classe existante, les constantes AES existantes et ignorer la génération de la sous-clé AES si la clé AES est la même
  3. L'API de chiffrement devrait protéger l'appelant des composants internes de gestion de la taille des blocs, comme la plupart des autres API de chiffrement ou même des API .NET existantes. Appelant concerné par la taille de la mémoire tampon optimisée pour son cas d'utilisation, par exemple les E/S réseau via des mémoires tampons de 16 K+). Démo avec un nombre premier> 16K pour tester les hypothèses d'implémentation
  4. inputSpan est en lecture seule. Et entrée. Donc besoin d'un outSpan
  5. Update() chiffre ou déchiffre ? Il suffit d'avoir des interfaces Encrypt et Decrypt pour correspondre au modèle mental d'un développeur. tag est également la donnée souhaitée la plus importante à cet instant, renvoyez-la.

En fait, aller plus loin, pourquoi ne pas simplement

using (var cipher = new AesGcmCipher(myKey, iv, aad))
{
    var tag = cipher.EncryptFinal(inputSpan, outputSpan);
}

Aussi, s'il vous plaît évitez INonceProvider et les sortes. Les crypto primitives n'en ont pas besoin, il suffit de s'en tenir à byte[] iv (mon préféré pour les petites données) ou Span (le supposé nouveau cool mais trop d'abstraction à mon humble avis). Le fournisseur Nonce fonctionne à une couche supérieure et le résultat pourrait simplement être le iv vu ici.

Le problème de rendre les primitives si primitives est que les gens les utiliseront simplement de manière incorrecte. Avec un fournisseur, nous pouvons au moins forcer une réflexion sur leur utilisation.

On parle de l'AEAD en général dont GCM est spécifique. Donc, tout d'abord, le cas généralisé ( iv ) devrait piloter la conception, pas le cas spécifique ( nonce ).

Deuxièmement, comment le simple fait de passer de byte[] iv à GetNextNonce(Span<byte> writeNonceHere) résout-il réellement le problème du nonce ? Vous avez seulement changé le nom/l'étiquette du problème tout en le rendant plus complexe qu'il ne devrait l'être.

Troisièmement, puisque nous entrons dans des politiques de protection iv , devrions-nous également entrer dans des politiques de protection clés ? Qu'en est-il des politiques de distribution des clés ? Ce sont évidemment des préoccupations de niveau supérieur.

Enfin, nonce est extrêmement situationnel sur l'utilisation aux couches supérieures. Vous ne voulez pas avoir une architecture fragile où les préoccupations inter-couches sont couplées.

Franchement, si nous pouvions cacher les primitifs à moins que quelqu'un ne fasse un geste pour dire que je sais ce que je fais, je pousserais pour cela. Mais nous ne pouvons pas. Il y a beaucoup trop de mauvaises implémentations de chiffrement parce que les gens pensent "Oh c'est disponible, je vais l'utiliser". Heck regarde AES lui-même, je vais juste l'utiliser sans HMAC.

Je veux que les API soient sécurisées par défaut, et si cela signifie un peu plus de douleur, franchement, je suis tout à fait d'accord. 99 % des développeurs ne savent pas ce qu'ils font en matière de cryptographie et faciliter la tâche aux 1 % qui le savent devrait être une priorité moindre.

L'étendue n'existe pas. Je me fiche de ce qui peut arriver ou non - ce n'est pas dans NetStandard2

@sdrapkin comme le souligne @Drawaes Span<T> est .NET Standard 1.0 et peut donc être utilisé sur n'importe quel framework. C'est aussi plus sûr que ArraySegment<T> car il ne vous permet d'accéder qu'à la fenêtre référencée ; plutôt que l'ensemble du tableau.

De plus, ReadOnlySpan<T> empêche la modification de cette fenêtre ; encore une fois contrairement au segment de tableau où tout ce qui est passé peut modifier et/ou conserver une référence au tableau passé.

Span devrait être la référence générale pour les apis de synchronisation (le fait qu'une API utilisant Span peut également gérer la mémoire native allouée à la pile ainsi que les tableaux ; c'est la cerise sur le gâteau)

c'est à dire
Avec ArraySegment, la lecture seule est suggérée via docs; et aucune lecture/modification hors limites n'est empêchée

void Encrypt(
    ArraySegment<byte> iv, // readonly; covered by authentication
    ArraySegment<byte> plaintext, // readonly; covered by authentication
    ref ArraySegment<byte> ciphertext, // must be of at least [plaintext_length + MaxTagSize] length. iv is not part of ciphertext.
    ArraySegment<byte> additionalData = default(ArraySegment<byte>) // readonly; optional; covered by authentication
    );

Cependant, avec Span, la lecture seule est appliquée par api ; ainsi que les lectures hors limites des tableaux étant empêchées

void Encrypt(
    ReadOnlySpan<byte> iv, // covered by authentication
    ReadOnlySpan<byte> plaintext, // covered by authentication
    Span<byte> ciphertext, // must be of at least [plaintext_length + MaxTagSize] length. iv is not part of ciphertext.
    ReadOnlySpan<byte> additionalData = ReadOnlySpan<byte>.Empty) // optional; covered by authentication
    );

Il transmet beaucoup mieux l'intention avec les paramètres; et est moins sujet aux erreurs en ce qui concerne les lectures/écritures hors limites.

@benaadams @Drawaes n'a jamais dit que Span<T> était dans NetStandard ( tout NetStandard livré ). Ce qu'il a dit est (1) d'accord que Span<T> n'est dans aucun NetStandard livré ; (2) que Span<T> seront _"expédiés autour de la période 2.1"_.

Pour ce problème Github particulier, cependant, la discussion (en lecture seule) Span<T> concerne le bikeshed en ce moment - il n'y a aucune clarté sur la portée ou l'objectif de l'API à concevoir.

Soit nous optons pour l'API AEAD primitive brute de bas niveau (ex. similaire à CAESAR):

  • Avantages : bon ajustement pour AES-GCM/CCM, vecteurs de test existants provenant de bonnes sources (NIST, RFC). @sidshetye sera content. @blowdart méditera sur _"rendre les primitifs si primitifs"_, mais finira par voir le Yin et le Yang parce que les primitifs sont primitifs et qu'il n'y a aucun moyen de les protéger des enfants.
  • Inconvénients : les utilisateurs experts (le proverbial 1 %) utiliseront les API de bas niveau de manière responsable, tandis que les autres utilisateurs non experts (99 %) en feront un mauvais usage pour écrire un logiciel .NET défectueux qui sera responsable de la grande majorité de .NET CVE, qui contribueront grandement à la perception que .NET est une plate-forme non sécurisée.

Ou nous optons pour une API AEAD de haut niveau impossible ou résistante aux abus :

  • Avantages : 99 % des utilisateurs non experts continueront à faire des erreurs, mais du moins pas dans le code AEAD. Le _ "Je veux que les API soient sécurisées par défaut"_ de @blowdart résonne profondément dans l'écosystème, et la sécurité, la prospérité et le bon karma s'abattent sur tous. De nombreuses bonnes conceptions et implémentations d'API sont déjà disponibles.
  • Inconvénients : Aucune norme. Pas de vecteurs de test. Aucun consensus sur la question de savoir si AEAD est même le bon objectif à cibler pour une API de streaming en ligne de haut niveau (spoiler : ce n'est pas le cas - voir l'article de Rogaway ).

Ou, nous faisons les deux. Ou nous entrons dans la paralysie de l'analyse et ferions aussi bien de clore ce problème tout de suite.

Je pense fermement que, faisant partie du langage de base, la crypto doit avoir une API de base solide et de bas niveau. Une fois que vous avez cela, la création d'API de haut niveau ou d'API "roues d'entraînement" peut être rapidement intégrée par le noyau ou la communauté. Mais je défie quiconque de faire l'inverse avec élégance. De plus, le sujet est " Primitive générale de bas niveau pour les chiffrements " !

@Drawaes existe-t-il un calendrier pour converger et résoudre ce problème ? Avez-vous l'intention d'impliquer des personnes non Microsoft au-delà de ces alertes GitHub ? Comme une conférence téléphonique de 30 minutes ? J'essaie de rester en dehors d'un terrier de lapin, mais nous parions que la cryptographie de base .NET sera à un certain niveau de maturité et de stabilité.

Nous continuons d'y prêter attention et d'y travailler. Nous avons rencontré le Microsoft Cryptography Board (l'ensemble de chercheurs et d'autres experts qui conseillent l'utilisation de la cryptographie par Microsoft) et @bartonjs aura bientôt plus d'informations à partager.

Sur la base d'un petit griffonnage de flux de données et des conseils du Crypto Board, nous avons proposé ce qui suit. Notre modèle était GCM, CCM, SIV et CBC+HMAC (notez que nous ne parlons pas de faire SIV ou CBC+HMAC pour le moment, juste que nous voulions prouver la forme).

```C#
interface publique INonceProvider
{
ReadOnlySpanGetNextNonce(int nonceSize);
}

classe abstraite publique AuthenticatedEncryptor : IDisposable
{
public int NonceOrIVSizeInBits { obtenir ; }
public int TagSizeInBits { obtenir ; }
public bool SupportsAssociatedData { obtenir ; }
public ReadOnlySpanLastNonceOrIV { obtenir ; }
public ReadOnlySpanLastTag { obtenir ; }

protected AuthenticatedEncryptor(
    int tagSizeInBits,
    bool supportsAssociatedData,
    int nonceOrIVSizeInBits) => throw null;

protected abstract bool TryEncrypt(
    ReadOnlySpan<byte> data,
    ReadOnlySpan<byte> associatedData,
    Span<byte> encryptedData,
    out int bytesWritten,
    Span<byte> tag,
    Span<byte> nonceOrIVUsed);

public abstract void GetEncryptedSizeRange(
    int dataLength,
    out int minEncryptedLength,
    out int maxEncryptedLength);

public bool TryEncrypt(
    ReadOnlySpan<byte> data,
    ReadOnlySpan<byte> associatedData,
    Span<byte> encryptedData,
    out int bytesWritten) => throw null;

public byte[] Encrypt(
    ReadOnlySpan<byte> data,
    ReadOnlySpan<byte> associatedData) => throw null;

// some variant of the Dispose pattern here.

}

classe scellée publique AesGcmEncryptor : AuthenticatedEncryptor
{
public AesGcmEncryptor(ReadOnlySpankeySize, INonceProvider nonceProvider)
: base(128, vrai, 96)
{
}
}

classe scellée publique AesCcmEncryptor : AuthenticatedEncryptor
{
public AesCcmEncryptor(
ReadOnlySpanclé,
int nonceSizeInBits,
INonceProvider nonceProvider,
int tagSizeInBits)
: base(tagSizeInBits, true, nonceSizeInBits)
{
valider nonceSize et tagSize par rapport à la spécification de l'algorithme ;
}
}

classe abstraite publique AuthenticatedDecryptor : IDisposable
{
public abstract bool TryDecrypt(
ReadOnlySpanétiqueter,
ReadOnlySpannonceOrIV,
ReadOnlySpanDonnées cryptées,
ReadOnlySpanDonnéesassociées,
EnvergureLes données,
out int bytesWritten);

public abstract void GetEncryptedSizeRange(
    int encryptedDataLength,
    out int minDecryptedLength,
    out int maxDecryptedLength);

public byte[] Decrypt(
    ReadOnlySpan<byte> tag,
    ReadOnlySpan<byte> nonceOrIV,
    ReadOnlySpan<byte> encryptedData,
    ReadOnlySpan<byte> associatedData) => throw null;

// some variant of the Dispose pattern here.

}

classe scellée publique AesGcmDecryptor : AuthenticatedDecryptor
{
public AesGcmDecryptor(ReadOnlySpanclé) => lancer null ;
}

classe scellée publique AesCcmDecryptor : AuthenticatedDecryptor
{
public AesCcmDecryptor(ReadOnlySpanclé) => lancer null ;
}
```

Cette proposition élimine le flux de données. Nous n'avons pas vraiment beaucoup de flexibilité sur ce point. Le besoin réel (faible) combiné aux risques associés (extrêmement élevés pour GCM) ou son impossibilité (CCM) signifie qu'il est tout simplement parti.

Cette proposition utilise une source externalisée de nonce pour le chiffrement. Nous n'aurons aucune implémentation publique de cette interface. Chaque application/protocole doit faire de sa propre liaison la clé du contexte afin qu'elle puisse alimenter les choses de manière appropriée. Bien que chaque appel à TryEncrypt ne fasse qu'un seul appel à GetNextNonce, il n'y a aucune garantie que ce TryEncrypt particulier réussira, il appartient donc toujours à l'application de comprendre si cela signifie qu'elle doit réessayer le nonce. Pour CBC+HMAC, nous créerions une nouvelle interface, IIVProvider, pour éviter de brouiller la terminologie. Pour SIV, l'IV est construit, il n'y a donc pas de paramètre acceptable ; et sur la base de la spécification, le nonce (lorsqu'il est utilisé) semble être simplement considéré comme faisant partie de l'associatedData. Ainsi, SIV, au moins, suggère que le fait d'avoir nonceOrIV comme paramètre de TryEncrypt n'est généralement pas applicable.

TryDecrypt lance très certainement une balise invalide. Il ne renvoie false que si la destination est trop petite (selon les règles de Try-methods)

Choses qui sont définitivement ouvertes aux commentaires :

  • Les tailles devraient-elles être en bits (comme des parties importantes des spécifications) ou en octets (puisque seules les valeurs %8 sont légales de toute façon, et nous allons toujours diviser, et certaines parties des spécifications parlent de choses comme la taille nonce en octets ) ?
  • Noms et ordre des paramètres.
  • Les propriétés LastTag/LastNonceOrIV. (Les rendre (inscriptibles) Spans sur un TryEncrypt public signifie simplement qu'il y a trois tampons qui pourraient être trop petits, en les faisant s'asseoir sur le côté, le "Try" est plus clair; la classe de base peut faire la promesse que ça va n'offrez jamais un tampon trop court.).
  • Offrir un algorithme AE pour lequel cela ne fonctionne pas.
  • Faut-il déplacer associatedData à la fin avec une valeur par défaut de ReadOnlySpan<byte>.Empty ?

    • Ou des surcharges faites qui l'omettent ?

  • Quelqu'un veut-il affirmer son amour ou sa haine pour les méthodes de retour byte[] ? (Une faible allocation peut être obtenue en utilisant la méthode Span, c'est juste pour plus de commodité)
  • Les méthodes des gammes de taille étaient en quelque sorte boulonnées à la fin.

    • Leur but est que

    • Si la plage de destination est inférieure à min, renvoyez immédiatement false.

    • Les méthodes de retour byte[] alloueront un tampon de max, puis Array.Resize si nécessaire.

    • Oui, pour GCM et CCM min=max=input.Length , mais ce n'est pas vrai pour CBC+HMAC ou SIV

    • Existe-t-il un algorithme qui devrait prendre en compte la longueur des données associées ?

Certainement des octets - pas des bits.
Un fournisseur nonce qui ne connaît pas la clé est une grosse erreur.

Un fournisseur nonce qui ne connaît pas la clé est une grosse erreur.

Vous pouvez écrire votre fournisseur nonce comme vous le souhaitez. Nous n'en fournissons aucun.

Qu'en est-il du nettoyage déterministe/ IDisposable ?

Qu'en est-il du nettoyage déterministe/IDisposable ?

Bon appel. Ajouté à AuthenticatedEncryptor/AuthenticatedDecryptor. Je ne pense pas qu'ils devraient rechercher la disponibilité sur le fournisseur nonce, l'appelant peut simplement empiler les instructions d'utilisation.

INonceProvider concept/but n'a aucun sens pour moi (en écho aux autres). Laissez les primitives être primitives - passez le nonce de la même manière que vous passez la clé (c'est-à-dire sous forme d'octets - quelle que soit la déclaration). Aucune spécification AE / AEAD n'impose un algorithme sur la manière dont les nonces sont générés / dérivés - il s'agit d'une responsabilité de couche supérieure (au moins dans le modèle let-primitives-be-primitive).

Pas de streaming ? Ah bon? Quelle est la justification pour supprimer de force le streaming d'un chiffrement de flux comme AES-GCM à un niveau fondamental ?

Par exemple, que recommande votre conseil de cryptographie dans ces deux scénarios récents que nous avons examinés ?

  1. Le client a de gros fichiers de soins de santé entre 10 et 30 Go. Le noyau ne voit qu'un flux de données entre deux machines, il s'agit donc d'un flux de passage unique. De toute évidence, une nouvelle clé est émise pour chaque fichier de 10 Go, mais vous venez de rendre chaque flux de travail inutile. Vous voulez maintenant que nous a) tampons ces données (mémoire, pas de pipe-lining) b) effectuons le chiffrement (toutes les machines du pipeline sont maintenant inactives !) c) écrivons les données (le premier octet écrit après a et b est à 100 % Fini) ? S'il vous plaît dites-moi que vous plaisantez. Vous remettez sciemment "le cryptage est un fardeau" dans le jeu.

  2. L'unité de sécurité physique dispose de plusieurs flux 4K qui sont également chiffrés pour les scénarios au repos. La nouvelle émission de clé se produit à la limite de 15 Go. Vous proposez de mettre en mémoire tampon l'intégralité du clip ?

Je ne vois aucune contribution de la communauté, de personnes construisant réellement des logiciels du monde réel, demandant de supprimer la prise en charge du streaming. Mais ensuite, l'équipe disparaît du dialogue communautaire, se rassemble en interne, puis revient avec quelque chose que personne n'a demandé, quelque chose qui tue les applications réelles et renforce le fait que "le chiffrement est lent et coûteux, sautez-le ?"

Vous pouvez fournir Encrypt et EncryptFinal qui prendraient en charge les deux options au lieu d'imposer votre décision à l'ensemble de l'écosystème.

Le design élégant élimine la complexité, pas le contrôle.

Quelle est la justification pour supprimer de force le streaming d'un chiffrement de flux comme AES-GCM à un niveau fondamental ?

Je pense que c'était quelque chose comme

Cette proposition élimine le flux de données. Nous n'avons pas vraiment beaucoup de flexibilité sur ce point. Le besoin réel (faible) combiné aux risques associés (extrêmement élevés pour GCM) ou son impossibilité (CCM) signifie qu'il est tout simplement parti.

GCM a trop de moments oops où il permet la récupération de clé. Si un attaquant peut faire un texte chiffré choisi et regarder la sortie en continu avant la vérification de la balise, il peut récupérer la clé. (Ou alors l'un des cryptanalystes me dit). En effet, si des données traitées par GCM sont observables à tout moment avant la vérification des balises, la clé est compromise.

Je suis presque sûr que le Crypto Board recommanderait de NE PAS utiliser GCM pour le premier scénario, mais plutôt CBC + HMAC.

Si votre deuxième scénario est le cadrage 4k et que vous chiffrez chaque image 4k, cela fonctionne avec ce modèle. Chaque trame 4k + nonce + tag est déchiffrée et vérifiée avant de récupérer les octets, de sorte que vous ne divulguez jamais le flux de clés / la clé.

À titre de comparaison : je développe actuellement cette API de chiffrement "laisser les primitives être primitives". Voici ma classe pour le chiffrement authentifié.

Pour moi, il s'est avéré utile de pouvoir parler d'une crypto primitive indépendamment d'une clé. Par exemple, je veux souvent brancher une primitive spécifique dans une méthode qui fonctionne avec n'importe quel algorithme AEAD et laisser la génération de clés, etc. à cette méthode. Par conséquent, il existe une classe AeadAlgorithm et une classe Key distincte.

Une autre chose très utile qui a déjà empêché plusieurs bogues est d'utiliser des types distincts pour représenter des données de différentes formes, par exemple, un Key et un Nonce , au lieu d'utiliser un simple byte[] ou Span<byte> pour tout.


API AeadAlgorithm (cliquez pour développer)

public abstract class AeadAlgorithm : Algorithm
{
    public int KeySize { get; }

    public int NonceSize { get; }

    public int TagSize { get; }

    public byte[] Decrypt(
        Key key,
        Nonce nonce,
        ReadOnlySpan<byte> associatedData,
        ReadOnlySpan<byte> ciphertext)

    public void Decrypt(
        Key key,
        Nonce nonce,
        ReadOnlySpan<byte> associatedData,
        ReadOnlySpan<byte> ciphertext,
        Span<byte> plaintext)

    public byte[] Encrypt(
        Key key,
        Nonce nonce,
        ReadOnlySpan<byte> associatedData,
        ReadOnlySpan<byte> plaintext)

    public void Encrypt(
        Key key,
        Nonce nonce,
        ReadOnlySpan<byte> associatedData,
        ReadOnlySpan<byte> plaintext,
        Span<byte> ciphertext)

    public bool TryDecrypt(
        Key key,
        Nonce nonce,
        ReadOnlySpan<byte> associatedData,
        ReadOnlySpan<byte> ciphertext,
        out byte[] plaintext)

    public bool TryDecrypt(
        Key key,
        Nonce nonce,
        ReadOnlySpan<byte> associatedData,
        ReadOnlySpan<byte> ciphertext,
        Span<byte> plaintext)
}

@bartonjs il a raison, vous devez vous fier au programme qui ne sort pas avant l'authentification. Ainsi, par exemple, si vous ne vous authentifiez pas (ou tout simplement pas encore), vous pouvez contrôler l'entrée d'un bloc et donc connaître la sortie et travailler à rebours à partir de là...

Par exemple, un homme au milieu de l'attaque peut injecter des blocs connus dans un flux cbc et effectuer une attaque de retournement de bits classique.

Je ne sais pas vraiment comment résoudre les gros morceaux de données, merci de les couper avec des nonces en série ou similaires ... ala TLS.

Eh bien, permettez-moi de reformuler ce que je fais, mais uniquement dans le cas d'un réseau de petite taille, ce qui n'est pas suffisant pour une bibliothèque à usage général.

Dans un esprit d'ouverture, est-il possible de révéler qui fait partie du Microsoft Cryptography Review Board (et idéalement les commentaires/opinions de membres spécifiques qui ont examiné ce sujet) ? Brian LaMacchia et qui d'autre ?

_en utilisant la psychologie inversée :_

Je suis heureux que le streaming AEAD soit sorti. Cela signifie qu'Inferno continue d'être le seul AEAD de streaming pratique basé sur CryptoStream pour le Joe moyen. Merci au comité d'examen MS Crypto !

S'appuyant sur le commentaire de @ektrah , son approche (son ?) est pilotée par la RFC 5116 , à laquelle j'ai fait référence plus tôt. Il existe de nombreuses citations notables dans la RFC 5116 :

3.1. Exigences relatives à la génération de nonce
Il est essentiel pour la sécurité que les nonces soient construits d'une manière qui respecte l'exigence que chaque valeur de nonce soit distincte pour chaque invocation de l'opération de chiffrement authentifiée, pour toute valeur fixe de la clé.
...

  1. Exigences relatives aux spécifications de l'algorithme AEAD
    Chaque algorithme AEAD DOIT accepter tout nonce d'une longueur comprise entre N_MIN et N_MAX octets, inclus, où les valeurs de N_MIN et N_MAX sont spécifiques à cet algorithme. Les valeurs de N_MAX et N_MIN PEUVENT être égales. Chaque algorithme DEVRAIT accepter un nonce d'une longueur de douze (12) octets. Les algorithmes randomisés ou avec état, qui sont décrits ci-dessous, PEUVENT avoir une valeur N_MAX de zéro.
    ...
    Un algorithme de chiffrement authentifié PEUT incorporer ou utiliser une source aléatoire, par exemple, pour la génération d'un vecteur d'initialisation interne qui est incorporé dans la sortie du texte chiffré. Un algorithme AEAD de ce type est appelé randomisé ; notez cependant que seul le chiffrement est aléatoire et que le déchiffrement est toujours déterministe. Un algorithme randomisé PEUT avoir une valeur de N_MAX égale à zéro.

Un algorithme de chiffrement authentifié PEUT incorporer des informations d'état internes qui sont maintenues entre les invocations de l'opération de chiffrement, par exemple, pour permettre la construction de valeurs distinctes qui sont utilisées comme nonces internes par l'algorithme. Un algorithme AEAD de ce type est appelé avec état. Cette méthode pourrait être utilisée par un algorithme pour fournir une bonne sécurité même lorsque l'application entre des nonces de longueur nulle. Un algorithme avec état PEUT avoir une valeur de N_MAX égale à zéro.

Une idée qui mérite potentiellement d'être explorée est le passage de Nonce de longueur nulle/nulle, qui pourrait même être la valeur par défaut. Le passage d'une telle valeur Nonce "spéciale" rendra aléatoire la valeur Nonce réelle, qui sera disponible en tant que sortie de Encrypt.

Si INonceProvider reste parce que "raisons", une autre idée est d'ajouter un appel Reset() , qui sera déclenché à chaque fois que le AuthenticatedEncryptor est réinitialisé. Si, d'un autre côté, le plan est de ne jamais redéfinir les instances AuthenticatedEncryptor , cela supprimera GC si nous voulons construire une API de cryptage de morceaux en continu (ex. morceau = paquet réseau), et chaque morceau doit être crypté avec une clé différente (ex. protocole Netflix MSL , Inferno, autres). Surtout pour les opérations enc/dec parallèles où nous voudrions maintenir un pool de moteurs AEAD et emprunter des instances de ce pool pour faire enc/dec. Donnons un peu d'amour à GC :)

De mon point de vue, le seul but des primitives cryptographiques est d'implémenter des protocoles de sécurité de niveau supérieur bien conçus. Chacun de ces protocoles insiste pour générer des nonces à sa manière. Par example:

  • TLS 1.2 suit les recommandations de la RFC 5116 et concatène un IV de 4 octets avec un compteur de 8 octets,
  • TLS 1.3 xor est un compteur de 8 octets complété à 12 octets avec un IV de 12 octets,
  • Le bruit utilise un compteur de 8 octets rempli à 12 octets dans l'ordre des octets gros-boutien pour AES-GCM et un compteur de 8 octets rempli à 12 octets dans l'ordre des octets petit-boutien pour ChaCha/Poly.

GCM est beaucoup trop fragile pour les nonces randomisés à des tailles de nonce typiques (96 bits). Et je ne connais aucun protocole de sécurité prenant en charge les nonces aléatoires.

Il n'y a pas beaucoup de demande pour plus d'API fournissant des primitives de chiffrement. 99,9% des développeurs ont besoin de recettes de haut niveau pour des scénarios liés à la sécurité : stockage d'un mot de passe dans une base de données, chiffrement d'un fichier au repos, transfert sécurisé d'une mise à jour logicielle, etc.

Cependant, les API pour ces recettes de haut niveau sont rares. Les seules API disponibles sont souvent uniquement HTTPS et les primitives de chiffrement, ce qui oblige les développeurs à déployer leurs propres protocoles de sécurité. IMO la solution n'est pas de mettre beaucoup d'efforts dans la conception d'API pour travailler avec des primitives. Ce sont des API pour des recettes de haut niveau.

Merci pour les commentaires, tout le monde ! Quelques questions :

  1. Alors que le décryptage en continu peut échouer de manière catastrophique, le cryptage en continu pourrait être faisable. Le cryptage en streaming (avec une option sans streaming) mais uniquement le décryptage sans streaming semble-t-il plus utile ? Si oui, il y a quelques problèmes à résoudre :
    une. Certains algorithmes (CCM, SIV) ne prennent pas en charge le streaming. Devrions-nous mettre le chiffrement en continu sur la classe de base et mettre en mémoire tampon les entrées diffusées ou les rejeter à partir des classes dérivées ?
    b. Le streaming AAD n'est probablement pas possible en raison de contraintes d'implémentation, mais différents algorithmes en ont besoin à différents moments (certains en ont besoin au début, d'autres n'en ont pas besoin jusqu'à la fin). Devrions-nous l'exiger dès le départ ou avoir une méthode pour l'ajouter qui fonctionne lorsque les algorithmes individuels le permettent ?
  1. Nous sommes ouverts aux améliorations de INonceProvider tant que le point est que les utilisateurs doivent écrire du code générant un nouveau nonce. Quelqu'un a-t-il une autre forme proposée pour cela?

1 . a = Je pense que cela pourrait être un problème de ne pas avertir l'utilisateur tôt. Imaginez le scénario de quelqu'un au-dessus, un fichier de 10 Go. Ils pensent qu'ils reçoivent du streaming, puis un peu plus tard, un autre développeur modifie le chiffrement et ensuite le code met en mémoire tampon 10 Go (ou essaie) avant de renvoyer une valeur.

  1. b = Encore une fois avec l'idée de "streaming" ou de mise en réseau, par exemple AES GCM, etc., vous n'obtenez pas les informations AAD jusqu'à la fin pour le décryptage. En ce qui concerne le cryptage, je n'ai pas encore vu de cas où vous n'avez pas les données à l'avance. Je dirais donc qu'au moins pour le chiffrement, vous devriez l'exiger au début, le déchiffrement est plus complexe.
  1. Je pense que ce n'est vraiment pas un problème, fournir les "octets" pour le nonce via une interface ou simplement directement n'est ni ici ni là. Vous pouvez obtenir la même chose dans les deux sens, je trouve juste que c'est plus moche pour un primitif mais je ne m'y oppose pas avec véhémence si cela permet aux gens de mieux dormir la nuit. Je voudrais simplement rayer cela comme un fait accompli et passer aux autres questions.

Concernant le processus de délibération

@bartonjs : Nous pourrions discuter toute la journée si des décisions à huis clos sans implication de la communauté sont une justification efficace, mais nous allons sortir du sujet, donc je vais laisser faire. De plus, sans communication en face à face ou en temps réel plus riche, je ne veux déranger personne.

Concernant la diffusion en continu

1. l'argument "le streaming n'implique aucune sécurité AES-GCM"

Plus précisément, steaming => renvoie les données décryptées à l'appelant avant la vérification de la balise => pas de sécurité. Ce n'est pas du son. @bartonjs revendique 'texte chiffré choisi => regarder la sortie => récupérer la clé' tandis que @drawaes revendique 'contrôler l'entrée pour un bloc => donc connaître la sortie => "travailler à partir de là" '

Eh bien, dans AES-GCM, la seule chose que fait la balise, c'est la vérification de l'intégrité (protection anti-effraction). Il a 0 impact sur la vie privée. En fait, si vous supprimez le traitement des balises GCM/GHASH d'AES-GCM, vous obtenez simplement le mode AES-CTR. C'est cette construction qui gère l'aspect de la vie privée. Et CTR est malléable aux retournements de bits mais n'est "cassé" d'aucune des manières que vous affirmez tous les deux (récupération de la clé ou du texte en clair) car cela signifierait que la primitive AES fondamentale est compromise. Si votre cryptanalyste (qui est-ce ?) sait quelque chose que le reste d'entre nous ne sait pas, il devrait le publier. La seule chose possible est qu'un attaqué peut retourner le bit N et savoir que le bit N du texte en clair a été retourné - mais il ne sait jamais ce qu'est le texte en clair réel.

Alors

1) la confidentialité du texte en clair est toujours appliquée
2) la vérification de l'intégrité est simplement différée (jusqu'à la fin du flux) et
3) aucune clé n'est jamais compromise.

Pour les produits et les systèmes où la diffusion en continu est fondamentale, vous pouvez désormais au moins concevoir un compromis où l'on passe momentanément de l'AEAD au cryptage AES normal, puis on revient à l'AEAD lors de la vérification des balises. Cela débloque plusieurs concepts innovants pour adopter la sécurité au lieu de dire "Vous voulez mettre tout cela en mémoire tampon - êtes-vous fou ? Nous ne pouvons pas faire de cryptage !".

Tout cela parce que vous souhaitez implémenter uniquement EncryptFinal plutôt que Encrypt et EncryptFinal (ou équivalents).

2. Non spécifique à GCM !

Maintenant, AES-GCM n'est pas une bête magique pour avoir des "moments oups" à gogo. C'est simplement AES-CTR + GHASH (une sorte de hachage si je puis me permettre). Les considérations nonce liées à la confidentialité sont héritées du mode CTR et les considérations de balise liées à l'intégrité proviennent des tailles de balise variables autorisées dans la spécification. Pourtant, AES-CTR + GHASH est très similaire à quelque chose comme AES-CBC + HMAC-SHA256 en ce que le premier algorithme gère la confidentialité et le second gère l'intégrité. Dans AES-CBC + HMAC-SHA256, les retournements de bits dans le texte chiffré corrompent le bloc correspondant dans le texte déchiffré (contrairement au CTR) ET retournent également de manière déterministe les bits dans le bloc de texte en clair déchiffré suivant (comme le CTR). Encore une fois, un attaquant ne saura pas ce que sera le texte en clair résultant - juste que les bits ont été inversés (comme CTR). Enfin, le contrôle d'intégrité (HMAC-SHA256) l'attrapera. Mais ne traitant que le dernier octet (comme GHASH).

Donc, si votre argument consistant à retenir TOUTES les données déchiffrées jusqu'à ce que l'intégrité soit correcte est vraiment bon, il doit être appliqué de manière cohérente. Ainsi, TOUTES les données sortant du chemin AES-CBC doivent également être mises en mémoire tampon (en interne par la bibliothèque) jusqu'à ce que HMAC-SHA256 passe. Cela signifie essentiellement que sur .NET, aucune donnée en streaming ne peut même bénéficier des avancées de l'AEAD. .NET force la rétrogradation des données en continu. Pour choisir entre aucun cryptage ou cryptage régulier. Pas d'AEAD. Lorsque la mise en mémoire tampon est techniquement impossible, les architectes devraient au moins avoir la possibilité d'avertir les utilisateurs finaux que "les images de drones peuvent être corrompues" plutôt que "pas d'yeux pour vous".

3. C'est le meilleur que nous ayons

Les données sont de plus en plus volumineuses et la sécurité doit être renforcée. Le streaming est également une réalité que les concepteurs doivent adopter. Jusqu'à ce que le monde élabore un algorithme AEAD véritablement intégré capable de détecter nativement la falsification de la corruption en cours de route, nous sommes bloqués avec le cryptage + l'authentification en tant que copains boulonnés. Les vraies primitives AEAD sont en cours de recherche, mais nous n'avons pour l'instant que le cryptage + l'authentification.

Je me soucie moins de "AES-GCM" que d'un algorithme AEAD rapide et populaire qui peut prendre en charge les charges de travail en continu - super répandu dans un monde riche en données et hyper-connecté.

4. Utilisez AES-CBC-HMAC, utilisez (insérer une solution de contournement)

le Crypto Board recommande de NE PAS utiliser GCM pour le premier scénario, mais plutôt CBC + HMAC.

Laissant de côté tout ce qui est mentionné ci-dessus ou même les spécificités du scénario - suggérant qu'AES-CBC-HMAC n'est pas gratuit. Il est environ 3 fois plus lent qu'AES-GCM car le chiffrement AES-CBC n'est pas parallélisable et puisque GHASH peut être accéléré via l'instruction PCLMULQDQ. Donc, si vous êtes à 1 Go/sec avec AES-GCM, vous allez maintenant atteindre environ 300 Mo/sec avec AES-CBC-HMAC. Cela perpétue à nouveau l'état d'esprit "Crypto vous ralentit, sautez-le" - un état d'esprit que les spécialistes de la sécurité s'efforcent de combattre.

chiffrement de chaque image 4k

les codecs vidéo devraient soudainement faire du cryptage ? Ou la couche de chiffrement doit maintenant comprendre les codecs vidéo ? C'est juste un bitstream au niveau de la couche de sécurité des données. Le fait qu'il s'agisse d'une vidéo/de données génomiques/d'images/de formats propriétaires, etc. ne devrait pas être un problème de couche de sécurité. Une solution globale ne doit pas mélanger les responsabilités principales.

Nonce

Le NIST autorise les IV aléatoires pour une longueur supérieure à 96 bits. Voir la section 8.2.2 au NIST 800-38D . Rien de nouveau ici, les exigences nonce viennent du mode CTR. Ce qui est également assez standard dans la plupart des chiffrements de flux . Je ne comprends pas la peur soudaine envers les nonces - ça a toujours été number used once . Pourtant, alors que le débat INonce crée une interface maladroite, au moins, il n'élimine pas l'innovation comme l'imposition sans flux pour vous. Je concéderai à INonce n'importe quel jour si nous pouvons obtenir les innovations AEAD en matière de sécurité et de charge de travail en continu. Je déteste appeler quelque chose de basique comme le streaming une innovation - mais c'est là que je crains que nous ne régressions.

J'aimerais qu'on me prouve que j'ai tort

Je suis juste un gars qui, après une longue journée de travail, a renoncé à une soirée cinéma avec mes enfants pour taper ça. Je suis fatigué et je peux me tromper. Mais au moins, ayez un dialogue communautaire ouvert basé sur des faits plutôt que des anecdotes ou des "raisons de comité" ou un autre vaudou. Je suis dans le domaine de la promotion des innovations sécurisées .NET et Azure. Je pense que nous avons des objectifs alignés.

En parlant de dialogue communautaire...

Pouvons-nous s'il vous plaît avoir un appel Skype communautaire ? Exprimer un sujet complexe comme celui-ci souffle dans un mur de texte géant. Jolie s'il-vous-plaît?

S'il vous plaît, ne faites pas d'appel Skype - c'est la définition même d'une "réunion à huis clos", sans aucun enregistrement disponible pour la communauté. Les problèmes Github sont le bon moyen pour toutes les parties d'avoir un discours civil documenté (en ignorant les précédents de suppression des commentaires MS).

MS Crypto Review Board a probablement également passé un appel Skype. Ce n'est pas la faute des gens de MS qui participent à ce fil - ils ont probablement un accès très limité et un pouvoir de persuasion sur les tours d'ivoire du MS Crypto Review Board (quel qu'il soit).

Concernant le streaming AEAD :

Le _chiffrement_ de streaming de taille octet est possible pour les modes MAC-last comme GCM, CTR + HMAC, mais pas possible pour les modes MAC-first comme CCM. Le _decryption_ de streaming de taille octet fuit fondamentalement et n'est donc pris en compte par personne. Le _cryptage_ de streaming de taille de bloc est également possible pour CBC+HMAC, mais cela ne change rien. C'est à dire. Les approches de taille d'octet ou de taille de bloc pour le streaming AEAD sont défectueuses.

Le _chiffrement_ et le _décryptage_ du streaming de taille de bloc fonctionnent très bien, mais ils ont 2 contraintes :

  • ils nécessitent une mise en mémoire tampon (au-delà de la taille du bloc). Cela peut être fait par la bibliothèque/API si la mise en mémoire tampon est contrôlée/plafonnée (ex. Inferno), ou laissée à la couche supérieure (couche appelante) pour s'en occuper. Quoi qu'il en soit fonctionne.

  • Le streaming fragmenté AEAD n'est pas standardisé. Ex. nacl-stream , Inferno, MS-propre DataProtection, faites votre propre.

Ceci n'est qu'un résumé de ce que tout le monde dans cette discussion sait déjà jusqu'à présent.

@sdrapkin , pour m'assurer que je comprends bien, êtes-vous d'accord avec cette API fournissant un cryptage en streaming, mais pas de décryptage en streaming ?

@sdrapkin eh bien, le brainstorming humain en temps réel est certainement bénéfique, les problèmes de tenue de registres peuvent être résolus avec les procès-verbaux de réunion. Pour en revenir au côté technique, bien que la segmentation fonctionne pour le décryptage en continu, ce n'est pas une primitive de sécurité de bas niveau. C'est un protocole personnalisé. Et un non standard comme vous l'avez noté.

@morganbr

_Êtes-vous d'accord avec cette API fournissant un cryptage en continu, mais pas de décryptage en continu ?_

Non, je ne suis pas. Si une telle API était disponible, il serait facile de créer un texte chiffré chiffré par flux d'une taille qu'aucun déchiffrement de tampon ne pourra déchiffrer (mémoire insuffisante).

^^^^ Ceci, il n'y a pas eu beaucoup d'accord jusqu'à présent, mais je pense que nous pouvons tous convenir quelle que soit la manière dont cela se passe, une API asymétrique serait un désastre. À la fois d'un "hé, ce sont les méthodes de décryptage de flux sur lesquelles je pensais m'appuyer parce qu'il y avait des méthodes de cryptage", et à cause des commentaires @sdrapkin ci-dessus.

@Drawaes D'accord. Une API enc/dec asymétrique serait affreuse.

Des mises à jour les amis ?

Apparemment, j'ai confondu quelques attaques.

Les faiblesses inhérentes aux chiffrements de flux (que sont AES-CTR et AES-GCM) permettent des attaques de texte chiffré choisies pour permettre une récupération arbitraire du texte en clair. La défense contre les attaques par texte chiffré choisi est l'authentification ; AES-GCM est donc immunisé ... à moins que vous ne fassiez un décryptage en continu et que vous puissiez identifier à partir d'observations de canaux secondaires ce qu'aurait été le texte en clair. Par exemple, si les données décryptées sont traitées en tant que XML, cela échouera très rapidement si des caractères autres que les espaces ou < se trouvent au début des données décryptées. C'est donc "le décryptage en continu réintroduit des problèmes avec la conception du chiffrement de flux" (qui, vous l'avez peut-être remarqué, .NET n'en a aucun).

Tout en cherchant d'où venait la récupération de clé, il y a des papiers comme Authentificationfaiblesses dans GCM (Ferguson/Microsoft) , mais celui-ci récupère la clé d'authentification basée sur des tailles de balises courtes (ce qui explique en partie pourquoi l'implémentation de Windows n'autorise que les balises 96 bits). J'ai probablement été informé d'autres vecteurs de récupération de clé d'authentification pour expliquer pourquoi le streaming GCM est dangereux.

Dans un commentaire précédent , @sdrapkin a noté que "le décryptage en streaming de taille octet fuit fondamentalement et n'est donc pris en compte par personne. ... Les approches de taille d'octet ou de taille de bloc pour le streaming AEAD sont défectueuses.". Cela, combiné au fait que CCM (et SIV) ne sont pas capables de chiffrer en streaming et que le commentaire serait bizarre d'avoir un streaming et pas l'autre, suggère que nous sommes de retour à la proposition d'avoir juste un chiffrement à un coup et décrypter.

Il semble donc que nous soyons de retour à ma dernière proposition d'API (https://github.com/dotnet/corefx/issues/23629#issuecomment-329202845). À moins qu'il y ait d'autres problèmes en suspens que j'ai réussi à oublier tout en prenant du temps.

Bienvenue à nouveau @bartonjs

Je vais dormir bientôt mais brièvement:

  1. Nous avons déjà confondu la conception du protocole avec la conception primitive sur ce fil. Je dirai simplement que les attaques par texte chiffré choisi sont un problème de conception de protocole, pas un problème primitif.

  2. Le décryptage AEAD en streaming vous permet au moins d'avoir la confidentialité, puis passe immédiatement à la confidentialité + authenticité au dernier octet. Sans prise en charge de la diffusion en continu sur AEAD (c'est-à-dire la confidentialité traditionnelle uniquement), vous limitez en permanence les gens à une assurance de confidentialité inférieure uniquement.

Si les mérites techniques sont insuffisants ou si vous êtes (à juste titre) sceptique quant à l'autorité de mes arguments, j'essaierai la voie de l'autorité extérieure. Vous devez savoir que votre implémentation sous-jacente actuelle prend en charge AEAD (y compris AES GCM) en mode streaming. Le système d'exploitation Windows ( bcrypt ) permet de diffuser GCM via les fonctions BCryptEncrypt ou BCryptDecrypt . Voir dwFlags ici. Ou un exemple de code utilisateur . Ou un wrapper CLR créé par Microsoft. Ou que la mise en œuvre a été certifiée NIST FIP-140-2 aussi récemment qu'au début de cette année. Ou que Microsoft et le NIST ont tous deux dépensé des ressources importantes autour de l'implémentation AES et l'ont certifié ici et ici . Et malgré tout cela, personne n'a blâmé les primitifs. Cela n'a aucun sens que .NET Core arrive soudainement et impose sa propre crypto-thèse pour édulcorer la puissante implémentation sous-jacente. Surtout lorsque le streaming et le one-shot peuvent être pris en charge simultanément, de manière très triviale.

Suite? Eh bien, ce qui précède est vrai pour OpenSSL, même avec leurs API evp "plus récentes".

Et c'est vrai pour BouncyCastle.

Et c'est vrai avec Java Cryptography Architecture.

à votre santé!
Sid

@sidshetye ++10 si le cryptoboard est si inquiet, pourquoi laissent-ils Windows CNG faire cela?

Si vous vérifiez la validation AES NIST FIPS-140-2 de Microsoft (ex. # 4064 ), vous remarquerez ce qui suit :

AES-GCM :

  • Longueurs du texte brut : 0, 8, 1016, 1024
  • Longueurs AAD : 0, 8, 1016, 1024

AES-CCM :

  • Longueur du texte brut : 0-32
  • Longueur AAD : 0-65536

Il n'y a pas de validation pour le streaming. Je ne sais même pas si le NIST vérifie cet ex. L'implémentation AES-GCM ne devrait pas être autorisée à chiffrer plus de 64 Go de texte en clair (une autre limitation ridicule de GCM).

Je ne suis pas massivement attaché au streaming car mon utilisation ne devrait pas dépasser 16k, mais des tampons fragmentés seraient bien et ne devraient poser aucun risque (je soupçonne en fait que cng a créé son interface telle qu'elle est exactement dans ce but) ... par exemple, je veux pouvoir transmettre un certain nombre de plages ou similaire (liste chaînée par exemple) et le décrypter en une seule fois. S'il déchiffre dans un tampon contigu, tout va bien.

Donc, je suppose que déplacer la carte de cryptage sombre sur l'API "style streaming" n'est pas envisageable pour l'instant, alors allons de l'avant pour créer une API unique. Il est toujours possible d'étendre une API SI suffisamment de personnes montrent un besoin plus tard

@sdrapkin le fait est que c'est l'API de streaming qui a fait l'objet d'un examen approfondi par NIST Labs et MSFT. Chaque version validée coûte entre 80 000 $ et 50 000 $ et MSFT (et OpenSSL et Oracle et d'autres poids lourds de la cryptographie) ont investi ÉNORMÉMENT pour faire valider ces API et ces implémentations depuis plus de 10 ans. Ne nous laissons pas distraire par les tailles de texte brut spécifiques du plan de test, car je suis convaincu que .NET prendra en charge des tailles autres que 0, 8, 1016, 1024, quel que soit le streaming ou le one-shot. Le fait est que toutes ces API testées au combat (littéralement; sur les systèmes de support d'armes), sur toutes ces plates-formes prennent en charge le streaming AEAD au niveau de l'API crypto-primitive. Malheureusement, jusqu'à présent, tous les arguments contre cela ont été une préoccupation au niveau de l'application ou du protocole citée comme une pseudo-préoccupation au niveau crypto primitif.

Je suis tout à fait pour "laisser la meilleure idée gagner", mais à moins que l'équipe de cryptographie .net core (MSFT ou communauté) n'ait une découverte révolutionnaire, je ne vois tout simplement pas comment tout le monde fait de la crypto jusqu'à présent, de toutes les différentes organisations ont tort et ils ont raison.

PS : Je sais que nous sommes en désaccord ici, mais nous voulons tous ce qu'il y a de mieux pour la plateforme et ses clients.

@Drawaes à moins que l'interface AEAD (pas nécessairement l'implémentation) définie aujourd'hui ne prenne en charge une surface d'API de streaming, je ne vois pas comment les gens peuvent l'étendre sans avoir deux interfaces ou des interfaces personnalisées. Ce serait un désastre. J'espère que cette discussion mènera à une interface qui est à l'épreuve du temps (ou du moins, qui reflète d'autres interfaces AEAD qui existent depuis de nombreuses années !).

J'ai tendance à être d'accord. Mais ce problème ne va nulle part rapidement et lorsque cela se produit, nous risquons d'atteindre un point critique, soit il ne le fera pas pour 2.1, soit il devra être résolu sans avoir le temps de résoudre les problèmes. Je vais être honnête, je suis retourné à mes anciens emballages et je les réorganise juste pour 2.0 ;)

Nous avons quelques API de référence pour Java , OpenSSL ou C# Bouncy Castle ou CLR Security . Franchement, n'importe lequel d'entre eux fera l'affaire et à long terme, je souhaite que C # ait quelque chose comme "l'architecture de cryptographie Java" de Java où toutes les implémentations de chiffrement sont contre une interface bien établie permettant d'échanger des bibliothèques de chiffrement sans affecter le code utilisateur.

De retour ici, je pense qu'il est préférable d'étendre l'interface ICryptoTransform .NET Core comme

public interface IAuthenticatedCryptoTransform : ICryptoTransform 
{
    bool CanChainBlocks { get; }
    byte[] GetTag();
    void SetExpectedTag(byte[] tag);
}

Si nous Span ifions tous les byte[] s, cela devrait imprégner toute l'API dans l'espace de noms System.Security.Cryptography pour une cohérence globale.

Edit : Liens JCA fixes

Si nous étendons tous les octets [], cela devrait imprégner l'intégralité de l'API dans l'espace de noms System.Security.Cryptography pour une cohérence globale.

Nous l'avons déjà fait. Tout sauf ICryptoTransform, car nous ne pouvons pas changer d'interface.

Je pense qu'il est préférable d'étendre ICryptoTransform de .NET Core ...

Le problème avec ceci est que le modèle d'appel est très gênant pour sortir la balise à la fin (en particulier si CryptoStream est impliqué). J'ai écrit ça à l'origine, et c'était moche. Il y a aussi le problème de savoir comment en obtenir un, puisque les paramètres GCM sont différents des paramètres CBC/ECB.

Alors, voici mes pensées.

  • Le décryptage en continu est dangereux pour AE.
  • En général, je suis fan du "donnez-moi le primitif, et laissez-moi gérer mon risque"
  • Je suis aussi fan de ".NET ne devrait pas (facilement) autoriser des choses complètement dangereuses, car c'est une partie de sa proposition de valeur"
  • Si, comme je l'avais mal compris à l'origine, les risques de mal déchiffrer GCM étaient la récupération de la clé d'entrée, alors je serais toujours à "c'est trop dangereux". (La différence entre .NET et tout le reste serait "ayant pris plus de temps pour le faire, le monde en a appris plus")
  • Mais, puisque ce n'est pas le cas, si vous voulez vraiment que les roues d'entraînement se détachent, alors je suppose que je vais entretenir cette idée.

Mes pensées assez brutes à cette fin (ajoutant aux suggestions existantes, donc le one-shot reste, bien que je suppose comme un impl virtuel par défaut au lieu d'un résumé):

```C#
classe partielle AuthenticatedEncryptor
{
// lance si une opération est déjà en cours
public abstract void Initialize(ReadOnlySpanDonnées associées );
// vrai en cas de succès, faux en cas de "destination trop petite", exception sur tout le reste.
public abstract bool TryEncrypt(ReadOnlySpandonnées, étendueencryptedData, out int bytesRead, out int bytesWritten);
// false si leftEncryptedData est trop petit, lève si les autres entrées sont trop petites, voir les propriétés NonceOrIVSizeInBits et TagSizeInBits.
// NonceOrIvUsed pourrait passer à Initialize, mais il pourrait alors être interprété comme une entrée.
public abstract bool TryFinish(ReadOnlySpandonnées restantes, étenduerestantEncryptedData, out int bytesWritten, Spanbalise, portéenonceOrIvUsed);
}

classe partielle AuthenticatedDecryptor
{
// lance si une opération est déjà en cours
public abstract void Initialize(ReadOnlySpanbalise, ReadOnlySpannonceOrIv, ReadOnlySpanDonnées associées );
// vrai en cas de succès, faux en cas de "destination trop petite", exception sur tout le reste.
public abstract bool TryDecrypt(ReadOnlySpandonnées, étenduedecryptedData, out int bytesRead, out int bytesWritten);
// lance une balise incorrecte, mais peut de toute façon divulguer les données.
// (remainingDecryptedData est requis pour CBC + HMAC, et peut donc aussi bien ajouter des données restantes, je suppose ?)
public abstract bool TryFinish(ReadOnlySpandonnées restantes, étenduerestantDecryptedData, out int bytesWritten);
}
```

AssociatedData vient à Initialize, car les algorithmes qui en ont besoin en dernier peuvent le conserver, et les algorithmes qui en ont besoin en premier ne peuvent pas l'avoir autrement.

Une fois qu'une forme est décidée pour ce à quoi ressemblerait le streaming (et si les gens pensent que CCM devrait mettre en mémoire tampon interne, ou devrait lancer, en mode de cryptage de streaming), je reviendrai au tableau.

@bartonjs Je sais ce que vous voulez dire par arracher et programmer la balise à partir de la fin du flux pour une symétrie entre chiffrement/déchiffrement. C'est délicat mais pire si c'est à chaque utilisateur de résoudre. J'ai une implémentation que je peux partager sous MIT ; devra regarder en interne avec mon équipe (pas à mon bureau/mobile)

Un terrain d'entente pourrait être comme OpenSSL ou le bcrypt de NT où vous devez brancher la balise juste avant l'appel de décryptage final puisque c'est à ce moment que les comparaisons de balises se produisent. c'est-à-dire qu'un SetExpectedTag (avant le décryptage final) et GetTag (après le cryptage final) fonctionneraient mais déchargeraient la gestion des balises à l'utilisateur. La plupart ajouteront simplement la balise au flux de chiffrement puisque c'est l'ordre temporel naturel.

Je pense que s'attendre à ce que la balise dans Initialize elle-même (en déchiffrement) rompe la symétrie dans l'espace (flux d'octets) et le temps (vérification de la balise à la fin, pas au début), ce qui limite son utilité. Mais les API de balises ci-dessus résolvent ce problème.

Également pour le chiffrement, Initialize a besoin de l'IV avant toute transformation de chiffrement.

Enfin, pour chiffrer et déchiffrer, Initialize a besoin des clés de chiffrement AES avant toute transformation. (Il me manque quelque chose d'évident ou vous avez oublié de taper ce bit ?)

Je pense que s'attendre à ce que la balise dans Initialize lui-même (en déchiffrement) brise la symétrie

Dans CBC + HMAC, la recommandation habituelle est de vérifier le HMAC avant de commencer tout décryptage, il s'agit donc d'un algorithme de décryptage tag-first. De même, il pourrait y avoir un algorithme "pur AE" qui effectue des opérations destructives sur la balise pendant les calculs et vérifie simplement que la réponse finale était 0. Ainsi, comme la valeur de données associée, puisqu'il pourrait y avoir des algorithmes qui en ont besoin en premier, il a pour venir en premier dans une API entièrement généralisée.

Les faire flotter dans SetAssociatedData et SetTag pose le problème que si la classe de base était indépendante de l'algorithme, l'utilisation devient dépendante de l'algorithme. Changer AesGcm en AesCbcHmacSha256 ou SomeTagDesctructiveAlgorithm entraînerait désormais le lancement de TryDecrypt car la balise n'était pas encore fournie. Pour moi, c'est pire que de ne pas être polymorphe du tout, donc permettre la flexibilité suggère de séparer le modèle pour qu'il soit complètement isolé par algorithme. (Oui, il pourrait être contrôlé par plus de propriétés caractéristiques d'identification d'algorithme comme NeedsTagFirst , mais cela le rend vraiment plus difficile à utiliser)

Également pour le chiffrement, Initialize a besoin de l'IV avant toute transformation de chiffrement.

Enfin, pour chiffrer et déchiffrer, Initialize a besoin des clés de chiffrement AES avant toute transformation.

La clé était un paramètre de classe ctor. Le IV/nonce provient du fournisseur IV/nonce dans le paramètre ctor.

Le modèle de fournisseur résout SIV, où aucun IV n'est donné pendant le chiffrement, un est généré au nom des données. Sinon, SIV a le paramètre et exige qu'une valeur vide soit fournie.

ou vous avez oublié de taper ce bit?

Les méthodes de streaming ont été ajoutées à ma proposition existante, qui avait déjà la clé et le fournisseur IV/nonce comme paramètres ctor.

@bartonjs : Bon point que certains algos pourraient vouloir marquer en premier tandis que d'autres à la fin et merci pour le rappel qu'il s'agit d'un ajout à la spécification d'origine. J'ai trouvé que l'examen d'un cas d'utilisation facilite les choses, alors voici un exemple axé sur le cloud :

Nous allons effectuer des analyses sur un ou plusieurs fichiers cryptés AES-GCM de 10 Go (c'est-à-dire des balises après le texte chiffré) conservés en stockage. Un agent d'analyse déchiffre simultanément plusieurs flux entrants dans des machines/clusters distincts et, après les vérifications du dernier octet + balise, démarre chaque charge de travail d'analyse. Toutes les machines virtuelles de stockage, de travail et d'analyse se trouvent dans Azure US-West.

Ici, il n'y a aucun moyen de récupérer la balise à la fin de chaque flux et de la fournir à la méthode Initialize de AuthenticatedDecryptor. Ainsi, même si un utilisateur se porte volontaire pour modifier le code pour l'utilisation de GCM, il ne peut même pas commencer à utiliser l'API.

À bien y penser, la seule façon dont nous pourrions avoir une API qui s'adapte à divers AEAD ET n'a pas de changement de code utilisateur, c'est si les fournisseurs de chiffrement pour différents algorithmes AEAD gèrent automatiquement les balises. Pour ce faire, Java place les balises à la fin du texte chiffré pour GCM et les supprime lors du décryptage sans intervention de l'utilisateur. En dehors de cela, chaque fois que quelqu'un modifie l'algorithme de manière significative (par exemple, CBC-HMAC => GCM), il devra modifier son code en raison de la nature mutuellement exclusive du traitement tag-first et tag-last.

À mon humble avis, nous devrions d'abord décider si

Option 1) Les fournisseurs d'algorithmes gèrent en interne la gestion des balises (comme Java)

ou

Option 2) Exposez suffisamment sur l'API pour que les utilisateurs le fassent eux-mêmes (comme WinNT bcrypt ou openssl)

L'option 1 simplifierait vraiment l'expérience globale des utilisateurs de bibliothèques car la gestion des tampons peut devenir complexe. Résolvez-le bien dans la bibliothèque et chaque utilisateur n'aura plus à le résoudre à chaque fois maintenant. De plus, tous les AEAD ont la même interface (tag-first, tag-last, tag-less) et l'échange d'algorithmes est également plus simple.

Mon vote serait pour l'option 1.

Enfin, nous avons pu déterrer notre implémentation permettant ICryptoTransform opérations de streaming sur GCM d'extraire automatiquement la balise in-stream source . Il s'agissait d'une mise à jour importante du propre wrapper de CLR Security et malgré les copies de tampon supplémentaires, il est toujours très rapide (~ 4 Go / s sur notre test macbook pro dans Windows 10 bootcamp). Nous avons essentiellement intégré la sécurité CLR pour créer l'option 1 pour nous-mêmes afin que nous n'ayons pas besoin de le faire partout ailleurs. Ce visuel aide vraiment à expliquer ce qui se passe dans les TransformBlock et TransformFinalBlock de l'interface ICryptoTransform .

@sidshetye Je ne sais pas pourquoi votre exemple cloud-first est bloqué. Si vous lisez à partir du stockage, vous pouvez d'abord télécharger les derniers octets de balise et les fournir au décrypteur. Si vous utilisez les API de stockage Azure, cela se fera via CloudBlockBlob.DownloadRangeXxx .

@GrabYourPitchforks Ne pas trop s'égarer sur cet exemple, mais c'est une capacité spécifique d'Azure Blob Storage. En général, les charges de travail de stockage basées sur des machines virtuelles (IaaS) ou de stockage non Azure reçoivent généralement un flux réseau qui n'est pas recherchable.

Personnellement, je suis très excité de voir @GrabYourPitchforks - yay !

Nous allons effectuer des analyses sur un ou plusieurs fichiers cryptés AES-GCM de 10 Go (c'est-à-dire des balises après le texte chiffré) conservés en stockage. Un agent d'analyse déchiffre simultanément plusieurs flux entrants dans des machines/clusters distincts et, après les vérifications du dernier octet + balise, démarre chaque charge de travail d'analyse. Toutes les machines virtuelles de stockage, de travail et d'analyse se trouvent dans Azure US-West.

@sidshetye , vous étiez si catégorique sur la séparation des primitives stupides et dangereuses et des protocoles intelligents et câlins ! J'ai fait un rêve - et j'y ai cru. Et puis tu nous lances ça. Ceci est un protocole - une conception de système. Celui qui a conçu ce protocole que vous avez décrit - a foiré. Il ne sert à rien de pleurer sur l'incapacité d'insérer une cheville carrée dans un trou rond maintenant.

Quiconque a des fichiers 10 Go cryptés par GCM vit non seulement dangereusement près du bord primitif (GCM n'est pas bon après 64 Go), mais il y avait aussi une affirmation implicite que l'ensemble du texte chiffré devra être mis en mémoire tampon.

Celui qui chiffre les fichiers de 10 Go avec GCM commet une erreur de protocole avec une probabilité écrasante. La solution : le chiffrement fragmenté. TLS a une segmentation limitée à 16k de longueur variable, et il existe d'autres versions plus simples et sans PKI. Le sex-appeal "cloud-first" de cet exemple hypothétique ne diminue pas les erreurs de conception.

(J'ai beaucoup de rattrapage à faire sur ce fil.)

@sdrapkin a soulevé un point sur la réutilisation de l'interface IAuthenticatedEncryptor de la couche de protection des données. Pour être honnête, je ne pense pas que ce soit la bonne abstraction pour une primitive, car la couche de protection des données est assez opiniâtre dans la façon dont elle effectue la cryptographie. Par exemple, il interdit l'auto-sélection d'un IV ou d'un nonce, il exige qu'une implémentation conforme comprenne le concept d'AAD, et il produit un résultat quelque peu propriétaire. Dans le cas d'AES-GCM, la valeur de retour de IAuthenticatedEncryptor.Encrypt est la concaténation d'une chose étrange presque nonce utilisée pour la dérivation de sous-clé, le texte chiffré résultant de l'exécution d'AES-GCM sur le texte en clair fourni (mais pas le AAD !), et la balise AES-GCM. Ainsi, bien que chaque étape impliquée dans la génération de la charge utile protégée soit sécurisée, la charge utile elle-même ne suit aucun type de convention acceptée, et vous ne trouverez personne en dehors de la bibliothèque de protection des données qui puisse déchiffrer avec succès le texte chiffré résultant. Cela en fait un bon candidat pour une bibliothèque destinée aux développeurs d'applications, mais un horrible candidat pour une interface à implémenter par des primitives.

Je dois également dire que je ne vois pas l'intérêt d'avoir une One True Interface(tm) IAuthenticatedEncryptionAlgorithm que tous les algorithmes de chiffrement authentifiés sont censés implémenter. Ces primitives sont "complexes", contrairement aux simples primitives de chiffrement par blocs ou aux primitives de hachage. Il y a tout simplement trop de variables dans ces primitives complexes. L'AE primitif est-il uniquement, ou est-ce l'AEAD ? L'algorithme accepte-t-il un IV / nonce du tout ? (J'en ai vu qui ne le font pas.) Y a-t-il des inquiétudes quant à la façon dont l'entrée IV/nonce ou les données doivent être structurées ? IMO, les primitives complexes devraient simplement être des API autonomes, et les bibliothèques de niveau supérieur prendraient en charge les primitives complexes spécifiques dont elles se soucient. Ensuite, la bibliothèque de niveau supérieur expose l'API uniforme qu'elle juge appropriée pour ses scénarios.

@sdrapkin On s'éloigne encore du sujet. Je dirai simplement qu'un système est construit à l'aide de primitives. Les primitives crypto ici sont nues et puissantes. Alors que la couche système/protocole gérait la mise en mémoire tampon ; cela aussi au niveau du cluster, certainement pas dans la mémoire système principale que les primitives à un coup forceraient. la limite de 'chunking' est X (X = 10 Go ici) car < 64 Go, car la capacité de mise en mémoire tampon du cluster était presque illimitée et rien ne pouvait / ne pouvait démarrer tant que le dernier octet n'était pas chargé dans le cluster. C'est exactement la séparation des préoccupations, optimisant chaque couche pour ses points forts dont j'ai parlé. Et cela ne peut se produire que si les primitives sous-jacentes ne handicapent pas les conceptions/limitations des couches supérieures (notez que davantage d'applications du monde réel sont livrées avec leurs propres handicaps hérités).

NIST 800-38d sec9.1 stipule :

Afin d'empêcher une partie non autorisée de contrôler ou d'influencer la génération d'IV,
GCM doit être mis en œuvre uniquement dans un module cryptographique qui répond aux exigences de
Pub FIPS. 140-2. En particulier, la frontière cryptographique du module doit contenir une
"unité de génération" qui produit des IV selon l'une des constructions de la Sec. 8.2 ci-dessus.
La documentation du module pour sa validation par rapport aux exigences de FIPS 140-2 doit
décrire comment le module est conforme à l'exigence d'unicité sur les IV.

Cela implique pour moi que les GCM IV doivent être générés automatiquement en interne (et non transmis en externe).

@sdrapkin Bon point mais si vous lisez encore plus près, vous verrez que pour des longueurs IV de 96 bits et plus, la section 8.2.2 permet de générer un IV avec un générateur de bits aléatoires (RBG) où au moins 96 bits sont aléatoires (vous pourrait juste 0 autres bits). Je l'ai mentionné le mois dernier sur ce fil lui-même ( ici sous nonce).

LT;DR : INonce est un piège menant au non-respect des directives NIST et FIPS.

La section 9.1 indique simplement que, pour FIPS 140-2, l'unité de génération IV (entièrement aléatoire, c'est-à-dire sec 8.2.2 ou implémentation déterministe, c'est-à-dire sec 8.2.1) doit se trouver dans la limite du module soumis à la validation FIPS. Puisque ...

  1. Les RBG sont déjà validés FIPS
  2. Une lentille IV >= 96 est recommandée
  3. concevoir une unité de génération IV qui persiste à redémarrer, une perte de puissance indéfinie dans une couche crypto primitive est difficile
  4. obtenir 3 ci-dessus implémentés dans la bibliothèque de chiffrement ET l'obtenir certifié est difficile et coûteux (50 000 $ pour tout ce qui entraîne une image de construction non exacte)
  5. Aucun code utilisateur n'implémentera jamais 3 et ne le fera certifier en raison de 4 ci-dessus. (laissons de côté certaines installations militaires/gouvernementales exotiques).

... la plupart des bibliothèques de chiffrement (voir Java d'Oracle, bcryptprimitives de WinNT, OpenSSL, etc.) en cours de certification FIPS utilisent la route RBG pour IV et prennent simplement un tableau d'octets en entrée. Notez que le fait d'avoir l'interface INonce est en fait un piège du point de vue du NIST et du FIPS, car cela suggère implicitement qu'un utilisateur doit transmettre une implémentation de cette interface à la fonction de chiffrement. Mais toute implémentation utilisateur de INonce est presque garantie de ne PAS avoir subi le processus de certification NIST de 9 mois + et 50 000 $ +. Pourtant, s'ils venaient d'envoyer un tableau d'octets en utilisant la construction RVB (déjà dans la bibliothèque de chiffrement), ils seraient entièrement conformes aux directives.

Je l'ai déjà dit - ces bibliothèques de chiffrement existantes ont fait évoluer leur surface d'API et ont été testées au combat dans plusieurs scénarios. Plus que ce que nous avons abordé dans ce long fil. Mon vote est à nouveau de tirer parti de ces connaissances et de cette expérience dans toutes ces bibliothèques, toutes ces validations et toutes ces installations plutôt que d'essayer de réinventer la roue. Ne réinventez pas la roue. Utilisez-le pour inventer la fusée :)

Salut les gens,

Des mises à jour à ce sujet ? Je n'ai vu aucune mise à jour sur le fil de la feuille de route cryptographique de @ karelz ou sur le fil AES GCM .

Merci
Sid

La dernière proposition concrète provient donc de https://github.com/dotnet/corefx/issues/23629#issuecomment -334328439 :

partial class AuthenticatedEncryptor
{
    // throws if an operation is already in progress
    public abstract void Initialize(ReadOnlySpan<byte> associatedData);
    // true on success, false on “destination too small”, exception on anything else.
    public abstract bool TryEncrypt(ReadOnlySpan<byte> data, Span<byte> encryptedData, out int bytesRead, out int bytesWritten);
    // false if remainingEncryptedData is too small, throws if other inputs are too small, see NonceOrIVSizeInBits and TagSizeInBits properties.
    // NonceOrIvUsed could move to Initialize, but then it might be interpreted as an input.
    public abstract bool TryFinish(ReadOnlySpan<byte> remainingData, Span<byte> remainingEncryptedData, out int bytesWritten, Span<byte> tag, Span<byte> nonceOrIvUsed);
}

partial class AuthenticatedDecryptor 
{
    // throws if an operation is already in progress
    public abstract void Initialize(ReadOnlySpan<byte> tag, ReadOnlySpan<byte> nonceOrIv, ReadOnlySpan<byte> associatedData);
    // true on success, false on “destination too small”, exception on anything else.
    public abstract bool TryDecrypt(ReadOnlySpan<byte> data, Span<byte> decryptedData, out int bytesRead, out int bytesWritten);
    // throws on bad tag, but might leak the data anyways.
    // (remainingDecryptedData is required for CBC+HMAC, and so may as well add remainingData, I guess?)
    public abstract bool TryFinish(ReadOnlySpan<byte> remainingData, Span<byte> remainingDecryptedData, out int bytesWritten);
}

Seuls quelques problèmes potentiels ont été soulevés depuis :

  • La balise est requise au départ, ce qui entrave certains scénarios. Soit l'API doit devenir beaucoup plus complexe pour permettre une plus grande flexibilité, soit ce problème doit être considéré comme un problème de protocole (c'est-à-dire de haut niveau).
  • INonceProvider peut être inutilement complexe et/ou entraîner une non-conformité aux directives du NIST et de la FIPS.
  • L'abstraction prévue des primitives de chiffrement authentifiées pourrait être une chimère, car les différences pourraient être trop importantes. Il n'y a pas eu d'autres discussions sur cette suggestion.

J'aimerais suggérer ce qui suit :

  1. La complexité supplémentaire de ne pas exiger la balise dès le départ semble sévère, le scénario de problème correspondant semble peu courant, et le problème ressemble en effet beaucoup à une question de protocole. Un bon design peut contenir beaucoup de choses, mais pas tout. Personnellement, je me sens à l'aise de laisser cela au protocole. (Des contre-exemples forts sont les bienvenus.)
  2. La discussion s'est constamment orientée vers une implémentation flexible et de bas niveau qui ne protège pas contre les abus, à l'exception de la génération IV . Soyons cohérents. Le consensus général semble être qu'une API de haut niveau est une prochaine étape importante, vitale pour une utilisation correcte par la majorité des développeurs - c'est ainsi que nous nous en sortons sans nous protéger contre les abus dans l'API de bas niveau . Mais il semble qu'une dose supplémentaire de peur ait soutenu l'idée d'une prévention des abus _dans le domaine de la génération IV_. Dans le cadre d'une API de bas niveau, et pour être cohérent, je pencherais plutôt pour un équivalent byte[] . Mais l'échange d'implémentation est plus transparent avec le INonceProvider injecté. Le commentaire de @sidshetye est-il irréfutable, ou une simple implémentation INonceProvider qui appelle simplement le RNG peut-elle toujours être considérée comme conforme ?
  3. Les abstractions semblent utiles, et tant d'efforts ont été déployés pour les concevoir, que je suis maintenant convaincu qu'elles feront plus de bien que de mal. En outre, les API de haut niveau peuvent toujours choisir d'implémenter des API de bas niveau qui ne sont pas conformes aux abstractions de bas niveau.
  4. IV est le terme général, et un nonce est un type spécifique de IV, n'est-ce pas ? Cela demande des renommages de INonceProvider à IIVProvider , et de nonceOrIv* à iv* . Après tout, nous avons toujours affaire à un IV, mais pas nécessairement à un nonce.

La balise initiale n'est pas un démarreur pour mon scénario, je vais donc probablement garder ma propre implémentation. Ce qui est bien, je ne suis pas sûr que ce soit la tasse de thé de tout le monde pour écrire du code de haute performance dans ce domaine.

Le problème est que cela entraînera une latence inutile. Vous devez pré-mettre en mémoire tampon un message entier pour obtenir la balise à la fin pour commencer à décoder la trame. Cela signifie que vous ne pouvez pas chevaucher IO et décryptage.

Je ne sais pas pourquoi il est si difficile de l'autoriser à la fin. Mais je ne vais pas bloquer la route pour cette API, cela n'aura tout simplement aucun intérêt dans mon scénario.

IV est le terme général, et un nonce est un type spécifique de IV, n'est-ce pas ?

Non. Un nonce est un nombre utilisé une fois . Un algorithme qui spécifie un nonce indique que la réutilisation viole les garanties de l'algorithme. Dans le cas de GCM, l'utilisation du même nonce avec la même clé et un message différent peut entraîner la compromission de la clé GHASH, réduisant GCM à CTR.

De http://nvlpubs.nist.gov/nistpubs/ir/2013/NIST.IR.7298r2.pdf :

Nonce : une valeur utilisée dans les protocoles de sécurité qui n'est jamais répétée avec la même clé. Par exemple, les nonces utilisés comme défis dans les protocoles d'authentification défi-réponse ne doivent généralement pas être répétés jusqu'à ce que les clés d'authentification soient modifiées. Sinon, il y a une possibilité d'attaque de rejeu. L'utilisation d'un nonce comme défi est une exigence différente d'un défi aléatoire, car un nonce n'est pas nécessairement imprévisible.

Un "IV" n'a pas les mêmes exigences strictes. Par exemple, la répétition d'un IV avec CBC ne fuit que si le message crypté est identique ou différent d'un précédent avec le même IV. Cela n'affaiblit pas l'algorithme.

Un nonce est un nombre utilisé une fois.
Un "IV" n'a pas les mêmes exigences strictes.

@bartonjs Oui. Je dirais que, puisqu'un nonce est utilisé pour initialiser la crypto primitive, c'est son vecteur d'initialisation. Il adhère parfaitement à toute définition de IV que je peux trouver. Il a des exigences plus strictes, oui, tout comme être une vache a des exigences plus strictes qu'être un animal. La formulation actuelle semble demander un paramètre "cowOrAnimal". Le fait que différents modes aient des exigences variables de l'IV ne change pas le fait qu'ils demandent tous une forme d'IV. S'il y a quelque chose qui me manque, gardez bien sûr le libellé actuel, mais pour autant que je sache, seuls "iv" ou "IVVProvider" sont à la fois simples et corrects.

Pour vous adonner au bikeshedding nonceOrIv :

Le GCM IV 96 bits est parfois défini comme un salt nonce octets (ex. RFC 5288). RFC 4106 définit GCM nonce comme salt à 4 octets et $# iv salt à 8 octets. RFC 5084 (GCM dans CMS) dit que CCM prend un nonce , GCM prend un iv , mais _"... pour avoir un ensemble commun de termes pour AES-CCM et AES-GCM , l'AES-GCM IV est appelé un nonce dans le reste de ce document."_ RFC 5647 (GCM pour SSH) dit _"remarque : dans [RFC5116], l'IV est appelé le nonce."_ RFC 4543 ( GCM dans IPSec) dit _"nous nous référons à l'entrée AES-GMAC IV comme un nonce, afin de la distinguer des champs IV dans les paquets."_ RFC 7714 (GCM pour SRTP) parle d'un IV 12 octets

Étant donné le manque total de cohérence dans la plupart des spécifications GCM, nonceOrIv a un peu de sens. 0,02 $

Tag upfront est un non-démarreur

Comme d'autres clients qui s'expriment ici, exiger la balise à l'avance est également un non-démarrage pour nous. Il n'y a aucun moyen pour .NET de traiter ensuite des flux simultanés avec cette limitation artificiellement introduite. Tue totalement l'évolutivité.

Pouvez-vous soutenir l'affirmation selon laquelle cela ajoute de la complexité? Parce que cela devrait en fait être trivial. De plus, aucune des implémentations de chiffrement spécifiques à la plate-forme (que vous allez envelopper) n'a cette limitation. Plus précisément, la raison en est que la balise d'entrée doit être simplement comparée en temps constant à la balise calculée. Et la balise calculée n'est disponible qu'après le décryptage du dernier bloc pendant TryFinish . Donc, essentiellement, lorsque vous démarrez votre implémentation, vous constaterez que vous stockez simplement le tag dans votre instance jusqu'au TryFinish . Vous pourriez très bien l'avoir comme entrée facultative

public abstract bool TryFinish(ReadOnlySpan<byte> remainingData, 
                             Span<byte> remainingDecryptedData, 
                             out int bytesWritten, 
                             ReadOnlySpan<byte> tag = null); // <==

Je pense aussi que nous essayons trop de nous normaliser en une seule interface qui couvrira tous les scénarios de cryptographie. Moi aussi, je préfère les interfaces généralisées, mais jamais au détriment de la fonctionnalité ou de l'évolutivité - en particulier à une couche aussi fondamentale que la bibliothèque cryto standard du langage lui-même. À mon humble avis, si l'on se retrouve à le faire, cela signifie généralement que l'abstraction est défectueuse.

Si une interface simple et cohérente est nécessaire, je préfère l'approche Java - également évoquée précédemment ici en tant qu'option 1 . Il évite également le problème ci-dessus de balise en premier/balise en dernier en les gardant dans les implémentations d'algorithmes (à mon humble avis, comme je pense qu'il le devrait). Mon équipe ne met pas cela en œuvre, donc ce n'est pas notre décision MAIS si nous devions prendre une décision et commencer à mettre en œuvre - nous emprunterions cette voie à coup sûr.

Veuillez éviter l'interface INonce , un simple byte[] ou span<> devrait suffire pour une interface de bas niveau conforme.

IV vs Nonce - Le cas généralisé est en effet IV. Pour GCM, l'IV doit être un Nonce (par exemple, Car vs RedOrCar). Et comme je copie-colle ceci, je viens de remarquer que @timovzl a utilisé un exemple très similaire :)

@sidshetye Pouvez-vous faire une proposition précise selon laquelle (1) prend en charge les algorithmes qui ont besoin de la balise à l'avance, et (2) n'a besoin de la balise que jusqu'à TryFinish dans toutes les autres situations ?

Je suppose que vous pensez quelque chose dans la direction suivante?

  • La balise dans Initialize peut être nulle. Seuls les algorithmes qui en ont besoin au départ lanceront null.
  • La balise dans TryFinish est obligatoire, ou (alternativement) est autorisée à être nulle pour les algorithmes qui l'ont déjà requise au départ.

Je suppose que ce qui précède ne fait qu'ajouter de la complexité sous la forme de documentation et de savoir-faire. Pour une API de bas niveau, cela pourrait être considéré comme un petit sacrifice, car une documentation et un savoir-faire adéquats sont de toute façon nécessaires.

Je commence à être convaincu que cela devrait être possible, pour la compatibilité avec d'autres implémentations, et le streaming.

@timovzl Bien sûr, j'espère prévoir un peu de temps demain pour cela.

@Timovzl , j'ai fini par avoir du temps juste aujourd'hui et cela s'est avéré être un trou de lapin ! C'est long, mais je pense qu'il capture la plupart des cas d'utilisation, capture les points forts de .NET crypto ( ICryptoTransform ) tout en adoptant la direction .NET Core/Standard ( Span<> ). J'ai relu mais j'espère qu'il n'y a pas de fautes de frappe ci-dessous. Je pense aussi que certaines communications en temps réel (chat, appel de conf, etc.) sont vitales pour un brainstorming rapide ; J'espère que vous pourrez y réfléchir.

Modèle de programmation

Je parlerai d'abord du modèle de programmation résultant pour les utilisateurs de l'API.

Crypter la diffusion en continu

var aesGcm = new AesGcm();
using (var encryptor = aesGcm.CreateAuthenticatedEncryptor(Key, IV, AAD))
{
  using (var cryptoOutStream = new CryptoStream(cipherOutStream, encryptor, CryptoStreamMode.Write))
  {
    clearInStream.CopyTo(cryptoOutStream);
  }
}

Décryptage en continu

var aesGcm = new AesGcm();
using (var decryptor = aesGcm.CreateAuthenticatedDecryptor(Key, IV, AAD))
{
  using (var decryptStream = new CryptoStream(cipherInStream, encryptor, CryptoStreamMode.Write))
  {
    decryptStream.CopyTo(clearOutStream);
  }
}

Non-streaming

Étant donné que le non-streaming est un cas particulier de streaming, nous pouvons encapsuler le code utilisateur ci-dessus dans des méthodes d'assistance sur AuthenticatedSymmetricAlgorithm (définies ci-dessous) pour exposer une API plus simple. c'est à dire

public class AesGcm : AuthenticatedSymmetricAlgorithm
{
  ...
  // These return only after consuming entire input buffer

  // Code like Streaming Encrypt from above within here
​  public abstract bool TryEncrypt(ReadOnlySpan<byte> clearData, Span<byte> encryptedData);

  // Code like Streaming Decrypt from above within here
  public abstract bool TryDecrypt(ReadOnlySpan<byte> encryptedData, Span<byte> clearData); 
  ...
}

Cela peut doubler en présentant une API plus simple comme

Cryptage sans streaming

var aesGcm = new AesGcm(Key, IV, AAD);
aesGcm.TryEncrypt(clearData, encryptedData);
var tag = aesGcm.Tag;

Décryptage non-streaming

var aesGcm = new AesGcm(Key, IV, AAD);
aesGcm.Tag = tag;
aesGcm.TryDecrypt(encryptedData, clearData);

Sous la capuche

En regardant la source corefx, Span<> est partout. Cela inclut System.Security.Cryptography.* - à l'exception des chiffrements symétriques, corrigeons donc ce chiffrement authentifié en premier et en couche.

1. Créez ICipherTransform pour Span I/O

C'est comme une version compatible Span de ICryptoTransform . Je changerais simplement l'interface elle-même dans le cadre de la mise à niveau du framework, mais comme les gens peuvent être sensibles à ce sujet, je l'appelle ICipherTransform .

public partial interface ICipherTransform : System.IDisposable
{
  bool CanReuseTransform { get; }
  bool CanTransformMultipleBlocks { get; } // multiple blocks in single call?
  bool CanChainBlocks { get; }             // multiple blocks across Transform/TransformFinal
  int InputBlockSize { get; }
  int OutputBlockSize { get; }
  int TransformBlock(ReadOnlySpan<byte> inputBuffer, int inputOffset, int inputCount, Span<byte> outputBuffer, int outputOffset);
  Span<byte> TransformFinalBlock(ReadOnlySpan<byte> inputBuffer, int inputOffset, int inputCount);
}

Marquez également ICryptoTransform comme [Obsolete]

Pour être poli avec les personnes ayant une connaissance préalable de la cryptographie .NET

[Obsolete("See ICipherTransform")]
public partial interface ICryptoTransform : System.IDisposable { ... }

2. Étendre la classe SymmetricAlgorithm existante pour Span I/O

public abstract class SymmetricAlgorithm : IDisposable
{
  ...
  public abstract ICipherTransform CreateDecryptor(ReadOnlySpan<byte> Key, ReadOnlySpan<byte> IV);
  public abstract ICipherTransform CreateEncryptor(ReadOnlySpan<byte> Key, ReadOnlySpan<byte> IV);
  public virtual ReadOnlySpan<byte> KeySpan {...}
  public virtual ReadOnlySpan<byte> IVSpan {...}
  public virtual ReadOnlySpan<byte> KeySpan {...}
  ...
}

3. Étendre CryptoStream existant pour Span I/O

C'est comme Stream dans System.Runtime. De plus, nous ajouterons un c'tor pour notre cas AEAD à suivre.

CRITIQUE : CryptoStream nécessitera une mise à jour obligatoire dans FlushFinalBlock pour ajouter la balise à la fin du flux lors du chiffrement et extraire automatiquement la balise (TagSize octets) lors du déchiffrement . Ceci est similaire à d'autres API testées au combat comme l'architecture cryptographique de Java ou C# BouncyCastle. C'est inévitable mais c'est le meilleur endroit pour le faire car en streaming, la balise est produite à la fin mais n'est pas nécessaire jusqu'à ce que le bloc final soit transformé lors du décryptage. L'avantage est qu'il simplifie considérablement le modèle de programmation.

Remarque : 1) Avec CBC-HMAC, vous pouvez choisir de vérifier d'abord la balise. C'est l'option la plus sûre, mais si c'est le cas, cela en fait un algorithme à deux passes. La 1ère passe calcule la balise HMAC puis la 2ème passe fait le déchiffrement. Ainsi, le flux mémoire ou le flux réseau devra toujours être mis en mémoire tampon en le réduisant au modèle à un seul coup ; pas en streaming. Les véritables algorithmes AEAD tels que GCM ou CCM peuvent diffuser efficacement.

public class CryptoStream : Stream, IDisposable
{
  ...
  public CryptoStream(Stream stream, IAuthenticatedCipherTransform transform, CryptoStreamMode mode);
  public override int Read(Span<byte> buffer, int offset, int count);
  public override Task<int> ReadAsync(Span<byte> buffer, int offset, int count, CancellationToken cancellationToken);
  public override void Write(ReadOnlySpan<byte> buffer, int offset, int count);
  public override Task WriteAsync(ReadOnlySpan<byte> buffer, int offset, int count, CancellationToken cancellationToken);

  public void FlushFinalBlock()
  {
    ...
    // If IAuthenticatedCipherTransform
    //    If encrypting, `TransformFinalBlock` -> `GetTag` -> append to out stream
    //    If decryption, extract last `TagSize` bytes -> `SetExpectedTag` -> `TransformFinalBlock`
    ...
  }

  ...
}

Couche dans le chiffrement authentifié

Avec ce qui précède, nous pouvons ajouter les bits manquants pour permettre le chiffrement authentifié avec données associées (AEAD)

Prolonger le nouveau ICipherTransform pour AEAD

Cela permet CryptoStream faire son travail correctement. Nous pouvons également utiliser l'interface IAuthenticatedCipherTransform pour implémenter notre propre classe/utilisation de streaming personnalisée, mais travailler avec CryptoStream permet d'obtenir une API de chiffrement .net super cohérente et cohérente.

  public interface IAuthenticatedCipherTransform : ICipherTransform
  {
    Span<byte> GetTag();
    void SetExpectedTag(Span<byte> tag);
  }

Classe de base de chiffrement authentifié

Développe simplement SymmetricAlgorithm

public abstract class AuthenticatedSymmetricAlgorithm : SymmetricAlgorithm
{
  ...
  // Program Key/IV/AAD via class properties OR CreateAuthenticatedEn/Decryptor() params
  public abstract IAuthenticatedCipherTransform CreateAuthenticatedDecryptor(ReadOnlySpan<byte> Key = default, ReadOnlySpan<byte> IV = default, ReadOnlySpan<byte> AuthenticatedData = default);
  public abstract IAuthenticatedCipherTransform CreateAuthenticatedEncryptor(ReadOnlySpan<byte> Key = default, ReadOnlySpan<byte> IV = default, ReadOnlySpan<byte> AuthenticatedData = default);
  public virtual Span<byte> AuthenticatedData {...}
  public virtual Span<byte> Tag {...}
  public virtual int TagSize {...}
  ...
}

Classe AES GCM

public class AesGcm : AuthenticatedSymmetricAlgorithm
{
  public AesGcm(ReadOnlySpan<byte> Key = default, ReadOnlySpan<byte> IV = default, ReadOnlySpan<byte> AuthenticatedData = default)

  /* other stuff like valid key sizes etc similar to `System.Security.Cryptography.Aes` */
}

@sidshetye J'applaudis l'effort.

Streaming-Encrypt sur GCM est faisable. Streaming-Decrypt sur GCM est

  • non autorisé dans NIST 800-38d. La section 5.2.2 « Fonction de décryptage authentifié » indique clairement que le retour du texte en clair décrypté P doit impliquer une authentification correcte via l'étiquette T.
  • pas sécurisé. Il existe une notion de sécurité selon laquelle les algorithmes sont sécurisés dans le paramètre "Release of Unverified Plaintext" (RUP). La sécurité RUP est formalisée dans un article de 2014 par Andreeva-et-al. GCM n'est pas sécurisé dans le paramètre RUP. La compétition CAESAR où chaque entrée est comparée à GCM répertorie la sécurité RUP comme une propriété souhaitable. Le texte en clair non vérifié publié par GCM est trivialement sujet aux attaques par retournement de bits.

Plus tôt dans ce fil, la possibilité d'API Encrypt/Decrypt asymétriques a été évoquée (conceptuellement), et je pense que le consensus était que ce serait une très mauvaise idée.

En résumé, vous ne pouvez pas disposer d'une API de streaming granulaire de haut niveau pour le déchiffrement GCM. Je l'ai dit plusieurs fois auparavant, et je le répète. Le seul moyen d'avoir une API de streaming est le chiffrement fragmenté. J'épargnerai à tout le monde le manège du cryptage par morceaux.

Quoi que MS décide de faire pour l'API GCM, RUP ne peut pas être autorisé.

@sdrapkin RUP a été discuté ici en détail et nous avons déjà traversé ce pont. En bref, RUP implique que les données décryptées n'ont pas besoin d'être utilisées jusqu'à ce que la balise soit vérifiée, mais comme Java JCE, WinNT bcrypt, OpenSSL, etc., il n'est pas nécessaire de l'appliquer à la limite de la méthode. Comme avec la plupart des primitives cryptographiques, en particulier celles de bas niveau, utilisez-les avec prudence.

^^ que tant. Je suis d'accord avec les API de flux de niveau supérieur, etc., puis appliquez-le. Mais quand je veux utiliser une primitive de bas niveau, je dois pouvoir utiliser des choses comme des tampons divisés, etc., et c'est à moi de m'assurer que les données ne sont pas utilisées. Lancez une exception dans le point de calcul/vérification des balises, mais ne bloquez pas les primitives de bas niveau.

c'est à moi de m'assurer que les données ne sont pas utilisées

Tort. Cela ne dépend pas de vous. AES-GCM a une définition très _spécifique_, et cette définition garantit que ce n'est pas à vous de décider. Ce que vous voulez, c'est une primitive AES-CTR distincte et une primitive GHASH distincte, que vous pouvez ensuite combiner et appliquer comme bon vous semble. Mais nous ne discutons pas de primitives AES-CTR et GHASH distinctes, n'est-ce pas ? Nous discutons AES-GCM. Et AES-GCM exige que RUP ne soit pas autorisé.

Je suggère également de revoir la réponse d'Ilmari Karonen de crypto.stackexchange .

@sdrapkin Vous faites un bon point. Il serait souhaitable, cependant, d'avoir éventuellement un algorithme qui soit sécurisé sous RUP, et que cet algorithme corresponde à l'API qui est décidée ici. Nous devons donc choisir :

  1. L'API ne prend pas en charge la diffusion en continu. Simple, mais manquant pour une API de bas niveau. On pourrait le regretter un jour.
  2. L' implémentation AES-GCM empêche le streaming, adhérant à la spécification sans limiter l'API.

Pouvons-nous détecter le scénario de diffusion en continu et lever une exception expliquant pourquoi cette utilisation est incorrecte ? Ou devons-nous recourir à faire en sorte que son implémentation de streaming consomme tout le tampon ? Ce dernier serait malheureux : vous pourriez penser que vous diffusez, mais ce n'est pas le cas.

Nous pourrions ajouter une méthode SupportsStreaming(out string whyNot) qui est vérifiée par l'implémentation du streaming.

Avons-nous un argument solide contre le streaming/tag-at-the-end en général ? Si ce n'est pas le cas, je pense que nous devrions viser à ne pas l'empêcher avec l'API.

@sdrapkin : Prenons une vue plus large de RUP puisqu'il s'agit d'une bibliothèque, pas d'une application. Il s'agit donc davantage d'un problème de mise en mémoire tampon et de conception de couche que de la publication/utilisation réelle de données non vérifiées. En regardant la publication spéciale NIST 800-38D pour AES GCM , nous voyons que

  1. La spécification GCM définit la longueur maximale du texte en clair comme étant de 2^39-256 bits ~ 64 Go. Une mise en mémoire tampon proche de celle de la mémoire système est déraisonnable.

  2. La spécification GCM a défini la sortie comme FAIL si la balise échoue. Mais il n'est pas normatif de savoir quelle couche d'une implémentation doit mettre en mémoire tampon jusqu'à la vérification des balises. Regardons une pile d'appels comme :

A => AESGCM_Decrypt_App(key, iv, ciphertext, aad, tag)
B =>  +- AESGCM_Decrypt_DotNet(key, iv, ciphertext, aad, tag)
C =>    +- AESGCM_Decrypt_OpenSSL(key, iv, ciphertext, aad, tag)


A est AES GCM au niveau de la couche application
B est AES-GCM au niveau du langage
C est AES-GCM au niveau de la plate-forme

Le texte en clair est publié en (A) si la balise est vérifiée, mais FAIL est renvoyé dans le cas contraire. Cependant, absolument nulle part dans la spécification, cela ne suggère que la mémoire principale est le seul endroit pour mettre en mémoire tampon le texte en clair en cours, ni que la mise en mémoire tampon devrait se produire en (B) ou (C) ou ailleurs. En fait OpenSSL, Windows NT Bcrypt à l'exemple de (C) où le streaming permet la mise en mémoire tampon à la couche supérieure. Et Java JCA, la sécurité CLR de Microsoft et ma proposition ci-dessus sont des exemples de (B) où le streaming permet la mise en mémoire tampon au niveau de la couche application. Il est présomptueux de supposer que les concepteurs de A n'ont pas de meilleures capacités de mise en mémoire tampon avant de publier du texte en clair. Ce tampon, en théorie et en pratique, pourrait être de la mémoire ou des SSD ou un cluster de stockage sur le réseau. Ou des cartes perforées ;) !

Même en laissant de côté la mise en mémoire tampon, l'article aborde d'autres problèmes pratiques (voir Section 9.1, Considérations de conception et 9.2, Considérations opérationnelles ) comme la fraîcheur des clés ou la non-répétition IV sur des pannes de puissance indéfinie. Nous n'allons évidemment pas cuire cela dans la couche B, c'est-à-dire ici.

@timovzl la proposition récente ci-dessus aborde les deux scénarios - one-shot (l'architecte ne se soucie pas de la mise en mémoire tampon) ainsi que le streaming (l'architecte a de meilleures capacités de mise en mémoire tampon). Tant que la documentation de l'API de streaming de bas niveau indique clairement que le consommateur est désormais responsable de sa mise en mémoire tampon, il n'y a aucune réduction de la preuve de sécurité et il n'y a aucun écart par rapport à la spécification.

EDIT : grammaire, fautes de frappe et tentative de faire fonctionner Markdown

Bingo .. Encore une fois, c'est la décision des concepteurs de couches quant à l'endroit où la vérification des balises se produit. Je ne préconise en aucun cas la divulgation de données non vérifiées à la couche application.

La discussion revient sur ces API de niveau "consommateur" ou sont-elles de véritables primitives. S'il s'agit de véritables primitives, elles doivent exposer la fonctionnalité afin que des API "plus sûres" de niveau supérieur puissent être construites par-dessus. Il a déjà été décidé ci-dessus avec la discussion sur Nonce qu'il devrait s'agir de véritables primitives, ce qui signifie que vous pourriez vous tirer une balle dans le pied, je pense qu'il en va de même pour le décodage de texte chiffré en continu/partiel.

Cela dit, il sera impératif de fournir rapidement des API de niveau "supérieur" et plus sûrs pour empêcher les gens de "rouler" les leurs.

Mon intérêt vient de la mise en réseau/des pipelines et si vous ne pouvez pas faire de tampons partiels et que vous devez faire "un seul coup", alors il n'y aurait aucun avantage à l'inconvénient de ces API, donc je continuerais à aller directement à BCrypt/OpenSsl etc.

Mon intérêt vient de la mise en réseau/des pipelines et si vous ne pouvez pas faire de tampons partiels et que vous devez faire "un seul coup", alors il n'y aurait aucun avantage à l'inconvénient de ces API, donc je continuerais à aller directement à BCrypt/OpenSsl etc.

Exactement. Le besoin ne disparaîtra pas, donc les gens utiliseront d'autres implémentations ou lanceront les leurs. Ce n'est pas nécessairement un résultat plus sûr que d'autoriser le streaming avec une bonne documentation d'avertissement.

@Timovzl , je pense que nous avons suscité beaucoup de commentaires techniques et d'exigences de conception autour de la dernière proposition . Des réflexions sur la mise en œuvre et la publication ?

@Sidshetye a fait une proposition détaillée qui, je pense, répond à toutes les exigences. La seule critique, concernant le RUP, a été adressée sans autre opposition. (Plus précisément, RUP peut être empêché dans l'une des couches, et l'API de bas niveau ne devrait pas dicter laquelle ; et offrir _no_ streaming devrait avoir de pires effets.)

Dans l'intérêt du progrès, je voudrais inviter toute personne ayant d'autres préoccupations concernant la dernière proposition à prendre la parole - et, bien sûr, à proposer des alternatives.

Je suis enthousiasmé par cette proposition et par une API qui prend forme.

@Sidshetye , j'ai quelques questions et suggestions :

  1. Est-il souhaitable d'hériter du SymmetricAlgorithm existant ? Y a-t-il des composants existants que nous souhaitons intégrer ? À moins que je ne manque un avantage à cette approche, je préférerais voir AuthenticatedEncryptionAlgorithm sans classe de base. Si rien d'autre, cela évite d'exposer les méthodes indésirables CreateEncryptor / CreateDecryptor (non authentifiées !).
  2. Aucun des composants impliqués n'est utilisable avec la cryptographie asymétrique, n'est-ce pas ? Presque tous les composants omettent "Symmetric" de leurs noms, ce avec quoi je suis d'accord. À moins que nous continuions à hériter SymmetricAlgorithm , AuthenticatedSymmetricAlgorithm pourrait être renommé en AuthenticatedEncryptionAlgorithm , en adhérant au terme conventionnel Authenticated Encryption.
  3. Modifiez TryEncrypt / TryDecrypt pour écrire dans / recevoir la balise, plutôt que d'avoir une propriété réglable Tag sur l'algorithme.
  4. Quel est le but de définir key , iv et authenticatedAdditionalData via des setters publics ? J'éviterais autant que possible les multiples approches valides et les propriétés mutables. Pourriez-vous créer une proposition mise à jour sans eux ?
  5. Voulons-nous un état dans AesGcm ? Mon instinct est de garder définitivement iv et authenticatedAdditionalData , car ils sont par message. Le key peut valoir la peine d'avoir comme état, car nous voulons généralement effectuer plusieurs opérations avec une seule clé. Pourtant, il est également possible de prendre la clé par appel. Les mêmes questions valent pour CreateAuthenticatorEncryptor . Dans tous les cas, nous devrions nous contenter de _sens unique_ pour passer les paramètres. Je suis impatient de discuter des avantages et des inconvénients. Je penche vers l'état clé dans AesGcm , et le reste dans CreateAuthenticatedEncryptor ou TryEncrypt respectivement. Si nous sommes déjà d'accord, veuillez nous montrer une proposition mise à jour. :-)
  6. ICipherTransform devrait probablement être une classe abstraite, CipherTransform , afin que des méthodes puissent être ajoutées sans casser les implémentations existantes.
  7. Tous les paramètres de fonction doivent utiliser camelCase, c'est-à-dire commencer en minuscules. Aussi, devrions-nous dire authenticatedData ou authenticatedAdditionalData ? De plus, je pense que nous devrions choisir les noms de paramètre plaintext et ciphertext .
  8. Partout où l'IV est passé, j'aimerais le voir comme un paramètre facultatif, ce qui facilite l'obtention d'un IV correctement généré (crypto-aléatoire) plutôt que de fournir le nôtre. Rend l'utilisation abusive de l'API de bas niveau plus difficile, au moins, et nous l'obtenons gratuitement.
  9. J'essaie toujours de comprendre comment le code client TryEncrypt peut connaître la longueur de portée requise pour fournir ciphertext ! Idem pour TryDecrypt et la longueur de plaintext . Nous ne sommes sûrement pas censés les essayer en boucle jusqu'au succès, en doublant la longueur après chaque itération ratée ?

Enfin, en pensant à l'avenir, à quoi pourrait ressembler une API de haut niveau construite au-dessus de cela ? En ce qui concerne uniquement l'utilisation de l'API, il semble y avoir peu de place à l'amélioration, car les API de streaming et non-streaming sont déjà si simples ! Les principales différences que j'imagine sont un IV automatique, des tailles de sortie automatiques et éventuellement une limite sur la quantité de données cryptées.

Windows autorise la diffusion en continu. OpenSSL le fait aussi. Ces deux éléments l'ont pour la plupart classé dans des concepts existants (bien qu'ils aient tous les deux jeté une clé avec "et il y a cette chose sur le côté que vous devez gérer ou je vais me tromper").

Go ne le fait pas, et libsodium ne le fait pas.

Il semble que la première vague l'ait permis, et les suivantes non. Puisque nous sommes incontestablement dans une vague ultérieure, je pense que nous allons nous en tenir à ne pas le permettre. S'il y a une augmentation de la demande de streaming après l'introduction d'un modèle unique (pour le cryptage/décryptage, la clé peut être conservée d'un appel à l'autre), nous pouvons alors réévaluer. Ainsi, une proposition d'API qui adhère à ce modèle semble bénéfique. Bien que ni SIV ni CCM ne prennent en charge le cryptage en streaming, l'API de streaming pour eux est potentiellement fortement mise en mémoire tampon. Garder les choses claires semble mieux.

Les propositions ne doivent pas non plus intégrer la balise dans la charge utile (GCM et CCM l'appellent comme une donnée distincte), à ​​moins que l'algorithme lui-même (SIV) ne l'intègre dans la sortie du chiffrement. ( E(...) => (c, t) contre E(...) => c || t ou E(...) => t || c ). Les utilisateurs de l'API peuvent certainement l'utiliser comme concat (ouvrez simplement les étendues de manière appropriée).

La spécification GCM n'autorise pas la libération d'autre chose que FAIL en cas de non-concordance des balises. Le NIST est assez clair à ce sujet. L'article GCM original de McGrew & Viega dit également :

L'opération de déchiffrement renverrait FAIL plutôt que le texte en clair, et la décapsulation s'arrêterait et le texte en clair serait rejeté plutôt que transmis ou traité ultérieurement.

Aucun des commentaires précédents ne concernait RUP - ils l'ont simplement fait signe de la main ("la couche supérieure s'en occupera" - oui, c'est vrai).

C'est simple : le cryptage GCM peut diffuser. Le décryptage GCM ne peut pas diffuser. Tout le reste n'est plus GCM.

Il semble que la première vague l'ait permis, et les suivantes non. Puisque nous sommes incontestablement dans une vague ultérieure, je pense que nous allons nous en tenir à ne pas le permettre.

@bartonjs , vous ignorez littéralement toutes les analyses techniques et logiques et utilisez à la place les dates des projets Go et libsodium comme indicateur faible de la véritable analyse ? Imaginez si je fais un argument similaire basé sur les noms des projets. De plus, nous décidons de l'interface ET des implémentations. Vous vous rendez compte que le choix d'une interface sans streaming pour AEAD exclut toutes les implémentations de ce type, n'est-ce pas ?

S'il y a une augmentation de la demande de streaming après l'introduction d'un modèle unique (pour le cryptage/décryptage, la clé peut être conservée d'un appel à l'autre), nous pouvons alors réévaluer.

Pourquoi la demande démontrée jusqu'à présent sur GitHub est-elle insuffisante ? Il arrive au point où il semble tout à fait fantaisiste de supporter d'avoir à faire moins de travail que sur les mérites techniques ou de la demande des clients.

@bartonjs , vous ignorez littéralement toutes les analyses techniques et logiques et utilisez à la place les dates des projets Go et libsodium comme indicateur faible de la véritable analyse ?

Non, j'utilise les conseils de cryptographes professionnels qui disent que c'est extrêmement dangereux et que nous devrions éviter de diffuser AEAD. Ensuite, j'utilise les informations de l'équipe CNG selon lesquelles "beaucoup de gens disent qu'ils le veulent en théorie, mais en pratique, presque personne ne le fait" (je ne sais pas dans quelle mesure il s'agit de télémétrie par rapport à l'anecdotique des demandes d'assistance sur le terrain). Le fait que d'autres bibliothèques aient opté pour la voie unique _renforce_ simplement la décision.

Pourquoi la demande démontrée jusqu'à présent sur GitHub est-elle insuffisante ?

Quelques scénarios ont été évoqués. Le traitement des tampons fragmentés pourrait probablement être résolu en acceptant ReadOnlySequence , s'il semble qu'il y ait suffisamment de scénario pour justifier de compliquer l'API au lieu de demander à l'appelant de réassembler les données.

Les fichiers volumineux sont un problème, mais les fichiers volumineux sont déjà un problème puisque GCM a une coupure à un peu moins de 64 Go, ce qui n'est "pas si gros" (d'accord, c'est assez gros, mais ce n'est pas le "whoa, c'est gros" qui c'était le cas). Les fichiers mappés en mémoire permettraient d'utiliser des étendues (jusqu'à 2 ^ 31-1) sans nécessiter 2 Go de RAM. Nous avons donc réduit de quelques bits le maximum... cela se produirait probablement avec le temps de toute façon.

Vous vous rendez compte que le choix d'une interface sans streaming pour AEAD exclut toutes les implémentations de ce type, n'est-ce pas ?

Je suis de plus en plus convaincu que @GrabYourPitchforks avait raison (https://github.com/dotnet/corefx/issues/23629#issuecomment-334638891) qu'il n'y a probablement pas d'interface unificatrice sensée. GCM _nécessitant_ un nonce/IV et SIV _interdisant_ cela signifie que l'initialisation d'un mode/algorithme AEAD nécessite déjà une connaissance de ce qui va se passer... il n'y a pas vraiment de notion "abstraite" pour AEAD. SIV dicte où va "la balise". GCM/CCM ne le font pas. SIV est tag-first, par spec.

SIV ne peut pas commencer à chiffrer tant qu'il n'a pas toutes les données. Ainsi, son cryptage en continu va soit lancer (ce qui signifie que vous devez savoir ne pas l'appeler) ou mettre en mémoire tampon (ce qui pourrait entraîner un temps de fonctionnement n ^ 2). CCM ne peut pas démarrer tant que la longueur n'est pas connue ; mais CNG n'autorise pas un indice de pré-chiffrement sur la longueur, donc c'est dans le même bateau.

Nous ne devrions pas concevoir un nouveau composant où il est plus facile de faire la mauvaise chose que la bonne chose par défaut. Le décryptage en continu rend très facile et tentant de câbler une classe Stream (à la manière de votre proposition de le faire avec CryptoStream), ce qui permet d'obtenir très facilement un bogue de validation des données avant que la balise ne soit vérifiée, ce qui annule presque entièrement le bénéfice de AE . ( IGcmDecryptor => CryptoStream => StreamReader => XmlReader => "attendez, ce n'est pas du XML légal..." => oracle de texte chiffré adaptatif) .

Ça va droit au but... la demande des clients.

Comme je l'ai malheureusement entendu bien trop de fois dans ma vie : je suis désolé, mais vous n'êtes pas le client que nous avons en tête. Je concéderai que vous savez peut-être comment faire GCM en toute sécurité. Vous savez qu'il ne faut diffuser que vers un fichier/tampon/etc volatile jusqu'à la vérification des balises. Vous savez ce que signifie la gestion nonce et vous connaissez les risques de se tromper. Vous savez qu'il faut faire attention aux tailles de flux et passer à un nouveau segment GCM après 2^36-64 octets. Vous savez qu'après que tout soit dit et fait, c'est votre bogue si vous vous trompez.

Le client que j'ai en tête, en revanche, est quelqu'un qui sait "je dois chiffrer ça" parce que son patron le lui a dit. Et ils savent que lorsqu'ils recherchent comment effectuer le cryptage, certains tutoriels disent "toujours utiliser AE" et mentionnent GCM. Ensuite, ils trouvent un tutoriel "chiffrement dans .NET" qui utilise CryptoStream. Ils connectent ensuite le pipeline, n'ayant aucune idée qu'ils viennent de faire la même chose qu'en choisissant SSLv2... coché une case en théorie, mais pas vraiment en pratique. Et quand ils le font, ce bogue appartient à tous ceux qui savaient mieux, mais que la mauvaise chose soit trop facile à faire.

vous n'êtes pas le client que nous avons en tête [...] Le client que j'ai en tête, en revanche, est quelqu'un qui sait "je dois chiffrer ça" parce que son patron lui a dit de [...]

@bartonjs il y a des mois, nous avions déjà décidé que l'objectif était de cibler deux profils de clients en ayant une API de bas niveau (puissante mais peu sûre dans certaines conditions) et une API de haut niveau (infaillible). C'est même dans le titre. C'est certes un pays libre mais il est malhonnête de déplacer maintenant le poteau de but en prétendant le contraire.

Le client que j'ai en tête, en revanche, est quelqu'un qui sait "je dois chiffrer ça" parce que son patron le lui a dit. Et ils savent que lorsqu'ils recherchent comment effectuer le cryptage, certains tutoriels disent "toujours utiliser AE" et mentionnent GCM. Ensuite, ils trouvent un tutoriel "chiffrement dans .NET" qui utilise CryptoStream. Ils connectent ensuite le pipeline, n'ayant aucune idée qu'ils viennent de faire la même chose qu'en choisissant SSLv2... coché une case en théorie, mais pas vraiment en pratique. Et quand ils le font, ce bogue appartient à tous ceux qui savaient mieux, mais que la mauvaise chose soit trop facile à faire.

@bartonjs Attendez, qu'est-il arrivé à une primitive de bas niveau ? Je pensais que le but de ce numéro particulier était la flexibilité par rapport à la garde d'enfants. Faites-nous savoir si le plan a changé, afin que nous parlions tous de la même chose.

En outre, les méthodes par bloc sont-elles toujours à l'étude, ou uniquement les méthodes ponctuelles ?

Je suis de plus en plus convaincu que @GrabYourPitchforks avait raison (#23629 (commentaire)) qu'il n'y a probablement pas d'interface unificatrice sensée.

En voyant tous les exemples, cela commence à sembler de plus en plus futile - en particulier pour une API de bas niveau où les implémentations ont des restrictions si différentes. Peut-être devrions-nous simplement donner un exemple solide avec AES-GCM, plutôt qu'une interface unificatrice. En passant, ce dernier pourrait encore être intéressant pour une future API de haut niveau. Sa propriété d'être plus contraignante rendra probablement une interface fédératrice beaucoup plus facile à y réaliser.

les méthodes par bloc sont-elles toujours à l'étude, ou seulement les méthodes ponctuelles ?

Comme mentionné dans https://github.com/dotnet/corefx/issues/23629#issuecomment -378605071, nous pensons que le risque vs la récompense vs les cas d'utilisation exprimés disent que nous ne devrions autoriser que les versions uniques d'AE.

Je n'ai pas lu toute la discussion, juste des parties aléatoires. Je ne sais pas dans quelle direction tu vas. Désolé si ce que j'écris n'a pas de sens dans ce contexte. Mes 2 ¢ :

  • Les flux sont importants. Si vous ne pouvez pas les prendre en charge directement car cela signifierait une vulnérabilité de sécurité, fournissez si possible un wrapper de niveau supérieur construit au-dessus de votre API de bas niveau qui exposerait les flux (de manière inefficace mais sûre).
  • Si AES-GCM ne peut absolument pas utiliser de flux dans aucun scénario, fournissez une implémentation légitime d'AES-CBC-HMAC basée sur des flux. Ou un autre algorithme AE.
  • Plus le niveau est élevé, mieux c'est. Moins il y a de zones où l'utilisateur peut faire une erreur, mieux c'est. Signification - exposer l'API qui cacherait autant de choses que possible (par exemple cette balise d'authentification). Bien sûr, il peut (devrait) y avoir aussi des surcharges plus spécifiques.
  • À mon humble avis, ne vous souciez pas d'unifier les interfaces avec d'autres services de chiffrement si elles ne correspondent tout simplement pas. C'est ce que openssl a fait avec leur CLI et le résultat est médiocre (par exemple, aucune possibilité de fournir la balise d'authentification).

Nous (l'équipe de sécurité .NET) avons discuté entre nous et avec l'équipe de cryptographie au sein de Microsoft. Nous avons discuté de nombreux problèmes et préoccupations mentionnés dans ce fil. En fin de compte, ces préoccupations n'étaient pas suffisamment convaincantes pour justifier l'introduction d'une API GCM de streaming en tant que bloc de construction principal dans le cadre.

Cette décision peut être réexaminée à l'avenir si le besoin s'en fait sentir. Et en attendant, nous n'avons pas aggravé les choses qu'elles ne le sont aujourd'hui : les développeurs qui utilisent actuellement une bibliothèque de chiffrement tierce pour la prise en charge en continu de GCM peuvent continuer à le faire, et ils ne seront pas interrompus par notre introduction prévue d'un API GCM non diffusée en continu.

Comment gérer le cryptage des données qui ne rentrent pas dans la mémoire ?

@pgolebiowski Vous utilisez des bibliothèques de chiffrement .NET de haut niveau spécialement conçues pour offrir un chiffrement en continu sécurisé.

@sdrapkin c'est plus facile à dire qu'à faire. "sûr" est beaucoup demander. qu'est-ce qui est prouvé et auquel on peut réellement faire confiance ? tu dis toi-même :

Bibliothèque c# Bouncy Castle (une recommandation typique de StackOverflow). Bouncy Castle c# est un énorme catalogue de musée (145k LOC) peu performant de crypto (certains anciens), avec d'anciennes implémentations Java portées sur .NET tout aussi ancien (2.0?).

ok, alors quelles sont les options? peut-être votre propre bibliothèque? pas vraiment . hmm... peut-être libsodium-net ? pas vraiment .

lorsque vous recherchez une bibliothèque auditée provenant d'une source plutôt fiable (comme Microsoft ou largement utilisée par la communauté), je ne pense pas qu'une telle bibliothèque existe dans le monde .NET Core.


  • client : cryptage authentifié des données qui ne rentrent pas en mémoire ?
  • Microsoft : Je suis désolé, mais vous n'êtes pas le client que nous avons en tête. utilisez une bibliothèque qui n'est pas auditée et dont la sécurité est discutable, ce n'est pas notre problème si vous êtes victime d'une attaque par canal latéral.
  • client:

@pgolebiowski Les options consistent à utiliser un framework .NET établi - c'est-à-dire. la chose même qui a fait ses preuves et à laquelle on peut faire confiance, comme vous le souhaitez. D'autres bibliothèques (y compris la mienne) attendent que Microsoft ajoute les primitives de chiffrement manquantes comme ECDH dans NetStandard.

Vous pouvez également regarder les fourches Inferno. Il y a au moins 2 fourches où quelques changements triviaux ont atteint NetStandard20.

Votre bibliothèque a été auditée il y a 2 ans en 2 jours par une seule personne . Je n'y crois pas, désolé. Chez Microsoft, il y aurait une équipe dédiée à cela - des personnes en qui j'ai une plus grande confiance que celles de Cure53.

Sérieusement, nous pouvons parler de support tiers pour beaucoup de choses. Mais tous les éléments nécessaires liés à la sécurité doivent être fournis par la bibliothèque standard.

@pgolebiowski Loin de moi l'idée d'essayer de convaincre qui que ce soit de faire confiance à quoi que ce soit, mais votre déclaration n'est pas exacte. Inferno a été audité par 2 professionnels de l'association "Cure53". L'audit a duré 2 jours et la bibliothèque entière comptait environ 1 000 lignes de code. Cela représente environ 250 lignes de code par auditeur/jour - tout à fait gérable.

En fait, la facilité d'audit est l'une des principales caractéristiques d'Inferno, précisément pour ceux qui ne veulent pas faire confiance.

Le but de ce fil est d'ajouter la prise en charge d'AES-GCM. Votre bibliothèque ne prend même pas en charge AES-GCM. Si vous voulez que les gens utilisent votre code, soumettez-le en tant que proposition pour corefx. Dans un fil approprié.

Une autre chose, même s'il supportait cet algorithme - il n'a pas été examiné par le conseil de cryptographie .net et ne fait pas partie du corefx. Il n'est même pas candidat à un tel examen. Cela signifie la fin de cette discussion et de cette publicité futiles.

@pgolebiowski Je n'ai rien annoncé - j'ai simplement répondu à votre question, suggéré des alternatives pour votre cas d'utilisation et corrigé des affirmations inexactes. AES-GCM n'est pas adapté au chiffrement en continu, et c'est quelque chose qui a été examiné et accepté par l'équipe de sécurité .NET, vous pouvez donc faire confiance à cela .

est-ce que je défends le streaming AES-GCM n'importe où ? ne peut pas rappeler. mais je me souviens avoir dit :

  • Les flux sont importants. Si vous ne pouvez pas les prendre en charge directement car cela signifierait une vulnérabilité de sécurité, fournissez si possible un wrapper de niveau supérieur construit au-dessus de votre API de bas niveau qui exposerait les flux (de manière inefficace mais sûre).
  • Si AES-GCM ne peut absolument pas utiliser de flux dans aucun scénario, fournissez une implémentation légitime d'AES-CBC-HMAC basée sur des flux. Ou un autre algorithme AE.

ou en indiquant un problème ouvert pour corefx :

Comment gérer le cryptage des données qui ne rentrent pas dans la mémoire ?


prime:

Vous pouvez également regarder les fourches Inferno. Il y a au moins 2 fourches où quelques changements triviaux ont atteint NetStandard20. [...] Inferno a été audité par 2 professionnels de l'organisation "Cure53" [...] l'auditabilité facile fait partie des caractéristiques clés d'Inferno, précisément pour ceux qui ne veulent pas faire confiance.

je n'ai rien annoncé

En passant, je n'ai jamais vraiment voulu "streamer" dans le sens où la plupart le pensent. Je voulais juste un traitement "bloc" pour des raisons de performance et d'utilisation de la mémoire. Je ne veux pas réellement "diffuser" les résultats. C'est ce que le support d'openssl et cng semble dommage de perdre cela et rend fondamentalement les "primitifs" inutiles dans tous les scénarios auxquels je peux penser.

@Drawaes À bien y penser, l'opération de bloc pourrait être beaucoup plus sûre que d'utiliser des flux. Un profane peut toucher aux flux, mais il préférera de loin utiliser l'API one-shot plutôt que l'opération de bloc. De plus, l'opération de bloc ne peut pas _simplement_ être combinée avec, disons, XmlReader . Donc, en fait, bon nombre des dangers évoqués s'appliquent aux objets de flux, mais pas au blocage des opérations.

Travailler avec une opération de bloc lorsqu'une API unique est également disponible, suggère que nous savons ce que nous faisons et que nous avons spécifiquement besoin d'ajustements de bas niveau. Nous pourrions protéger le profane _et_ avoir de la flexibilité.

Quant à éviter le RUP, je me demande toujours à quel point une opération de bloc d'avantages est vraiment pour GCM. Le chiffrement récolte tous les avantages, tandis que le déchiffrement n'en profite que partiellement. Nous pouvons éviter de stocker le texte chiffré complet, mais nous devons toujours mettre en mémoire tampon le texte clair complet. Un décrypteur _pourrait_ choisir de stocker le texte en clair intermédiaire sur disque. Mais en retour, nous avons introduit plus de marge d'erreur. Avons-nous un argument convaincant pour ne pas résoudre ce problème à un niveau supérieur (par exemple, en faire un gros morceau ou utiliser un véritable algorithme de streaming) ?

TLS et pipelines. Actuellement (et dans un avenir prévisible), les pipelines utilisent des blocs de 4k, mais un message tls peut contenir 16k de texte chiffré. Avec un seul coup, vous devrez copier les 16k dans un seul tampon continu avant de pouvoir déchiffrer. Avec des blocs, vous pourriez avoir, par exemple, 4 ou 5 et vous devrez peut-être mettre en mémoire tampon jusqu'à 16 octets pour garantir la concurrence des blocs.

@Drawaes 16k est toujours constant et pas énorme. Cela fait-il une grande différence dans ce contexte?

Oui, cela signifie une autre copie dans le pipeline. Cela a un effet majeur et mesurable sur la perf.

Que faut-il pour que cela se produise ? Quelles sont les prochaines étapes? @Drawes

Quant à AES-GCM, je pense que sa livraison est altérée en raison du blocage du problème correspondant : https://github.com/dotnet/corefx/issues/7023. @blowdart , pourriez-vous débloquer ? C'est vraiment difficile d'avoir des progrès quand les gens ne peuvent pas discuter. Ou, si ce n'est pas une option, proposez peut-être une solution alternative qui permet de rendre cette fonctionnalité accessible au public.

Non, je ne débloque pas ça. Une décision a été prise, le sujet est terminé.

@blowdart Merci pour la réponse. Je comprends que ce n'était peut-être pas assez clair :

Ou, si ce n'est pas une option, proposez peut-être une solution alternative qui permet de rendre cette fonctionnalité accessible au public.

J'apprécie qu'il y ait une décision de soutenir AES-GCM. C'est génial, je veux vraiment cet algorithme. Ainsi, maintenant, ce serait cool de le prendre en charge. Souhaitez-vous que la discussion sur la conception et la mise en œuvre d'AES-GCM ait lieu ici ou dans un nouveau numéro ?

Aussi, si ce sujet est terminé, pourquoi ne pas le fermer? Et rendez-le plus explicite en changeant le titre de ce numéro, car pour le moment, il suggère que la discussion sur la mise en œuvre se tiendrait ici : https://github.com/dotnet/corefx/issues/7023. Peut-être quelque chose comme Décidez quel algorithme AEAD prendre en charge en premier .

En d'autres termes : je fournis des commentaires selon lesquels, dans la situation actuelle, ce qui est nécessaire pour faire avancer AES-GCM n'est pas clair.

@karelz

@pgolebiowski Il y a déjà un PR sorti. Il sera probablement disponible en master mercredi la semaine prochaine.

Cette page vous a été utile?
0 / 5 - 0 notes

Questions connexes

EgorBo picture EgorBo  ·  3Commentaires

bencz picture bencz  ·  3Commentaires

jzabroski picture jzabroski  ·  3Commentaires

noahfalk picture noahfalk  ·  3Commentaires

sahithreddyk picture sahithreddyk  ·  3Commentaires