Timingproblemen met symmetrische ontsleuteling in de CBC-modus met behulp van opvulling
Microsoft is van mening dat het niet langer veilig is om gegevens te ontsleutelen die zijn versleuteld met de CBC-modus (Cipher-Block-Chaining) van symmetrische versleuteling wanneer verifieerbare opvulling is toegepast zonder eerst de integriteit van de coderingstekst te garanderen, met uitzondering van zeer specifieke omstandigheden. Dit oordeel is gebaseerd op momenteel bekend cryptografisch onderzoek.
Inleiding
Een opvullingsorakelaanval is een type aanval tegen versleutelde gegevens waarmee de aanvaller de inhoud van de gegevens kan ontsleutelen, zonder de sleutel te kennen.
Een oracle verwijst naar een 'tell' die een aanvaller informatie geeft over of de actie die ze uitvoeren juist is of niet. Stel dat u een bord- of kaartspel speelt met een kind. Als hun gezicht oplicht met een grote glimlach omdat ze denken dat ze op het punt staan om een goede beweging te maken, is dat een orakel. Je, als tegenstander, kan dit oracle gebruiken om je volgende stap op de juiste manier te plannen.
Opvulling is een specifieke cryptografische term. Sommige coderingen, die de algoritmen zijn die worden gebruikt om uw gegevens te versleutelen, werken aan gegevensblokken waarbij elk blok een vaste grootte heeft. Als de gegevens die u wilt versleutelen niet de juiste grootte hebben om de blokken te vullen, worden uw gegevens opgevuld totdat ze dat doen. Veel vormen van opvulling vereisen dat opvulling altijd aanwezig is, zelfs als de oorspronkelijke invoer de juiste grootte heeft. Hierdoor kan de opvulling altijd veilig worden verwijderd bij ontsleuteling.
Door de twee dingen samen te voegen, laat een software-implementatie met een opvullingorakel zien of ontsleutelde gegevens geldige opvulling hebben. Het oracle kan iets eenvoudigs zijn als het retourneren van een waarde met de tekst 'Ongeldige opvulling' of iets ingewikkelders, zoals het nemen van een onveranderlijk andere tijd om een geldig blok te verwerken in plaats van een ongeldig blok.
Op blok gebaseerde coderingen hebben een andere eigenschap, de modus genoemd, die de relatie van gegevens in het eerste blok bepaalt met de gegevens in het tweede blok, enzovoort. Een van de meest gebruikte modi is CBC. CBC introduceert een eerste willekeurig blok, ook wel de Initialization Vector (IV) genoemd, en combineert het vorige blok met het resultaat van statische versleuteling, zodat het versleutelen van hetzelfde bericht met dezelfde sleutel niet altijd dezelfde versleutelde uitvoer produceert.
Een aanvaller kan een opvullingorakel gebruiken, in combinatie met de structuur van CBC-gegevens, om enigszins gewijzigde berichten te verzenden naar de code die het oracle beschikbaar maakt en gegevens te blijven verzenden totdat het oracle aangeeft dat de gegevens juist zijn. Vanuit dit antwoord kan de aanvaller het bericht byte byte ontsleutelen.
Moderne computernetwerken hebben een dergelijke hoge kwaliteit dat een aanvaller zeer kleine (minder dan 0,1 ms) verschillen in uitvoeringstijd op externe systemen kan detecteren. Toepassingen die ervan uitgaan dat een geslaagde ontsleuteling alleen kan optreden wanneer er niet met de gegevens is geknoeid, kunnen kwetsbaar zijn voor aanvallen van hulpprogramma's die zijn ontworpen om verschillen in geslaagde en mislukte ontsleuteling te observeren. Hoewel dit tijdsverschil in sommige talen of bibliotheken belangrijker kan zijn dan andere, is het nu van mening dat dit een praktische bedreiging is voor alle talen en bibliotheken wanneer rekening wordt gehouden met de reactie van de toepassing op fouten.
Deze aanval is afhankelijk van de mogelijkheid om de versleutelde gegevens te wijzigen en het resultaat te testen met het oracle. De enige manier om de aanval volledig te beperken, is door wijzigingen in de versleutelde gegevens te detecteren en te weigeren acties uit te voeren. De standaardmethode hiervoor is het maken van een handtekening voor de gegevens en het valideren van de handtekening voordat er bewerkingen worden uitgevoerd. De handtekening moet verifieerbaar zijn, deze kan niet worden gemaakt door de aanvaller, anders zouden ze de versleutelde gegevens wijzigen en vervolgens een nieuwe handtekening berekenen op basis van de gewijzigde gegevens. Een veelvoorkomend type van de juiste handtekening wordt een sleutel-hash-berichtverificatiecode (HMAC) genoemd. Een HMAC verschilt van een controlesom omdat deze een geheime sleutel nodig heeft, alleen bekend bij de persoon die de HMAC produceert en aan de persoon die deze valideert. Zonder het bezit van de sleutel kunt u geen juiste HMAC produceren. Wanneer u uw gegevens ontvangt, neemt u de versleutelde gegevens op, berekent u de HMAC onafhankelijk met behulp van de geheime sleutel die u en de afzender delen, en vergelijkt u vervolgens de HMAC die ze hebben verzonden op basis van de sleutel die u hebt berekend. Deze vergelijking moet constante tijd zijn, anders hebt u een ander detecteerbaar oracle toegevoegd, waardoor een ander type aanval mogelijk is.
Kortom, als u versleutelde CBC-blokcoderingen veilig wilt gebruiken, moet u deze combineren met een HMAC (of een andere controle voor gegevensintegriteit) die u valideert met behulp van een constante tijdvergelijking voordat u de gegevens probeert te ontsleutelen. Omdat alle gewijzigde berichten dezelfde tijd in beslag nemen om een reactie te produceren, wordt de aanval voorkomen.
Wie is kwetsbaar
Dit beveiligingsprobleem is van toepassing op zowel beheerde als systeemeigen toepassingen die hun eigen versleuteling en ontsleuteling uitvoeren. Dit omvat bijvoorbeeld:
- Een toepassing die een cookie versleutelt voor latere ontsleuteling op de server.
- Een databasetoepassing die gebruikers de mogelijkheid biedt om gegevens in te voegen in een tabel waarvan de kolommen later worden ontsleuteld.
- Een toepassing voor gegevensoverdracht die afhankelijk is van versleuteling met behulp van een gedeelde sleutel om de gegevens in transit te beveiligen.
- Een toepassing die berichten 'binnen' de TLS-tunnel versleutelt en ontsleutelt.
Houd er rekening mee dat het gebruik van ALLEEN TLS u in deze scenario's mogelijk niet beveiligt.
Een kwetsbare toepassing:
- Ontsleutelt gegevens met behulp van de CBC-coderingsmodus met een verifieerbare opvullingsmodus, zoals PKCS#7 of ANSI X.923.
- Voert de ontsleuteling uit zonder dat er een controle op gegevensintegriteit is uitgevoerd (via een MAC of een asymmetrische digitale handtekening).
Dit geldt ook voor toepassingen die zijn gebouwd op abstracties boven op deze primitieven, zoals de PKCS#7/CMS-structuur (Cryptografische berichtsyntaxis).
Verwante aandachtspunten
Onderzoek heeft ertoe geleid dat Microsoft zich meer zorgen maakt over CBC-berichten die zijn gevuld met ISO 10126-equivalente opvulling wanneer het bericht een bekende of voorspelbare voettekststructuur heeft. Bijvoorbeeld inhoud die is voorbereid op basis van de regels van de W3C XML Encryption Syntax and Processing Recommendation (xmlenc, EncryptedXml). Hoewel de W3C-richtlijnen voor het ondertekenen van het bericht en versleutelen op dat moment als geschikt worden beschouwd, raadt Microsoft nu altijd aan om versleutelen en ondertekenen uit te voeren.
Toepassingsontwikkelaars moeten altijd rekening houden met het controleren van de toepasselijkheid van een asymmetrische handtekeningsleutel, omdat er geen inherente vertrouwensrelatie is tussen een asymmetrische sleutel en een willekeurig bericht.
DETAILS
In het verleden is er consensus dat het belangrijk is om belangrijke gegevens te versleutelen en te verifiëren, met behulp van middelen zoals HMAC- of RSA-handtekeningen. Er is echter minder duidelijke richtlijnen voor het sequentieren van de versleutelings- en verificatiebewerkingen. Vanwege het beveiligingsprobleem dat in dit artikel wordt beschreven, is de richtlijnen van Microsoft nu om altijd het paradigma 'encrypt-then-sign' te gebruiken. Dat wil gezegd, eerst gegevens versleutelen met behulp van een symmetrische sleutel en vervolgens een MAC- of asymmetrische handtekening berekenen via de coderingstekst (versleutelde gegevens). Wanneer u gegevens ontsleutelt, moet u het omgekeerde uitvoeren. Bevestig eerst de MAC of handtekening van de coderingstekst en ontsleutel deze.
Al meer dan 10 jaar bestaat er een klasse van beveiligingsproblemen die bekend staan als 'opvullingorakelaanvallen'. Met deze beveiligingsproblemen kan een aanvaller gegevens ontsleutelen die zijn versleuteld met symmetrische blokalgoritmen, zoals AES en 3DES, met niet meer dan 4096 pogingen per blok gegevens. Deze beveiligingsproblemen maken gebruik van het feit dat blok-coderingen het vaakst worden gebruikt met verifieerbare opvullingsgegevens aan het einde. Er is vastgesteld dat als een aanvaller kan knoeien met coderingstekst en erachter komt of de manipulatie een fout heeft veroorzaakt in de indeling van de opvulling aan het einde, de aanvaller de gegevens kan ontsleutelen.
In eerste instantie waren praktische aanvallen gebaseerd op services die verschillende foutcodes zouden retourneren op basis van of opvulling geldig was, zoals het ASP.NET beveiligingsprobleem MS10-070. Microsoft is echter van mening dat het praktisch is om vergelijkbare aanvallen uit te voeren met behulp van alleen de verschillen in timing tussen het verwerken van geldige en ongeldige opvulling.
Op voorwaarde dat het versleutelingsschema gebruikmaakt van een handtekening en dat de handtekeningverificatie wordt uitgevoerd met een vaste runtime voor een bepaalde lengte van gegevens (ongeacht de inhoud), kan de gegevensintegriteit worden geverifieerd zonder dat er informatie naar een aanvaller wordt verzonden via een zijkanaal. Omdat de integriteitscontrole geknoeid berichten weigert, wordt de opvulling oracle-bedreiging beperkt.
Richtlijn
Ten eerste raadt Microsoft aan dat alle gegevens die vertrouwelijkheid hebben, moeten worden verzonden via Transport Layer Security (TLS), de opvolger van Secure Sockets Layer (SSL).
Analyseer vervolgens uw toepassing om het volgende te doen:
- Precies begrijpen welke versleuteling u uitvoert en welke versleuteling wordt geleverd door de platforms en API's die u gebruikt.
- Zorg ervoor dat elk gebruik op elke laag van een symmetrisch blokcoderingsalgoritmen, zoals AES en 3DES, in de CBC-modus het gebruik van een integriteitscontrole met geheime sleutelgegevens bevat (een asymmetrische handtekening, een HMAC of om de coderingsmodus te wijzigen in een geverifieerde versleutelingsmodus (AE), zoals GCM of CCM).
Op basis van het huidige onderzoek wordt over het algemeen aangenomen dat wanneer de verificatie- en versleutelingsstappen onafhankelijk worden uitgevoerd voor niet-AE-versleutelingsmodi, het verifiëren van de coderingstekst (encrypt-then-sign) de beste algemene optie is. Er is echter geen geschikt antwoord op cryptografie en deze generalisatie is niet zo goed als omgeleid advies van een professionele cryptografie.
Toepassingen die hun berichtindeling niet kunnen wijzigen, maar niet-geverifieerde CBC-ontsleuteling uitvoeren, worden aangemoedigd om te proberen oplossingen op te nemen, zoals:
- Ontsleutelen zonder dat de ontsleuteling kan worden geverifieerd of verwijderd:
- Alle opvullingen die zijn toegepast, moeten nog steeds worden verwijderd of genegeerd. U verplaatst de last naar uw toepassing.
- Het voordeel is dat de opvullingsverificatie en verwijdering kunnen worden opgenomen in andere logica voor toepassingsgegevensverificatie. Als de opvullingsverificatie en gegevensverificatie in constante tijd kunnen worden uitgevoerd, wordt de bedreiging verminderd.
- Aangezien de interpretatie van de opvulling de waargenomen berichtlengte wijzigt, kan er nog steeds tijdsinformatie worden verzonden op basis van deze benadering.
- Wijzig de opvullingsmodus voor ontsleuteling in ISO10126:
- ISO10126 opvulling van ontsleuteling is compatibel met zowel PKCS7-versleuteling als ANSIX923 opvulling.
- Als u de modus wijzigt, vermindert u de kennis van het opvullingorakel in 1 byte in plaats van het hele blok. Als de inhoud echter een bekende voettekst heeft, zoals een sluitend XML-element, kunnen gerelateerde aanvallen de rest van het bericht blijven aanvallen.
- Dit voorkomt ook niet dat tekst zonder opmaak kan worden hersteld in situaties waarin de aanvaller dezelfde platte tekst kan dwingen om meerdere keren te worden versleuteld met een andere berichtverrekening.
- De evaluatie van een ontsleutelingsaanroep om het timingsignaal te dempen:
- De berekening van de bewaringstijd moet een minimum boven de maximale hoeveelheid tijd hebben die de ontsleutelingsbewerking zou duren voor elk gegevenssegment dat opvulling bevat.
- Tijdberekeningen moeten worden uitgevoerd volgens de richtlijnen bij het verkrijgen van tijdstempels voor hoge resolutie, niet met behulp Environment.TickCount van (onderhevig aan roll-over/overflow) of het aftrekken van twee systeemtijdstempels (afhankelijk van NTP-aanpassingsfouten).
- Tijdberekeningen moeten inclusief de ontsleutelingsbewerking zijn, inclusief alle mogelijke uitzonderingen in beheerde of C++-toepassingen, niet alleen opgevuld aan het einde.
- Als succes of mislukking is vastgesteld, moet de timingpoort een fout retourneren wanneer deze verloopt.
- Services die niet-geverifieerde ontsleuteling uitvoeren, moeten bewaking hebben om te detecteren dat er een overstroming van 'ongeldige' berichten is doorgekomen.
- Houd er rekening mee dat dit signaal zowel fout-positieven (legitiem beschadigde gegevens) als fout-negatieven bevat (de aanval over voldoende lange tijd uitspreiden om detectie te omzeilen).
Kwetsbare code zoeken - systeemeigen toepassingen
Voor programma's die zijn gebouwd op basis van de Windows Cryptography: Next Generation (CNG)-bibliotheek:
- De ontsleutelingsaanroep is naar BCryptDecrypt, waarbij de
BCRYPT_BLOCK_PADDING
vlag wordt opgegeven. - De sleutelgreep is geïnitialiseerd door BCryptSetProperty aan te roepen met BCRYPT_CHAINING_MODE ingesteld op
BCRYPT_CHAIN_MODE_CBC
.- Omdat
BCRYPT_CHAIN_MODE_CBC
dit de standaardwaarde is, heeft de betrokken code mogelijk geen waarde toegewezen voorBCRYPT_CHAINING_MODE
.
- Omdat
Voor programma's die zijn gebouwd op basis van de oudere Cryptografische API van Windows:
- De ontsleutelingsaanroep is CryptDecrypt met
Final=TRUE
. - De sleutelgreep is geïnitialiseerd door CryptSetKeyParam aan te roepen met KP_MODE ingesteld op
CRYPT_MODE_CBC
.- Omdat
CRYPT_MODE_CBC
dit de standaardwaarde is, heeft de betrokken code mogelijk geen waarde toegewezen voorKP_MODE
.
- Omdat
Kwetsbare code zoeken - beheerde toepassingen
- De aanroep van de ontsleuteling is aan de CreateDecryptor() of CreateDecryptor(Byte[], Byte[]) methoden op System.Security.Cryptography.SymmetricAlgorithm.
- Dit omvat de volgende afgeleide typen in .NET, maar kunnen ook typen van derden bevatten:
- De SymmetricAlgorithm.Padding eigenschap is ingesteld op PaddingMode.PKCS7, PaddingMode.ANSIX923of PaddingMode.ISO10126.
- Omdat PaddingMode.PKCS7 dit de standaardwaarde is, heeft de betrokken code de SymmetricAlgorithm.Padding eigenschap mogelijk nooit toegewezen.
- De SymmetricAlgorithm.Mode eigenschap is ingesteld op CipherMode.CBC
- Omdat CipherMode.CBC dit de standaardwaarde is, heeft de betrokken code de SymmetricAlgorithm.Mode eigenschap mogelijk nooit toegewezen.
Kwetsbare code zoeken - syntaxis van cryptografische berichten
Een niet-geverifieerd CMS EnvelopedData-bericht waarvan de versleutelde inhoud gebruikmaakt van de CBC-modus van AES (2.16.840.1.101.3.4.1.2, 2.16.840.1.101.3.4.1.22, 2.16.840.1.101.3.4.1.42), DES (1.3.14.3.2.7), 3DES (1.2.840.113549.3.7) of RC2 (1.2.840.113549.3.2) is kwetsbaar, evenals berichten die gebruikmaken van andere blok-coderingsalgoritmen in de CBC-modus.
Hoewel streamcoderingen niet vatbaar zijn voor dit specifieke beveiligingsprobleem, raadt Microsoft aan altijd de gegevens te verifiëren bij het inspecteren van de waarde ContentEncryptionAlgorithm.
Voor beheerde toepassingen kan een CMS EnvelopedData-blob worden gedetecteerd als elke waarde die wordt doorgegeven aan System.Security.Cryptography.Pkcs.EnvelopedCms.Decode(Byte[]).
Voor systeemeigen toepassingen kan een CMS EnvelopedData-blob worden gedetecteerd als elke waarde die wordt geleverd aan een CMS-handle via CryptMsgUpdate waarvan de resulterende CMSG_TYPE_PARAM is CMSG_ENVELOPED
en/of de CMS-ingang later een CMSG_CTRL_DECRYPT
instructie via CryptMsgControl wordt verzonden.
Voorbeeld van kwetsbare code - beheerd
Deze methode leest een cookie en ontsleutelt deze en er is geen controle op gegevensintegriteit zichtbaar. Daarom kan de inhoud van een cookie die door deze methode wordt gelezen, worden aangevallen door de gebruiker die deze heeft ontvangen of door een aanvaller die de versleutelde cookiewaarde heeft verkregen.
private byte[] DecryptCookie(string cookieName)
{
HttpCookie cookie = Request.Cookies[cookieName];
if (cookie == null)
{
return null;
}
using (ICryptoTransform decryptor = _aes.CreateDecryptor())
using (MemoryStream memoryStream = new MemoryStream())
using (CryptoStream cryptoStream =
new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Write))
{
byte[] readCookie = Convert.FromBase64String(cookie.Value);
cryptoStream.Write(readCookie, 0, readCookie.Length);
cryptoStream.FlushFinalBlock();
return memoryStream.ToArray();
}
}
Voorbeeldcode volgens aanbevolen procedures - beheerd
De volgende voorbeeldcode maakt gebruik van een niet-standaard berichtindeling van
cipher_algorithm_id || hmac_algorithm_id || hmac_tag || iv || ciphertext
waarbij de cipher_algorithm_id
id's en hmac_algorithm_id
algoritme-id's toepassings-lokale (niet-standaard) weergaven van deze algoritmen zijn. Deze id's kunnen zinvol zijn in andere delen van uw bestaande berichtenprotocol in plaats van als een bare samengevoegde bytestream.
In dit voorbeeld wordt ook één hoofdsleutel gebruikt om zowel een versleutelingssleutel als een HMAC-sleutel af te leiden. Dit wordt zowel als een gemak geboden voor het omzetten van een singly-keyed toepassing in een toepassing met twee sleutels en om de twee sleutels als verschillende waarden aan te moedigen. Het garandeert verder dat de HMAC-sleutel en versleutelingssleutel niet kunnen worden gesynchroniseerd.
Dit voorbeeld accepteert geen versleuteling Stream of ontsleuteling. De huidige gegevensindeling maakt het versleutelen van één wachtwoord moeilijk omdat de hmac_tag
waarde voorafgaat aan de coderingstekst. Deze indeling is echter gekozen omdat alle elementen van vaste grootte aan het begin worden bewaard om de parser eenvoudiger te houden. Met deze gegevensindeling is ontsleuteling met één wachtwoord mogelijk, hoewel een implementeerfunctie wordt gewaarschuwd om GetHashAndReset aan te roepen en het resultaat te controleren voordat TransformFinalBlock wordt aangeroepen. Als streamingversleuteling belangrijk is, is mogelijk een andere AE-modus vereist.
// ==++==
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// Shared under the terms of the Microsoft Public License,
// https://opensource.org/licenses/MS-PL
//
// ==--==
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
namespace Microsoft.Examples.Cryptography
{
public enum AeCipher : byte
{
Unknown,
Aes256CbcPkcs7,
}
public enum AeMac : byte
{
Unknown,
HMACSHA256,
HMACSHA384,
}
/// <summary>
/// Provides extension methods to make HashAlgorithm look like .NET Core's
/// IncrementalHash
/// </summary>
internal static class IncrementalHashExtensions
{
public static void AppendData(this HashAlgorithm hash, byte[] data)
{
hash.TransformBlock(data, 0, data.Length, null, 0);
}
public static void AppendData(
this HashAlgorithm hash,
byte[] data,
int offset,
int length)
{
hash.TransformBlock(data, offset, length, null, 0);
}
public static byte[] GetHashAndReset(this HashAlgorithm hash)
{
hash.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
return hash.Hash;
}
}
public static partial class AuthenticatedEncryption
{
/// <summary>
/// Use <paramref name="masterKey"/> to derive two keys (one cipher, one HMAC)
/// to provide authenticated encryption for <paramref name="message"/>.
/// </summary>
/// <param name="masterKey">The master key from which other keys derive.</param>
/// <param name="message">The message to encrypt</param>
/// <returns>
/// A concatenation of
/// [cipher algorithm+chainmode+padding][mac algorithm][authtag][IV][ciphertext],
/// suitable to be passed to <see cref="Decrypt"/>.
/// </returns>
/// <remarks>
/// <paramref name="masterKey"/> should be a 128-bit (or bigger) value generated
/// by a secure random number generator, such as the one returned from
/// <see cref="RandomNumberGenerator.Create()"/>.
/// This implementation chooses to block deficient inputs by length, but does not
/// make any attempt at discerning the randomness of the key.
///
/// If the master key is being input by a prompt (like a password/passphrase)
/// then it should be properly turned into keying material via a Key Derivation
/// Function like PBKDF2, represented by Rfc2898DeriveBytes. A 'password' should
/// never be simply turned to bytes via an Encoding class and used as a key.
/// </remarks>
public static byte[] Encrypt(byte[] masterKey, byte[] message)
{
if (masterKey == null)
throw new ArgumentNullException(nameof(masterKey));
if (masterKey.Length < 16)
throw new ArgumentOutOfRangeException(
nameof(masterKey),
"Master Key must be at least 128 bits (16 bytes)");
if (message == null)
throw new ArgumentNullException(nameof(message));
// First, choose an encryption scheme.
AeCipher aeCipher = AeCipher.Aes256CbcPkcs7;
// Second, choose an authentication (message integrity) scheme.
//
// In this example we use the master key length to change from HMACSHA256 to
// HMACSHA384, but that is completely arbitrary. This mostly represents a
// "cryptographic needs change over time" scenario.
AeMac aeMac = masterKey.Length < 48 ? AeMac.HMACSHA256 : AeMac.HMACSHA384;
// It's good to be able to identify what choices were made when a message was
// encrypted, so that the message can later be decrypted. This allows for
// future versions to add support for new encryption schemes, but still be
// able to read old data. A practice known as "cryptographic agility".
//
// This is similar in practice to PKCS#7 messaging, but this uses a
// private-scoped byte rather than a public-scoped Object IDentifier (OID).
// Please note that the scheme in this example adheres to no particular
// standard, and is unlikely to survive to a more complete implementation in
// the .NET Framework.
//
// You may be well-served by prepending a version number byte to this
// message, but may want to avoid the value 0x30 (the leading byte value for
// DER-encoded structures such as X.509 certificates and PKCS#7 messages).
byte[] algorithmChoices = { (byte)aeCipher, (byte)aeMac };
byte[] iv;
byte[] cipherText;
byte[] tag;
// Using our algorithm choices, open an HMAC (as an authentication tag
// generator) and a SymmetricAlgorithm which use different keys each derived
// from the same master key.
//
// A custom implementation may very well have distinctly managed secret keys
// for the MAC and cipher, this example merely demonstrates the master to
// derived key methodology to encourage key separation from the MAC and
// cipher keys.
using (HMAC tagGenerator = GetMac(aeMac, masterKey))
{
using (SymmetricAlgorithm cipher = GetCipher(aeCipher, masterKey))
using (ICryptoTransform encryptor = cipher.CreateEncryptor())
{
// Since no IV was provided, a random one has been generated
// during the call to CreateEncryptor.
//
// But note that it only does the auto-generation once. If the cipher
// object were used again, a call to GenerateIV would have been
// required.
iv = cipher.IV;
cipherText = Transform(encryptor, message, 0, message.Length);
}
// The IV and ciphertext both need to be included in the MAC to prevent
// tampering.
//
// By including the algorithm identifiers, we have technically moved from
// simple Authenticated Encryption (AE) to Authenticated Encryption with
// Additional Data (AEAD). By including the algorithm identifiers in the
// MAC, it becomes harder for an attacker to change them as an attempt to
// perform a downgrade attack.
//
// If you've added a data format version field, it can also be included
// in the MAC to further inhibit an attacker's options for confusing the
// data processor into believing the tampered message is valid.
tagGenerator.AppendData(algorithmChoices);
tagGenerator.AppendData(iv);
tagGenerator.AppendData(cipherText);
tag = tagGenerator.GetHashAndReset();
}
// Build the final result as the concatenation of everything except the keys.
int totalLength =
algorithmChoices.Length +
tag.Length +
iv.Length +
cipherText.Length;
byte[] output = new byte[totalLength];
int outputOffset = 0;
Append(algorithmChoices, output, ref outputOffset);
Append(tag, output, ref outputOffset);
Append(iv, output, ref outputOffset);
Append(cipherText, output, ref outputOffset);
Debug.Assert(outputOffset == output.Length);
return output;
}
/// <summary>
/// Reads a message produced by <see cref="Encrypt"/> after verifying it hasn't
/// been tampered with.
/// </summary>
/// <param name="masterKey">The master key from which other keys derive.</param>
/// <param name="cipherText">
/// The output of <see cref="Encrypt"/>: a concatenation of a cipher ID, mac ID,
/// authTag, IV, and cipherText.
/// </param>
/// <returns>The decrypted content.</returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="masterKey"/> is <c>null</c>.
/// </exception>
/// <exception cref="ArgumentNullException">
/// <paramref name="cipherText"/> is <c>null</c>.
/// </exception>
/// <exception cref="CryptographicException">
/// <paramref name="cipherText"/> identifies unknown algorithms, is not long
/// enough, fails a data integrity check, or fails to decrypt.
/// </exception>
/// <remarks>
/// <paramref name="masterKey"/> should be a 128-bit (or larger) value
/// generated by a secure random number generator, such as the one returned from
/// <see cref="RandomNumberGenerator.Create()"/>. This implementation chooses to
/// block deficient inputs by length, but doesn't make any attempt at
/// discerning the randomness of the key.
///
/// If the master key is being input by a prompt (like a password/passphrase),
/// then it should be properly turned into keying material via a Key Derivation
/// Function like PBKDF2, represented by Rfc2898DeriveBytes. A 'password' should
/// never be simply turned to bytes via an Encoding class and used as a key.
/// </remarks>
public static byte[] Decrypt(byte[] masterKey, byte[] cipherText)
{
// This example continues the .NET practice of throwing exceptions for
// failures. If you consider message tampering to be normal (and thus
// "not exceptional") behavior, you may like the signature
// bool Decrypt(byte[] messageKey, byte[] cipherText, out byte[] message)
// better.
if (masterKey == null)
throw new ArgumentNullException(nameof(masterKey));
if (masterKey.Length < 16)
throw new ArgumentOutOfRangeException(
nameof(masterKey),
"Master Key must be at least 128 bits (16 bytes)");
if (cipherText == null)
throw new ArgumentNullException(nameof(cipherText));
// The format of this message is assumed to be public, so there's no harm in
// saying ahead of time that the message makes no sense.
if (cipherText.Length < 2)
{
throw new CryptographicException();
}
// Use the message algorithm headers to determine what cipher algorithm and
// MAC algorithm are going to be used. Since the same Key Derivation
// Functions (KDFs) are being used in Decrypt as Encrypt, the keys are also
// the same.
AeCipher aeCipher = (AeCipher)cipherText[0];
AeMac aeMac = (AeMac)cipherText[1];
using (SymmetricAlgorithm cipher = GetCipher(aeCipher, masterKey))
using (HMAC tagGenerator = GetMac(aeMac, masterKey))
{
int blockSizeInBytes = cipher.BlockSize / 8;
int tagSizeInBytes = tagGenerator.HashSize / 8;
int headerSizeInBytes = 2;
int tagOffset = headerSizeInBytes;
int ivOffset = tagOffset + tagSizeInBytes;
int cipherTextOffset = ivOffset + blockSizeInBytes;
int cipherTextLength = cipherText.Length - cipherTextOffset;
int minLen = cipherTextOffset + blockSizeInBytes;
// Again, the minimum length is still assumed to be public knowledge,
// nothing has leaked out yet. The minimum length couldn't just be calculated
// without reading the header.
if (cipherText.Length < minLen)
{
throw new CryptographicException();
}
// It's very important that the MAC be calculated and verified before
// proceeding to decrypt the ciphertext, as this prevents any sort of
// information leaking out to an attacker.
//
// Don't include the tag in the calculation, though.
// First, everything before the tag (the cipher and MAC algorithm ids)
tagGenerator.AppendData(cipherText, 0, tagOffset);
// Skip the data before the tag and the tag, then read everything that
// remains.
tagGenerator.AppendData(
cipherText,
tagOffset + tagSizeInBytes,
cipherText.Length - tagSizeInBytes - tagOffset);
byte[] generatedTag = tagGenerator.GetHashAndReset();
// The time it took to get to this point has so far been a function only
// of the length of the data, or of non-encrypted values (e.g. it could
// take longer to prepare the *key* for the HMACSHA384 MAC than the
// HMACSHA256 MAC, but the algorithm choice wasn't a secret).
//
// If the verification of the authentication tag aborts as soon as a
// difference is found in the byte arrays then your program may be
// acting as a timing oracle which helps an attacker to brute-force the
// right answer for the MAC. So, it's very important that every possible
// "no" (2^256-1 of them for HMACSHA256) be evaluated in as close to the
// same amount of time as possible. For this, we call CryptographicEquals
if (!CryptographicEquals(
generatedTag,
0,
cipherText,
tagOffset,
tagSizeInBytes))
{
// Assuming every tampered message (of the same length) took the same
// amount of time to process, we can now safely say
// "this data makes no sense" without giving anything away.
throw new CryptographicException();
}
// Restore the IV into the symmetricAlgorithm instance.
byte[] iv = new byte[blockSizeInBytes];
Buffer.BlockCopy(cipherText, ivOffset, iv, 0, iv.Length);
cipher.IV = iv;
using (ICryptoTransform decryptor = cipher.CreateDecryptor())
{
return Transform(
decryptor,
cipherText,
cipherTextOffset,
cipherTextLength);
}
}
}
private static byte[] Transform(
ICryptoTransform transform,
byte[] input,
int inputOffset,
int inputLength)
{
// Many of the implementations of ICryptoTransform report true for
// CanTransformMultipleBlocks, and when the entire message is available in
// one shot this saves on the allocation of the CryptoStream and the
// intermediate structures it needs to properly chunk the message into blocks
// (since the underlying stream won't always return the number of bytes
// needed).
if (transform.CanTransformMultipleBlocks)
{
return transform.TransformFinalBlock(input, inputOffset, inputLength);
}
// If our transform couldn't do multiple blocks at once, let CryptoStream
// handle the chunking.
using (MemoryStream messageStream = new MemoryStream())
using (CryptoStream cryptoStream =
new CryptoStream(messageStream, transform, CryptoStreamMode.Write))
{
cryptoStream.Write(input, inputOffset, inputLength);
cryptoStream.FlushFinalBlock();
return messageStream.ToArray();
}
}
/// <summary>
/// Open a properly configured <see cref="SymmetricAlgorithm"/> conforming to the
/// scheme identified by <paramref name="aeCipher"/>.
/// </summary>
/// <param name="aeCipher">The cipher mode to open.</param>
/// <param name="masterKey">The master key from which other keys derive.</param>
/// <returns>
/// A SymmetricAlgorithm object with the right key, cipher mode, and padding
/// mode; or <c>null</c> on unknown algorithms.
/// </returns>
private static SymmetricAlgorithm GetCipher(AeCipher aeCipher, byte[] masterKey)
{
SymmetricAlgorithm symmetricAlgorithm;
switch (aeCipher)
{
case AeCipher.Aes256CbcPkcs7:
symmetricAlgorithm = Aes.Create();
// While 256-bit, CBC, and PKCS7 are all the default values for these
// properties, being explicit helps comprehension more than it hurts
// performance.
symmetricAlgorithm.KeySize = 256;
symmetricAlgorithm.Mode = CipherMode.CBC;
symmetricAlgorithm.Padding = PaddingMode.PKCS7;
break;
default:
// An algorithm we don't understand
throw new CryptographicException();
}
// Instead of using the master key directly, derive a key for our chosen
// HMAC algorithm based upon the master key.
//
// Since none of the symmetric encryption algorithms currently in .NET
// support key sizes greater than 256-bit, we can use HMACSHA256 with
// NIST SP 800-108 5.1 (Counter Mode KDF) to derive a value that is
// no smaller than the key size, then Array.Resize to trim it down as
// needed.
using (HMAC hmac = new HMACSHA256(masterKey))
{
// i=1, Label=ASCII(cipher)
byte[] cipherKey = hmac.ComputeHash(
new byte[] { 1, 99, 105, 112, 104, 101, 114 });
// Resize the array to the desired keysize. KeySize is in bits,
// and Array.Resize wants the length in bytes.
Array.Resize(ref cipherKey, symmetricAlgorithm.KeySize / 8);
symmetricAlgorithm.Key = cipherKey;
}
return symmetricAlgorithm;
}
/// <summary>
/// Open a properly configured <see cref="HMAC"/> conforming to the scheme
/// identified by <paramref name="aeMac"/>.
/// </summary>
/// <param name="aeMac">The message authentication mode to open.</param>
/// <param name="masterKey">The master key from which other keys derive.</param>
/// <returns>
/// An HMAC object with the proper key, or <c>null</c> on unknown algorithms.
/// </returns>
private static HMAC GetMac(AeMac aeMac, byte[] masterKey)
{
HMAC hmac;
switch (aeMac)
{
case AeMac.HMACSHA256:
hmac = new HMACSHA256();
break;
case AeMac.HMACSHA384:
hmac = new HMACSHA384();
break;
default:
// An algorithm we don't understand
throw new CryptographicException();
}
// Instead of using the master key directly, derive a key for our chosen
// HMAC algorithm based upon the master key.
// Since the output size of the HMAC is the same as the ideal key size for
// the HMAC, we can use the master key over a fixed input once to perform
// NIST SP 800-108 5.1 (Counter Mode KDF):
hmac.Key = masterKey;
// i=1, Context=ASCII(MAC)
byte[] newKey = hmac.ComputeHash(new byte[] { 1, 77, 65, 67 });
hmac.Key = newKey;
return hmac;
}
// A simple helper method to ensure that the offset (writePos) always moves
// forward with new data.
private static void Append(byte[] newData, byte[] combinedData, ref int writePos)
{
Buffer.BlockCopy(newData, 0, combinedData, writePos, newData.Length);
writePos += newData.Length;
}
/// <summary>
/// Compare the contents of two arrays in an amount of time which is only
/// dependent on <paramref name="length"/>.
/// </summary>
/// <param name="a">An array to compare to <paramref name="b"/>.</param>
/// <param name="aOffset">
/// The starting position within <paramref name="a"/> for comparison.
/// </param>
/// <param name="b">An array to compare to <paramref name="a"/>.</param>
/// <param name="bOffset">
/// The starting position within <paramref name="b"/> for comparison.
/// </param>
/// <param name="length">
/// The number of bytes to compare between <paramref name="a"/> and
/// <paramref name="b"/>.</param>
/// <returns>
/// <c>true</c> if both <paramref name="a"/> and <paramref name="b"/> have
/// sufficient length for the comparison and all of the applicable values are the
/// same in both arrays; <c>false</c> otherwise.
/// </returns>
/// <remarks>
/// An "insufficient data" <c>false</c> response can happen early, but otherwise
/// a <c>true</c> or <c>false</c> response take the same amount of time.
/// </remarks>
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
private static bool CryptographicEquals(
byte[] a,
int aOffset,
byte[] b,
int bOffset,
int length)
{
Debug.Assert(a != null);
Debug.Assert(b != null);
Debug.Assert(length >= 0);
int result = 0;
if (a.Length - aOffset < length || b.Length - bOffset < length)
{
return false;
}
unchecked
{
for (int i = 0; i < length; i++)
{
// Bitwise-OR of subtraction has been found to have the most
// stable execution time.
//
// This cannot overflow because bytes are 1 byte in length, and
// result is 4 bytes.
// The OR propagates all set bytes, so the differences are only
// present in the lowest byte.
result = result | (a[i + aOffset] - b[i + bOffset]);
}
}
return result == 0;
}
}
}