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.