Skip to content

Commit

Permalink
Unit test file bytes decryption
Browse files Browse the repository at this point in the history
  • Loading branch information
poulad committed Sep 2, 2018
1 parent d6893e5 commit ab1f460
Show file tree
Hide file tree
Showing 4 changed files with 300 additions and 42 deletions.
12 changes: 6 additions & 6 deletions .appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ nuget:
# branch: master


for:
-
branches:
only:
- master
configuration: Release
# for:
# -
# branches:
# only:
# - master
# configuration: Release
38 changes: 23 additions & 15 deletions src/Telegram.Bot.Extensions.Passport/Decryption/Decrypter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ EncryptedCredentials encryptedCredentials

byte[] decryptedData = DecryptDataBytes(data, secret, hash);
string json = Encoding.UTF8.GetString(decryptedData);
Credentials creds = JsonConvert.DeserializeObject<Credentials>(json);
Credentials credentials = JsonConvert.DeserializeObject<Credentials>(json);

return creds;
return credentials;
}

/// <inheritdoc />
Expand Down Expand Up @@ -98,14 +98,21 @@ FileCredentials fileCredentials
throw new ArgumentNullException(nameof(encryptedContent));
if (fileCredentials is null)
throw new ArgumentNullException(nameof(fileCredentials));
if (fileCredentials.Secret is null)
throw new ArgumentNullException(nameof(fileCredentials.Secret));
if (fileCredentials.FileHash is null)
throw new ArgumentNullException(nameof(fileCredentials.FileHash));
if (encryptedContent.Length == 0)
throw new ArgumentException("Data array is empty.", nameof(encryptedContent));
if (encryptedContent.Length % 16 != 0)
throw new PassportDataDecryptionException($"Invalid data length: {encryptedContent.Length}");
throw new PassportDataDecryptionException
($"Data length is not divisible by 16: {encryptedContent.Length}.");

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

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

return DecryptDataBytes(encryptedContent, dataSecret, dataHash);
}
Expand All @@ -130,17 +137,19 @@ public Task DecryptFileAsync(
throw new ArgumentNullException(nameof(destination));
if (!encryptedContent.CanRead)
throw new ArgumentException("Stream does not support reading.", nameof(encryptedContent));
if (encryptedContent.CanSeek && encryptedContent.Length == 0)
throw new ArgumentException("Stream is empty.", nameof(encryptedContent));
if (encryptedContent.CanSeek && encryptedContent.Length % 16 != 0)
throw new PassportDataDecryptionException
($"Length of padded data is not divisible by 16: {encryptedContent.Length}.");
throw new PassportDataDecryptionException("Data length is not divisible by 16: " +
$"{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($"file hash has invalid length: {dataHash.Length}.");
throw new PassportDataDecryptionException($"Hash length is not 32: {dataHash.Length}.");

return DecryptDataStreamAsync(encryptedContent, dataSecret, dataHash, destination, cancellationToken);
}
Expand Down Expand Up @@ -175,12 +184,11 @@ CancellationToken cancellationToken

byte paddingLength = paddingBuffer[0];
if (paddingLength < 32)
throw new PassportDataDecryptionException($"Data has invalid padding length: {paddingLength}.");
throw new PassportDataDecryptionException($"Data padding length is invalid: {paddingLength}.");

int actualDataLength = read - paddingLength;
if (actualDataLength < 1)
// ToDo test
throw new PassportDataDecryptionException($"Data has invalid length: {actualDataLength}.");
throw new PassportDataDecryptionException($"Data length is invalid: {actualDataLength}.");

await destination.WriteAsync(paddingBuffer, paddingLength, actualDataLength, cancellationToken)
.ConfigureAwait(false);
Expand Down Expand Up @@ -222,9 +230,9 @@ private static byte[] DecryptDataBytes(byte[] data, byte[] secret, byte[] hash)
aes.Key = dataKey;
aes.IV = dataIv;
aes.Padding = PaddingMode.None;
using (var decryptor = aes.CreateDecryptor())
using (var decrypter = aes.CreateDecryptor())
{
dataWithPadding = decryptor.TransformFinalBlock(data, 0, data.Length);
dataWithPadding = decrypter.TransformFinalBlock(data, 0, data.Length);
}
}
}
Expand All @@ -243,7 +251,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 PassportDataDecryptionException("Data hash mismatch.");
throw new PassportDataDecryptionException($"Data hash mismatch at position {i}.");
}
}

Expand All @@ -256,11 +264,11 @@ private static byte[] DecryptDataBytes(byte[] data, byte[] secret, byte[] hash)
{
byte paddingLength = dataWithPadding[0];
if (paddingLength < 32)
throw new PassportDataDecryptionException("Invalid data padding length");
throw new PassportDataDecryptionException($"Data padding length is invalid: {paddingLength}.");

int actualDataLength = dataWithPadding.Length - paddingLength;
if (actualDataLength < 1)
throw new PassportDataDecryptionException("Invalid data");
throw new PassportDataDecryptionException($"Data length is invalid: {actualDataLength}.");

decryptedData = new byte[actualDataLength];
Array.Copy(dataWithPadding, paddingLength, decryptedData, 0, actualDataLength);
Expand Down
233 changes: 233 additions & 0 deletions test/UnitTests/Decryption/FileBytesDecryptionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
// ReSharper disable CheckNamespace
// ReSharper disable StringLiteralTypo

using System;
using System.IO;
using System.Threading.Tasks;
using Telegram.Bot.Exceptions;
using Telegram.Bot.Passport;
using Telegram.Bot.Types.Passport;
using Xunit;

namespace UnitTests
{
/// <summary>
/// Tests for decrypting file byte arrays using <see cref="IDecrypter.DecryptFile"/> method
/// </summary>
public class FileBytesDecryptionTests
{
[Fact(DisplayName = "Should decrypt from file bytes")]
public async Task Should_Decrypt_From_Bytes()
{
FileCredentials fileCredentials = new FileCredentials
{
FileHash = "v3q47iscI6TS94CMo7HGQUOxw28LIf82NJBkImzP57c=",
Secret = "vF7nut7clg/H/pEaTJigo4mQJ0s8B+HGCWKTWtOTIdo=",
};

IDecrypter decrypter = new Decrypter();

byte[] encContent = await File.ReadAllBytesAsync("Files/driver_license-selfie.jpg.enc");

byte[] content = decrypter.DecryptFile(
encContent,
fileCredentials
);

Assert.NotEmpty(content);
Assert.InRange(content.Length, encContent.Length - 256, encContent.Length - 33);
}

[Fact(DisplayName = "Should throw when trying to decrypt an unencrypted file with invalid data length")]
public async Task Should_Throw_Decrypting_Unencrypted_File_Invalid_Length()
{
FileCredentials fileCredentials = new FileCredentials
{
FileHash = "v3q47iscI6TS94CMo7HGQUOxw28LIf82NJBkImzP57c=",
Secret = "vF7nut7clg/H/pEaTJigo4mQJ0s8B+HGCWKTWtOTIdo=",
};

IDecrypter decrypter = new Decrypter();
byte[] encContent = await File.ReadAllBytesAsync("Files/driver_license-selfie.jpg");

Exception exception = Assert.ThrowsAny<Exception>(() =>
decrypter.DecryptFile(
encContent,
fileCredentials
)
);

Assert.Matches(@"^Length of padded data is not divisible by 16: \d|\.$", exception.Message);
Assert.IsType<PassportDataDecryptionException>(exception);
}

[Fact(DisplayName = "Should throw when trying to decrypt an unencrypted file but with valid data length")]
public void Should_Throw_Decrypting_Unencrypted_File_Valid_Length()
{
FileCredentials fileCredentials = new FileCredentials
{
FileHash = "v3q47iscI6TS94CMo7HGQUOxw28LIf82NJBkImzP57c=",
Secret = "vF7nut7clg/H/pEaTJigo4mQJ0s8B+HGCWKTWtOTIdo=",
};

IDecrypter decrypter = new Decrypter();
byte[] encContent = new byte[2048]; // data length is divisible by 16

Exception exception = Assert.ThrowsAny<Exception>(() =>
decrypter.DecryptFile(
encContent,
fileCredentials
)
);

Assert.Matches(@"^Data hash mismatch at position \d+\.$", exception.Message);
Assert.IsType<PassportDataDecryptionException>(exception);
}

[Fact(DisplayName = "Should throw when decrypting a file(selfie) using wrong file credentials(front side)")]
public async Task Should_Throw_Decrypting_Bytes_With_Wrong_FileCredentials()
{
FileCredentials wrongFileCredentials = new FileCredentials
{
FileHash = "THTjgv2FU7kff/29Vty/IcqKPmOGkL7F35fAzmkfZdI=",
Secret = "a+jxJoKPEaz77VCjRvDVcYHfIO3+h+oI+ruZh+KkYa0=",
};

IDecrypter decrypter = new Decrypter();
byte[] encContent = await File.ReadAllBytesAsync("Files/driver_license-selfie.jpg.enc");

Exception exception = Assert.ThrowsAny<Exception>(() =>
decrypter.DecryptFile(
encContent,
wrongFileCredentials
)
);

Assert.Matches(@"^Data hash mismatch at position \d+\.$", exception.Message);
Assert.IsType<PassportDataDecryptionException>(exception);
}

[Fact(DisplayName = "Should throw when null data bytes is passed")]
public void Should_Throw_If_Null_EncryptedContent()
{
IDecrypter decrypter = new Decrypter();

Exception exception = Assert.ThrowsAny<Exception>(() =>
decrypter.DecryptFile(null, null)
);

Assert.Matches(@"^Value cannot be null\.\s+Parameter name: encryptedContent$", exception.Message);
Assert.IsType<ArgumentNullException>(exception);
}

[Fact(DisplayName = "Should throw when null file credentials is passed")]
public void Should_Throw_If_Null_FileCredentials()
{
IDecrypter decrypter = new Decrypter();

Exception exception = Assert.ThrowsAny<Exception>(() =>
decrypter.DecryptFile(new byte[0], null)
);

Assert.Matches(@"^Value cannot be null\.\s+Parameter name: fileCredentials$", exception.Message);
Assert.IsType<ArgumentNullException>(exception);
}

[Fact(DisplayName = "Should throw when null secret is passed")]
public void Should_Throw_If_Null_Secret()
{
IDecrypter decrypter = new Decrypter();
Exception exception = Assert.ThrowsAny<Exception>(() =>
decrypter.DecryptFile(new byte[0], new FileCredentials())
);

Assert.Matches(@"^Value cannot be null\.\s+Parameter name: Secret$", exception.Message);
Assert.IsType<ArgumentNullException>(exception);
}

[Fact(DisplayName = "Should throw when null file_hash is passed")]
public void Should_Throw_If_Null_Hash()
{
IDecrypter decrypter = new Decrypter();

FileCredentials fileCredentials = new FileCredentials {Secret = ""};
Exception exception = Assert.ThrowsAny<Exception>(() =>
decrypter.DecryptFile(new byte[0], fileCredentials)
);

Assert.Matches(@"^Value cannot be null\.\s+Parameter name: FileHash$", exception.Message);
Assert.IsType<ArgumentNullException>(exception);
}

[Fact(DisplayName = "Should throw when data byte array is empty")]
public void Should_Throw_If_Empty_Data_Bytes_Length()
{
IDecrypter decrypter = new Decrypter();
FileCredentials fileCredentials = new FileCredentials {Secret = "", FileHash = ""};

Exception exception = Assert.ThrowsAny<Exception>(() =>
decrypter.DecryptFile(new byte[0], fileCredentials)
);

Assert.Matches(@"^Data array is empty\.\s+Parameter name: encryptedContent$", exception.Message);
Assert.IsType<ArgumentException>(exception);
}

[Fact(DisplayName = "Should throw when data byte array has invalid length")]
public void Should_Throw_If_Invalid_Data_Bytes_Length()
{
IDecrypter decrypter = new Decrypter();
FileCredentials fileCredentials = new FileCredentials {Secret = "", FileHash = ""};

Exception exception = Assert.ThrowsAny<Exception>(() =>
decrypter.DecryptFile(new byte[16 + 1], fileCredentials)
);

Assert.Equal("Data length is not divisible by 16: 17.", exception.Message);
Assert.IsType<PassportDataDecryptionException>(exception);
}

[Theory(DisplayName = "Should throw when secret is not a valid base64-encoded string")]
[InlineData("foo")]
[InlineData("FooBarBazlg/H/pEaTJigo4mQJ0s8B+HGCWKTWtOTIdo=")]
public void Should_Throw_If_Invalid_Secret(string secret)
{
FileCredentials fileCredentials = new FileCredentials {Secret = secret, FileHash = ""};
IDecrypter decrypter = new Decrypter();

Assert.Throws<FormatException>(() =>
decrypter.DecryptFile(new byte[16], fileCredentials)
);
}

[Theory(DisplayName = "Should throw when file_hash is not a valid base64-encoded string")]
[InlineData("foo")]
[InlineData("FooBarBazlg/H/pEaTJigo4mQJ0s8B+HGCWKTWtOTIdo=")]
public void Should_Throw_If_Invalid_Hash(string fileHash)
{
FileCredentials fileCredentials = new FileCredentials {Secret = "", FileHash = fileHash};
IDecrypter decrypter = new Decrypter();

Assert.ThrowsAny<FormatException>(() =>
decrypter.DecryptFile(new byte[16], fileCredentials)
);
}

[Theory(DisplayName = "Should throw when length of file_hash is not 32")]
[InlineData("")]
[InlineData("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==")]
[InlineData("Zm9v")]
public void Should_Throw_If_Invalid_Hash_Length(string fileHash)
{
FileCredentials fileCredentials = new FileCredentials {Secret = "", FileHash = fileHash};
IDecrypter decrypter = new Decrypter();

Exception exception = Assert.ThrowsAny<Exception>(() =>
decrypter.DecryptFile(new byte[16], fileCredentials)
);

Assert.Matches(@"^Hash length is not 32: \d+\.$", exception.Message);
Assert.IsType<PassportDataDecryptionException>(exception);
}
}
}
Loading

0 comments on commit ab1f460

Please sign in to comment.