Az AES, mint szabvány 2001 novemberében került elfogadásra, mint a DES valódi utódja. A neve szintén mozaikszó, ami az Advanced Encryption Standard szavakat rövidíti.
A szabvány mögött álló algoritmust egy versenyen választották ki. 1997 szeptemberében az NIST versenyt hirdetett, aminek a célja az volt, hogy a fejlesztők alkossák meg a DES utódját. Az algoritmusoknak az alábbi feltételeknek kellett megfelelniük:
- 128 bites blokkméretekben dolgozzon
- 128, 192 és 256 bites kulcsméretet is támogasson
Ezen két feltételnek megfelelő algoritmus nem igen volt akkoriban. A versenyzőknek kilenc hónap állt rendelkezésükre és a beérkezett algoritmusokat egy szakértő csoport vizsgálta a biztonság és egyéb szempontok alapján. Vizsgált szempontok voltak például:
- Az algoritmus sebessége kevés memória és/vagy lassú processzor esetén
- Az algoritmus implementálhatósága különböző architektúrák esetén
- Cél hardver építésének a lehetősége
A legtöbb beküldött algoritmus a biztonság ponton elvérzett. A nyerő algoritmust végül több konferencián átívelő tanácskozás után választotta ki egy szakértő bizottság. A nyertes algoritmust két belga matematikus, Joan Daemen1 és Vincent Rijmen alkotta meg, amit Rijndael-nek neveztek el a neveikből alkotva.
Az algoritmus megfelelő használat mellett nagyon biztonságosnak tekinthető és működésében nem hasonlítható az előtte alkotott algoritmusokhoz, mivel ez mátrix transzformációk sorozatával működik. Fixen 128 bites blokkmérettel dolgozik, a szabvány követelményeinek megfelelően 128, 192 és 256 bites kulcsméretet is támogat, azonban a kulcsmérete szinte a végtelenségig növelhető.
Utóbbi tényező erős döntő faktor volt a kiválasztásánál. A 192 és 256 bites üzemmód a szabványba kifejezetten a DES feltöréséből tanultak miatt lett beépítve: ha 128 biten feltörnék az algoritmust, akkor több kör nélkül könnyen lehessen váltani egy biztonságosabb megoldásra.
Ez azonban nem fog egyhamar megtörténni, mivel több analízis szerint is 128 bites titkosítás esetén 126 bites kulcstérről beszélünk, ami megközelítőleg 8,5 x 1037 kombinációt jelent.
Mint minden szoftver esetén, a biztonság nagymértékben függ az implementáció milyenségétől. Éppen ezért az implementációk teszteléséhez az NIST biztosít egy tesztcsomagot, amivel bárki tesztelheti az algoritmus implementációját.
Az AES a legtöbb modern processzor esetén hardver gyorsított egy speciális utasításkészlet, az AES-NI segítségével. Ez az utasításkészlet bármely generációba tartozó i5, i7 és i9 Intel processzor esetén támogatott, illetve a 6., 2015-ben megjelent generáció óta i3 processzorok esetén is. AMD oldalon bármelyik 2011 után gyártott processzor támogatja.
ARM esetén a V8 és a V8-a utasításkészlet óta szintén hardveresen támogatott.
Az AES használata .NET esetén
Az AES használata nem meglepő módon az AES osztályon keresztül lehetséges. Az osztály statikus Create() metódusának meghívásával tudunk példányt létrehozni belőle. A Factory megoldás azért használatos, mivel a .NET keretrendszer különböző platformok esetén különböző könyvtárakra épít. Az osztály implementálja az IDisposable felületet, mivel a titkosítást nagy valószínűséggel egy, az operációs rendszer által biztosított implementáció fogja elvégezni.
Ezek után a használat módja erősen függ a választott blokk üzemmódtól. Az alábbi metódus a fájl hasheléshez hasonló API segítségével egy fájl titkosítását teszi lehetővé CBC üzemmóddal:
public static async Task AesCbcEncrypt(this Stream plainInput,
Stream encryptedOutput,
byte[] pass,
IProgress<float>? progress = null,
CancellationToken cancellationToken = default)
{
if (pass.Length < BlockSizeInBytes)
throw new ArgumentOutOfRangeException(nameof(pass), $"{nameof(pass)} length must be at least 16 bytes");
long position = 0;
long size = plainInput.Length;
int read = 0;
using (Aes aes = Aes.Create())
{
aes.Key = pass;
aes.Padding = PaddingMode.PKCS7;
aes.IV = RandomNumberGenerator.GetBytes(BlockSizeInBytes);
aes.Mode = CipherMode.CBC;
using (ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV))
{
using (var crypto = new CryptoStream(encryptedOutput, encryptor, CryptoStreamMode.Write, true))
{
byte[] buffer = RandomNumberGenerator.GetBytes(BufferSize);
crypto.Write(buffer, 0, buffer.Length);
position += buffer.Length;
do
{
cancellationToken.ThrowIfCancellationRequested();
read = await plainInput.ReadAsync(buffer, cancellationToken);
await crypto.WriteAsync(buffer, 0, read, cancellationToken);
position += read;
progress?.Report((float)position / size);
}
while (read != 0);
}
}
}
}
A CBC üzemmód használatához be kell állítanunk a Padding működését, ami az utolsó blokk méretét bővíti fel a blokkméretre, ha éppen nem annak többszöröse lenne a bemenet. Ezt a PKCS7 értékre érdemes állítani, mivel ez szintén egy RFC számmal (RFC 2315) rendelkező ajánlott algoritmus.
A kitöltésnek választhatnánk még nullákat (Zeros), ISO10126 és ANSIX923 értékeket is CBC esetén, azonban a nullákkal való feltöltés és az ISO10126 használata kifejezetten ellenjavallott, mivel az első nem tudott sosem biztonságot garantálni, az ISO10126 pedig 2007 óta nem tekinthető biztonságosnak és leginkább kompatibilitási okokból maradt a keretrendszerben. Az ANSIX923 megfelelő biztonságot nyújt, de ha lehetőségünk van használjuk inkább a PKCS7 algoritmust, mivel ez több lehetőséget biztosít a blokk integritásának ellenőrzésére, ami végeredményben erősebb titkosítást jelent.
A kulcs és a kitöltés után szükségünk lesz egy inicializációs vektorra. Ennek a méretének meg kell egyeznie az algoritmus blokk méretével és a legjobb, ha véletlenszerűen generáljuk.
Ezek után létrehozhatunk a CreateEncryptor metódus hívással egy ICryptoTransform példányt. Ez az interfész kifejezetten a transzformációk absztrakciójáért felelős. Az általa definiált TransformBlock és TransformFinalBlock metódusok a korábbi hash-es példából ismerősek lehetnek. Itt azonban nem direktben használjuk őket, mivel a hívások sorrendje eltérő titkosítás és visszafejtés során. Ezt a CryptoStream osztály absztraktálja nekünk.
Ezt követően a titkosítás már csak valójában egy stream másolás, aminek az eredményeként a cél stream-ben már a titkosított adat jelenik meg. A CBC mód miatt az első 4KiB buffer méretű adatcsomagot véletlenül generált értékre állítjuk.
Visszafejtéskor ezt egyszerűen ignoráljuk, mivel az inicializációs vektor ismeretének hiányában amúgy sem tudnánk visszafejteni, de az üzenetnek nem is képezi részét. A visszafejtő metódus kísértetiesen azonos szignatúrával rendelkezik és működésében is nagyon hasonló. Különösebb magyarázatra csupán a DiscardFirstBlock metódus szolgálhat. Ez, mint ahogy a neve is mutatja az első 4KiB blokkot hagyja figyelmen kívül. Azonban az implementációjában láthatunk egy ciklust, annak ellenére, hogy 4KiB-ot (BufferSize konstans által definiált érték) olvasunk. Erre azért van szükség, mert a Stream mögött lévő operációs rendszer implementáció nem garantálja, hogy ennyi adatot egyszerre be tudunk olvasni. Ennek számos oka lehet (eltérő bufferek, operációs rendszer implementációja, fájlrendszer, stb…), vagyis a BufferSize konstans értékének a csökkentése sem oldaná meg a problémát teljesen.
public static async Task AesCbcDecrypt(this Stream encryptedInput,
Stream plainOutput,
byte[] pass,
IProgress<float>? progress = null,
CancellationToken cancellationToken = default)
{
if (pass.Length < BlockSizeInBytes)
throw new ArgumentOutOfRangeException(nameof(pass), $"{nameof(pass)} length must be at least 16 bytes");
long position = 0;
long size = encryptedInput.Length;
int read = 0;
using (Aes aes = Aes.Create())
{
aes.Key = pass;
aes.Padding = PaddingMode.PKCS7;
aes.IV = RandomNumberGenerator.GetBytes(BlockSizeInBytes);
aes.Mode = CipherMode.CBC;
using (ICryptoTransform encryptor = aes.CreateDecryptor(aes.Key, aes.IV))
{
using (var crypto = new CryptoStream(encryptedInput, encryptor, CryptoStreamMode.Read, true))
{
byte[] buffer = new byte[BufferSize];
DiscardFirstBlock(crypto, buffer);
position += BufferSize;
do
{
cancellationToken.ThrowIfCancellationRequested();
read = await crypto.ReadAsync(buffer, cancellationToken);
await plainOutput.WriteAsync(buffer, 0, read, cancellationToken);
position += read;
progress?.Report((float)position / size);
}
while (read != 0);
}
}
}
}
private static void DiscardFirstBlock(CryptoStream crypto, byte[] buffer)
{
int remain = BufferSize;
while(remain > 0)
{
remain -= crypto.Read(buffer, 0, remain);
}
}
A fenti példában byte[] tömbként kell megadni a kulcsot. Azonban ha jelszó alapon szeretnénk megoldani a titkosítást, akkor valamilyen Hash-t alkalmazhatnánk, például SHA256-ot, ami 256 bites kulcsot generál, vagy akár használhatnánk egy pont ilyen célra kitalált algoritmust is, mint a PKDF2.
Támogatott üzemmódok
A fenti példában a CBC üzemmód az elterjedtsége miatt került kiválasztásra elsődlegesen. A második ok viszont az, hogy a működési mód kiválasztásához használt CipherMode enum és a .NET nem támogatja jelenleg a CTR üzemmódot. Viszont a CBC, CFB, CTS, ECB és OFB üzemmódok támogatottak.
Ha CTR üzemmódra van szükségünk, akkor azt magunknak kell leimplementálnunk ECB üzemmód és egy számláló használatával. Egy lehetséges minta implementáció ennek a megvalósítására:
public sealed class CounterModeCryptoTransform : ICryptoTransform
{
private readonly byte[] _nonceAndCounter;
private readonly ICryptoTransform _counterEncryptor;
private readonly Queue<byte> _xorMask = new Queue<byte>();
private readonly SymmetricAlgorithm _symmetricAlgorithm;
private byte[]? _counterModeBlock;
private ulong _counter;
public CounterModeCryptoTransform(SymmetricAlgorithm symmetricAlgorithm,
byte[] key,
ulong nonce,
ulong counter)
{
if (key == null) throw new ArgumentNullException(nameof(key));
_symmetricAlgorithm = symmetricAlgorithm ?? throw new ArgumentNullException(nameof(symmetricAlgorithm));
_counter = counter;
_nonceAndCounter = new byte[16];
BitConverter.TryWriteBytes(_nonceAndCounter, nonce);
BitConverter.TryWriteBytes(new Span<byte>(_nonceAndCounter, sizeof(ulong), sizeof(ulong)), counter);
var zeroIv = new byte[_symmetricAlgorithm.BlockSize / 8];
_counterEncryptor = symmetricAlgorithm.CreateEncryptor(key, zeroIv);
}
private bool NeedMoreXorMaskBytes()
{
return _xorMask.Count == 0;
}
private void EncryptCounterThenIncrement()
{
_counterModeBlock ??= new byte[_symmetricAlgorithm.BlockSize / 8];
_counterEncryptor.TransformBlock(_nonceAndCounter, 0, _nonceAndCounter.Length, _counterModeBlock, 0);
IncrementCounter();
foreach (var b in _counterModeBlock)
{
_xorMask.Enqueue(b);
}
}
private void IncrementCounter()
{
_counter++;
var span = new Span<byte>(_nonceAndCounter, sizeof(ulong), sizeof(ulong));
BitConverter.TryWriteBytes(span, _counter);
}
public bool CanReuseTransform => false;
public bool CanTransformMultipleBlocks => true;
public int InputBlockSize => _symmetricAlgorithm.BlockSize / 8;
public int OutputBlockSize => _symmetricAlgorithm.BlockSize / 8;
public void Dispose()
{
_counterEncryptor.Dispose();
}
public int TransformBlock(byte[] inputBuffer,
int inputOffset,
int inputCount,
byte[] outputBuffer,
int outputOffset)
{
for (var i = 0; i < inputCount; i++)
{
if (NeedMoreXorMaskBytes())
{
EncryptCounterThenIncrement();
}
var mask = _xorMask.Dequeue();
outputBuffer[outputOffset + i] = (byte)(inputBuffer[inputOffset + i] ^ mask);
}
return inputCount;
}
public byte[] TransformFinalBlock(byte[] inputBuffer, int inputOffset, int inputCount)
{
var output = new byte[inputCount];
TransformBlock(inputBuffer, inputOffset, inputCount, output, 0);
return output;
}
}
public sealed class AesCounterMode : SymmetricAlgorithm
{
private readonly ulong _nonce;
private readonly ulong _counter;
private readonly Aes _aes;
public AesCounterMode(byte[] nonce, ulong counter)
{
_aes = Aes.Create();
_aes.Mode = CipherMode.ECB;
_aes.Padding = PaddingMode.None;
_nonce = ConvertNonce(nonce);
_counter = counter;
}
private static ulong ConvertNonce(byte[] nonce)
{
if (nonce == null) throw new ArgumentNullException(nameof(nonce));
if (nonce.Length < sizeof(ulong)) throw new ArgumentException($"{nameof(nonce)} must have at least {sizeof(ulong)} bytes");
return BitConverter.ToUInt64(nonce);
}
public override ICryptoTransform CreateDecryptor(byte[] rgbKey, byte[]? rgbIV)
{
return new CounterModeCryptoTransform(_aes, rgbKey, _nonce, _counter);
}
public override ICryptoTransform CreateEncryptor(byte[] rgbKey, byte[]? rgbIV)
{
return new CounterModeCryptoTransform(_aes, rgbKey, _nonce, _counter);
}
public override void GenerateIV()
{
// IV not needed in Counter Mode
}
public override void GenerateKey()
{
_aes.GenerateKey();
}
}
Az AesCounterMode osztály megfelelően paraméterezi az AES algoritmust a CTR üzemmódban való működéshez, míg a CounterModeCryptoTransform egy kriptográfiai transzformációt definiál, ami a CTR üzemmódot valósítja meg. Ezeket az osztályokat felhasználva egy stream titkosító megoldás CTR üzemmódban:
public static async Task AesCtrEncrypt(this Stream plainInput,
Stream encryptedOutput,
byte[] pass,
byte[] nonce,
IProgress<float>? progress = null,
CancellationToken cancellationToken = default)
{
if (pass.Length < BlockSizeInBytes)
throw new ArgumentOutOfRangeException(nameof(pass), $"{nameof(pass)} length must be at least 16 bytes");
if (nonce.Length < 8)
throw new ArgumentOutOfRangeException(nameof(pass), $"{nameof(pass)} length must be at least 8 bytes");
long position = 0;
long size = plainInput.Length;
int read = 0;
using (var aesCtr = new AesCounterMode(nonce, 0))
{
using (var encryptor = aesCtr.CreateEncryptor(pass, null))
{
byte[] buffer = new byte[BufferSize];
byte[] outbuffer = new byte[BufferSize];
do
{
cancellationToken.ThrowIfCancellationRequested();
read = await plainInput.ReadAsync(buffer, cancellationToken);
encryptor.TransformBlock(buffer, 0, read, outbuffer, 0);
await encryptedOutput.WriteAsync(outbuffer, 0, read, cancellationToken);
position += read;
progress?.Report((float)position / size);
}
while (read != 0);
}
}
}
A metódusban a korábbi példákhoz hasonlóan a pass a kulcs, amit használni szeretnénk, míg a nonce érték a számláló inicializációs értéke, aminek legalább 8 byte méretűnek kell lennie. A CTR üzemmód érdekessége, hogy nincs szüksége CryptoStream példányra és ugyanaz a metódus, ami a titkosítást végzi alkalmazható a visszafejtésre is. Ha a titkosított stream-en hívjuk meg az AesCtrEncrypt metódust, akkor a visszafejtett szöveget kapjuk meg.
GCM üzemmód
private static (AesGcm gcm, byte[] iv) Create(string password, byte[]? iv = null)
{
const int KeySize = 32; //AES256 32 byte
if (iv == null)
{
iv = new byte[AesGcm.NonceByteSizes.MaxSize];
RandomNumberGenerator.Fill(iv);
}
Rfc2898DeriveBytes pbkdf = new Rfc2898DeriveBytes(Encoding.UTF8.GetBytes(password),
iv,
600000,
HashAlgorithmName.SHA256);
var key = pbkdf.GetBytes(KeySize);
AesGcm aes = new AesGcm(key, AesGcm.TagByteSizes.MaxSize);
return (aes, iv);
}
A GCM üzemmódot AES esetén az AesGcm osztály valósítja meg. Ennek a konstruktora két paramétert igényel. A kulcsot és egy tag méretet. A tag az ellenőrző adat, amivel ellenőrizni tudjuk visszafejtéskor, hogy az adatot módosították-e átvitel közben.
A Create metódus Rfc2898 segítségével generál egy kulcsot és egy inicializációs vektort a titkosításhoz, ha azt paraméterként nem kapta meg. A tag méretet a maximális tag méretre állítja, amit a AesGcm.TagByteSizes.MaxSize konstans tárol byte-ban kifejezve.
public static void AesGcmEncrypt(this string input, string password, Stream target)
{
byte[] plaintext = Encoding.UTF8.GetBytes(input);
byte[] cipherText = new byte[plaintext.Length];
byte[] tag = new byte[AesGcm.TagByteSizes.MaxSize];
(AesGcm aes, byte[] iv) = Create(password, null);
using (aes)
{
aes.Encrypt(iv, plaintext, cipherText, tag);
target.Write(iv);
target.Write(tag);
target.Write(cipherText);
}
}
public static string AesGcmDecrypt(this Stream input, string password)
{
long contentSize = input.Length - AesGcm.NonceByteSizes.MaxSize - AesGcm.TagByteSizes.MaxSize;
byte[] ciphertext = new byte[contentSize];
byte[] plainText = new byte[contentSize];
byte[] iv = new byte[AesGcm.NonceByteSizes.MaxSize];
byte[] tag = new byte[AesGcm.TagByteSizes.MaxSize];
input.Read(iv);
input.Read(tag);
input.Read(ciphertext);
(AesGcm aes, _) = Create(password, iv);
using (aes)
{
aes.Decrypt(iv, ciphertext, tag, plainText);
return Encoding.UTF8.GetString(plainText);
}
}
A titkosítás az AesGcmEncrypt segítségével történik. Ez paraméterként átveszi a titkosítandó adatot, a jelszót, illetve a cél Stream-et, amibe írni fogunk.
A Create metódus meghívása utána titkosító metódusban minden rendelkezésre áll a titkosításhoz, ami az AesGcm osztály Encrypt metóduisának meghívásával történik. Ez paraméterként átveszi az inicializációs vektor, a titkosítandó adatot, a kimeneti adat tömböt és a kimeneti tag tömböt.
A Stream-be a visszafejtéshez az inicializációs vektort és a tag-et is ki kell írnunk az adat mellett. Visszafejtéskor ezeket beolvasva tudjuk visszaállítani az adatokat. Ha a visszafejtéskor a tag-ben tárolt adat nem stimmel, vagyis az átvitel során módosult az adat, akkor egy CryptographicException kivételt kapunk.
-
Joan Daemen nevéhez köthető többek között az SHA-3 algoritmusa is.↩