Az aszimmetrikus titkosításhoz először szükségünk lesz egy kulcspárra. Ezt kódból az alábbi metódus segítségével könnyen elvégezhetjük:
public static RSAParameters GenerateKeyPair(int keySize, bool includePrivate)
{
using (var rsa = RSA.Create())
{
rsa.KeySize = keySize;
return rsa.ExportParameters(includePrivate);
}
}
A kód először példányosítja az RSA algoritmust az RSA.Create() metódushívással, majd a keySize paraméter beállítja a titkosítás bitjeinek a számát. Ez legalább 2048 legyen, mivel kisebb bitszám esetén nem garantálható a biztonság. A kulcs maximális mérete 16384 bit lehet és 64 bites lépésekben változtatható. A legkisebb támogatott kulcsméret 512 bit.
A kód a kucsot egy RSAParameters struktúrában adja vissza, ami lényegében a kulcspár adatait írja le. Az ExportParameters esetén a true paraméter arra utasítja a rendszert, hogy a kimeneti RSAParameters tartalmazza a privát kulcs paramétereit is.
A kapott kulcspárból csak a publikust kell továbbadni. Ennek kinyerése viszonylag egyszerű, mivel a kapott paraméterekből csak az Exponent és Modulus szükséges. Az alábbi rövid extension method ezt egyszerűsíti le:
public static RSAParameters GetPublicKey(this RSAParameters parameters)
{
return new RSAParameters
{
Exponent = parameters.Exponent,
Modulus = parameters.Modulus,
};
}
A kapott kulcsokat, hogy vissza is tudjunk állítani dolgokat, célszerű valamilyen formátumban a háttértárolón is tárolni. Erre a .NET a kezdetektől az XML formátumot alkalmazza. Az alábbi extension metódusok az XML alapú beolvasást és kiírást könnyítik meg számunkra:
public static void ExportKeysInXMLFormat(this RSAParameters parameters,
TextWriter xmlOutput,
bool includePrivateKey)
{
using (var rsa = RSA.Create())
{
rsa.ImportParameters(parameters);
xmlOutput.Write(rsa.ToXmlString(includePrivateKey));
}
}
public static RSAParameters ReadKeysFromXmlFormat(TextReader xmlInput)
{
using (var rsa = RSA.Create())
{
rsa.FromXmlString(xmlInput.ReadToEnd());
return rsa.ExportParameters(true);
}
}
PEM formátumú kulcsok használatára is lehetőségünk van .NET Core 3 óta. A korábbi .NET változatok nem rendelkeznek beépítetten PEM támogatással. Az alábbi metódusok a PEM formátumú kulcsok importálását könnyítik meg számunkra:
public static RSAParameters ReadKeysFromPemFormat(string pem,
bool includesPrivate = false)
{
using (var rsa = RSA.Create())
{
rsa.ImportFromPem(pem);
return rsa.ExportParameters(includesPrivate);
}
}
public static RSAParameters ReadKeysFromPemFormat(string pem,
string password,
bool includesPrivate = false)
{
using (var rsa = RSA.Create())
{
rsa.ImportFromEncryptedPem(pem, password);
return rsa.ExportParameters(includesPrivate);
}
}
A három paraméteres változat jelszó segítségével titkosított PEM kulcs importálására szolgál. Ezt a metódust akkor használjuk, ha az OpenSSL-nek kulcsgeneráláskor jelszót adtunk meg.
Ha kódból szeretnénk PEM formátumba írni a kulcsunkat, arra is lehetőségünk van. Ehhez az alábbi extension metódust készítettem:
public static void ExportKeysInPEMFormat(this RSAParameters parameters,
TextWriter pemOutput,
bool includePrivateKey)
{
using (var rsa = RSA.Create())
{
byte[] keyData = null;
string label = "";
rsa.ImportParameters(parameters);
if (includePrivateKey)
{
label = "RSA PRIVATE KEY";
keyData = rsa.ExportRSAPrivateKey();
}
else
{
label = "PUBLIC KEY";
keyData = rsa.ExportSubjectPublicKeyInfo();
}
pemOutput.Write(PemEncoding.Write(label, keyData));
}
}
Titkosítás
Miután a kulcspár kezelésen túl vagyunk, importáltuk fájlból vagy generáltattuk, rátérhetünk a titkosítás implementálásra. Az RSA szintén egy blokk alapú titkosítás, de a blokk mérete nem fix. A blokk méretét a kulcs mérete határozza meg, mivel egy x bites kulcs csak maximum x bit adatot tud titkosítani.
A titkosításhoz az alábbi aszinkron metódust készítettem:
public static async Task RsaEncrypt(this Stream plainInput,
Stream encryptedOutput,
RSAParameters key,
IProgress<float>? progress = null,
CancellationToken cancellationToken = default)
{
long position = 0;
long size = plainInput.Length;
int read = 0;
using (var rsa = RSA.Create())
{
rsa.ImportParameters(key);
byte[] buffer = new byte[(rsa.KeySize / 8) - Pkcs1PaddingSize];
do
{
cancellationToken.ThrowIfCancellationRequested();
read = await plainInput.ReadAsync(buffer, cancellationToken);
var encrypted = rsa.Encrypt(buffer[0..read], RSAEncryptionPadding.Pkcs1);
await encryptedOutput.WriteAsync(encrypted, cancellationToken);
position += read;
progress?.Report((float)position / size);
}
while (read != 0);
}
}
A metódus kísértetiesen hasonlít a korábbi megoldásokra. A lényegi titkosítás részt az rsa.Encrypt hívás végzi. A buffer méretének meghatározásánál van némi varázslat. A buffer mérete a kulcs mérete byte-ban, amiből levonjuk a Pkcs1PaddingSize konstans értéket. Ez 11 byte-ot jelent. A használt PKCS1 kitöltés esetén (ha az adat mérete nem érné el a blokk méretét) az utolsó 11 byte fenntartott, ez kódolja a blokk méretét.
Éppen ezért az Encrypt híváskor fontos, hogy a buffer csak azon része kerüljön titkosításra, ami ténylegesen adatot is hordoz. Ezt biztosítja a [0..read] indexelés.
A Pkcs1 utódjainak az OAEP módok tekinthetőek. Ezek egy hash segítségével minden blokk integritását is ellenőrzik és biztonságosabbnak tekintettek, mint a Pkcs1, de ennek a biztonságnak ára van. Mégpedig az, hogy a blokkméret jelentősen csökken, nem csak 11 byte-al. OAEP padding módok esetén a kódolható hasznos adat méretet az alábbi képlettel tudjuk kiszámítani: mLen = kLenBits / 8 – 2 * hLenBits / 8 – 2. A képletben kLenBits a kulcs mérete bitekben, míg a hLenBits a használt hash algoritmus mérete bitekben.
Példa kedvéért, ha 256 bites hasht alkalmazunk 2048 bites kulcs mérettel, akkor az egyszerre titkosítható adatmennyiség 190 byte lesz, vagyis 66 byte-ot visz el a padding. Az alábbi táblázat a különböző padding módok tárhelyigényét foglalja össze:
| Padding | Tárhely igény (byte) | Veszteség 4096 biten |
|---|---|---|
| Pkcs1 | 11 | ~3 % |
| OaepSHA1 | 42 | ~8 % |
| OaepSHA256 | 66 | ~ 13 % |
| OaepSHA384 | 98 | ~ 19 % |
| OaepSHA512 | 130 | ~ 25 % |
Visszafejtés esetén mindig egész számú blokkméretet kapunk, így nem kell szöszmötölni azzal, hogy mennyi adatot is fejtünk vissza, mivel olvasáskor a buffer mindig egy egész blokk lesz:
public static async Task RsaDecrypt(this Stream encryptedInput,
Stream plainOutput,
RSAParameters key,
IProgress<float>? progress = null,
CancellationToken cancellationToken = default)
{
long position = 0;
long size = encryptedInput.Length;
int read = 0;
using (var rsa = RSA.Create())
{
rsa.ImportParameters(key);
byte[] buffer = new byte[(rsa.KeySize / 8)];
do
{
cancellationToken.ThrowIfCancellationRequested();
read = await encryptedInput.ReadAsync(buffer, cancellationToken);
var decrypted = rsa.Decrypt(buffer, RSAEncryptionPadding.Pkcs1);
await plainOutput.WriteAsync(decrypted, cancellationToken);
position += read;
progress?.Report((float)position / size);
}
while (read != 0);
}
}
Megjegyzés: A fenti példák működőképesek, de nagy méretű adatok titkosítása ilyen módon, főleg hálózati kommunikáció esetén nem praktikus, mivel az RSA algoritmus igencsak számításigényes tud lenni, ami végső soron a sebességet limitálja.