Skip to content

Commit

Permalink
Refactor decryption methods
Browse files Browse the repository at this point in the history
  • Loading branch information
poulad committed Sep 1, 2018
1 parent 212d97e commit 5911413
Show file tree
Hide file tree
Showing 8 changed files with 233 additions and 194 deletions.
206 changes: 114 additions & 92 deletions src/Telegram.Bot.Extensions.Passport/Decryption/Decrypter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,35 @@
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Telegram.Bot.Extensions.Passport;
using Telegram.Bot.Exceptions;
using Telegram.Bot.Types.Passport;

// ReSharper disable once CheckNamespace
namespace Telegram.Bot.Passport
{
public class Decrypter : IDecrypter
{
/// <inheritdoc />
public Credentials DecryptCredentials(
RSA key,
EncryptedCredentials encryptedCredentials
)
{
key = key ?? throw new ArgumentNullException(nameof(key));
if (key is null)
throw new ArgumentNullException(nameof(key));
if (encryptedCredentials is null)
throw new ArgumentNullException(nameof(encryptedCredentials));

byte[] data = Convert.FromBase64String(encryptedCredentials.Data);
if (data.Length % 16 != 0)
throw new PassportDataDecryptionException($"Invalid data length: {data.Length}");

byte[] hash = Convert.FromBase64String(encryptedCredentials.Hash);
if (hash.Length != 32)
throw new PassportDataDecryptionException($"Invalid hash length: {hash.Length}");

byte[] data, hash, secret;
data = Convert.FromBase64String(encryptedCredentials.Data);
hash = Convert.FromBase64String(encryptedCredentials.Hash);
byte[] encryptedSecret = Convert.FromBase64String(encryptedCredentials.Secret);
secret = key.Decrypt(encryptedSecret, RSAEncryptionPadding.OaepSHA1);
byte[] secret = key.Decrypt(encryptedSecret, RSAEncryptionPadding.OaepSHA1);

byte[] decryptedData = DecryptDataBytes(data, secret, hash);
string json = Encoding.UTF8.GetString(decryptedData);
Expand All @@ -33,83 +42,118 @@ EncryptedCredentials encryptedCredentials
return creds;
}

/// <inheritdoc />
public string DecryptData(
string encryptedData,
DataCredentials dataCredentials
)
{
byte[] decryptedData;
{
byte[] data, dataHash, dataSecret;
data = Convert.FromBase64String(encryptedData);
dataHash = Convert.FromBase64String(dataCredentials.DataHash);
dataSecret = Convert.FromBase64String(dataCredentials.Secret);
decryptedData = DecryptDataBytes(data, dataSecret, dataHash);
}
if (encryptedData is null)
throw new ArgumentNullException(nameof(encryptedData));
if (dataCredentials is null)
throw new ArgumentNullException(nameof(dataCredentials));

byte[] data = Convert.FromBase64String(encryptedData);
if (data.Length % 16 != 0)
throw new PassportDataDecryptionException($"Invalid data length: {data.Length}");

byte[] dataHash = Convert.FromBase64String(dataCredentials.DataHash);
if (dataHash.Length != 32)
throw new PassportDataDecryptionException($"Invalid hash length: {dataHash.Length}");

byte[] dataSecret = Convert.FromBase64String(dataCredentials.Secret);

byte[] decryptedData = DecryptDataBytes(data, dataSecret, dataHash);
string content = Encoding.UTF8.GetString(decryptedData);

return content;
}

/// <inheritdoc />
public TValue DecryptData<TValue>(
string encryptedData,
DataCredentials dataCredentials
)
where TValue : IDecryptedValue
{
if (encryptedData is null)
throw new ArgumentNullException(nameof(encryptedData));
if (dataCredentials is null)
throw new ArgumentNullException(nameof(dataCredentials));

string json = DecryptData(encryptedData, dataCredentials);
return JsonConvert.DeserializeObject<TValue>(json);
}

public async Task DecryptFileAsync(
/// <inheritdoc />
public Task DecryptFileAsync(
Stream encryptedContent,
FileCredentials fileCredentials,
Stream destination,
CancellationToken cancellationToken = default
)
{
encryptedContent = encryptedContent ?? throw new ArgumentNullException(nameof(encryptedContent));
fileCredentials = fileCredentials ?? throw new ArgumentNullException(nameof(fileCredentials));

byte[] hash, secret;
hash = Convert.FromBase64String(fileCredentials.FileHash);
secret = Convert.FromBase64String(fileCredentials.Secret);

byte[] aesKey, aesIv;

#region Step 1: compute aes Key and IV
if (encryptedContent is null)
throw new ArgumentNullException(nameof(encryptedContent));
if (fileCredentials is null)
throw new ArgumentNullException(nameof(fileCredentials));
if (encryptedContent.Length % 16 != 0)
throw new PassportDataDecryptionException($"Invalid data length: {encryptedContent.Length}");
if (!destination.CanWrite)
throw new ArgumentException("Stream does not support writing.", nameof(destination));

byte[] dataSecret = Convert.FromBase64String(fileCredentials.Secret);
byte[] dataHash = Convert.FromBase64String(fileCredentials.FileHash);

if (dataHash.Length != 32)
throw new PassportDataDecryptionException($"Invalid hash length: {dataHash.Length}");

return DecryptDataStreamAsync(encryptedContent, dataSecret, dataHash, destination, cancellationToken);
}

{
byte[] dataSecretHash;
using (SHA512 sha512 = SHA512.Create())
{
byte[] secretAndHashBytes = new byte[secret.Length + hash.Length];
Array.Copy(secret, 0, secretAndHashBytes, 0, secret.Length);
Array.Copy(hash, 0, secretAndHashBytes, secret.Length, hash.Length);
dataSecretHash = sha512.ComputeHash(secretAndHashBytes);
}
/// <inheritdoc />
public byte[] DecryptFile(
byte[] encryptedContent,
FileCredentials fileCredentials
)
{
if (encryptedContent is null)
throw new ArgumentNullException(nameof(encryptedContent));
if (fileCredentials is null)
throw new ArgumentNullException(nameof(fileCredentials));
if (encryptedContent.Length % 16 != 0)
throw new PassportDataDecryptionException($"Invalid data length: {encryptedContent.Length}");

aesKey = new byte[32];
Array.Copy(dataSecretHash, 0, aesKey, 0, 32);
byte[] dataSecret = Convert.FromBase64String(fileCredentials.Secret);
byte[] dataHash = Convert.FromBase64String(fileCredentials.FileHash);

aesIv = new byte[16];
Array.Copy(dataSecretHash, 32, aesIv, 0, 16);
}
if (dataHash.Length != 32)
throw new PassportDataDecryptionException($"Invalid hash length: {dataHash.Length}");

#endregion
return DecryptDataBytes(encryptedContent, dataSecret, dataHash);
}

#region Step 2.1, 2.2 and 3: decrypt, verify, remove paddding
private static async Task DecryptDataStreamAsync(
Stream data,
byte[] secret,
byte[] hash,
Stream destination,
CancellationToken cancellationToken
)
{
FindDataKeyAndIv(secret, hash, out byte[] dataKey, out byte[] dataIv);

using (var aes = Aes.Create())
{
// ReSharper disable once PossibleNullReferenceException
aes.KeySize = 256;
aes.Mode = CipherMode.CBC;
aes.Key = aesKey;
aes.IV = aesIv;
aes.Key = dataKey;
aes.IV = dataIv;
aes.Padding = PaddingMode.None;

using (var decryptor = aes.CreateDecryptor())
using (CryptoStream aesStream = new CryptoStream(encryptedContent, decryptor, CryptoStreamMode.Read))
using (CryptoStream aesStream = new CryptoStream(data, decryptor, CryptoStreamMode.Read))
using (var sha256 = SHA256.Create())
using (CryptoStream shaStream = new CryptoStream(aesStream, sha256, CryptoStreamMode.Read))
{
Expand All @@ -119,10 +163,10 @@ public async Task DecryptFileAsync(

int paddingLength = paddingBuffer[0];
if (paddingLength < 32)
throw new DecryptionException("Invalid padding size");
throw new PassportDataDecryptionException("Invalid padding size");

if (read < paddingLength)
throw new DecryptionException("Invalid data");
throw new PassportDataDecryptionException("Invalid data");

await destination.WriteAsync(paddingBuffer, paddingLength, read - paddingLength, cancellationToken)
.ConfigureAwait(false);
Expand All @@ -134,58 +178,17 @@ await shaStream.CopyToAsync(destination, 81920, cancellationToken)
for (int i = 0; i < 32; i++)
{
if (hash[i] != paddedDataHash[i])
throw new DecryptionException("Data hash mismatch");
throw new PassportDataDecryptionException("Data hash mismatch");
}
}
}

#endregion
}

public byte[] DecryptFile(
byte[] encryptedContent,
FileCredentials fileCredentials
)
{
byte[] dataHash, dataSecret;
dataHash = Convert.FromBase64String(fileCredentials.FileHash);
dataSecret = Convert.FromBase64String(fileCredentials.Secret);

return DecryptDataBytes(encryptedContent, dataSecret, dataHash);
}

private static byte[] DecryptDataBytes(byte[] data, byte[] secret, byte[] hash)
{
if (data.Length % 16 != 0)
{
throw new DecryptionException($"Invalid data length: {data.Length}");
}

if (hash.Length != 32)
{
throw new DecryptionException($"Invalid hash length: {hash.Length}");
}

byte[] dataKey, dataIv;

#region Step 1: find data Key & IV

{
byte[] dataSecretHash;
using (SHA512 sha512 = SHA512.Create())
{
byte[] secretAndHashBytes = new byte[secret.Length + hash.Length];
Array.Copy(secret, 0, secretAndHashBytes, 0, secret.Length);
Array.Copy(hash, 0, secretAndHashBytes, secret.Length, hash.Length);
dataSecretHash = sha512.ComputeHash(secretAndHashBytes);
}

dataKey = new byte[32];
Array.Copy(dataSecretHash, 0, dataKey, 0, 32);

dataIv = new byte[16];
Array.Copy(dataSecretHash, 32, dataIv, 0, 16);
}
FindDataKeyAndIv(secret, hash, out byte[] dataKey, out byte[] dataIv);

#endregion

Expand All @@ -196,11 +199,12 @@ private static byte[] DecryptDataBytes(byte[] data, byte[] secret, byte[] hash)
{
using (var aes = Aes.Create())
{
// ReSharper disable once PossibleNullReferenceException
aes.KeySize = 256;
aes.Mode = CipherMode.CBC;
aes.Key = dataKey;
aes.IV = dataIv;
aes.Padding = PaddingMode.None; // ToDo: Try to remove this line
aes.Padding = PaddingMode.None;
using (var decryptor = aes.CreateDecryptor())
{
dataWithPadding = decryptor.TransformFinalBlock(data, 0, data.Length);
Expand All @@ -222,7 +226,7 @@ private static byte[] DecryptDataBytes(byte[] data, byte[] secret, byte[] hash)
for (int i = 0; i < hash.Length; i++)
{
if (hash[i] != paddedDataHash[i])
throw new DecryptionException("Data hash mismatch");
throw new PassportDataDecryptionException("Data hash mismatch");
}
}

Expand All @@ -236,7 +240,7 @@ private static byte[] DecryptDataBytes(byte[] data, byte[] secret, byte[] hash)
int paddingLength = dataWithPadding[0];
if (!(32 <= paddingLength && paddingLength < 256))
{
throw new DecryptionException("Invalid data padding length");
throw new PassportDataDecryptionException("Invalid data padding length");
}

int actualDataLength = dataWithPadding.Length - paddingLength;
Expand All @@ -249,5 +253,23 @@ private static byte[] DecryptDataBytes(byte[] data, byte[] secret, byte[] hash)

return decryptedData;
}

private static void FindDataKeyAndIv(byte[] secret, byte[] hash, out byte[] dataKey, out byte[] dataIv)
{
byte[] dataSecretHash;
using (var sha512 = SHA512.Create())
{
byte[] secretAndHashBytes = new byte[secret.Length + hash.Length];
Array.Copy(secret, 0, secretAndHashBytes, 0, secret.Length);
Array.Copy(hash, 0, secretAndHashBytes, secret.Length, hash.Length);
dataSecretHash = sha512.ComputeHash(secretAndHashBytes);
}

dataKey = new byte[32];
Array.Copy(dataSecretHash, 0, dataKey, 0, 32);

dataIv = new byte[16];
Array.Copy(dataSecretHash, 32, dataIv, 0, 16);
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System;

// ReSharper disable once CheckNamespace
namespace Telegram.Bot.Exceptions
{
/// <summary>
/// Represents a fatal error in decryption of Telegram Passport Data
/// </summary>
public class PassportDataDecryptionException : Exception
{
/// <summary>
/// Initializes a new instance of <see cref="PassportDataDecryptionException"/>
/// </summary>
/// <param name="message">Error description</param>
public PassportDataDecryptionException(string message)
: base(message)
{
}

/// <summary>
/// Initializes a new instance of <see cref="PassportDataDecryptionException"/>
/// </summary>
/// <param name="message">Error description</param>
/// <param name="innerException">Root cause of the error</param>
public PassportDataDecryptionException(string message, Exception innerException)
: base(message, innerException)
{
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,9 @@ public static async Task<File> DownloadAndDecryptPassportFileAsync(
)
{
if (passportFile == null)
{
throw new ArgumentNullException(nameof(passportFile));
}

if (!destination.CanWrite)
{
throw new ArgumentException("Stream msut be writable.", nameof(destination));
}

File fileInfo;

Expand Down
Loading

0 comments on commit 5911413

Please sign in to comment.