Skip to content

Commit

Permalink
Support decryption of non-seekable stream
Browse files Browse the repository at this point in the history
  • Loading branch information
poulad committed Sep 2, 2018
1 parent aac2cab commit 77f9cff
Show file tree
Hide file tree
Showing 14 changed files with 105 additions and 117 deletions.
90 changes: 43 additions & 47 deletions src/Telegram.Bot.Extensions.Passport/Decryption/Decrypter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ public Task DecryptFileAsync(
throw new ArgumentNullException(nameof(encryptedContent));
if (fileCredentials is null)
throw new ArgumentNullException(nameof(fileCredentials));
if (!encryptedContent.CanRead)
throw new ArgumentException("Stream does not support reading.", nameof(encryptedContent));
if (encryptedContent.CanSeek && 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));

Expand Down Expand Up @@ -144,57 +148,49 @@ CancellationToken cancellationToken
{
FindDataKeyAndIv(secret, hash, out byte[] dataKey, out byte[] dataIv);

try
using (var aes = Aes.Create())
{
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;

using (var decryptor = aes.CreateDecryptor())
using (CryptoStream aesStream = new CryptoStream(data, decryptor, CryptoStreamMode.Read))
using (var sha256 = SHA256.Create())
using (CryptoStream shaStream = new CryptoStream(aesStream, sha256, CryptoStreamMode.Read))
{
// ReSharper disable once PossibleNullReferenceException
aes.KeySize = 256;
aes.Mode = CipherMode.CBC;
aes.Key = dataKey;
aes.IV = dataIv;
aes.Padding = PaddingMode.None;
byte[] paddingBuffer = new byte[256];
int read = await shaStream.ReadAsync(paddingBuffer, 0, 256, cancellationToken)
.ConfigureAwait(false);

using (var decryptor = aes.CreateDecryptor())
using (CryptoStream aesStream = new CryptoStream(data, decryptor, CryptoStreamMode.Read))
using (var sha256 = SHA256.Create())
using (CryptoStream shaStream = new CryptoStream(aesStream, sha256, CryptoStreamMode.Read))
byte paddingLength = paddingBuffer[0];
if (paddingLength < 32)
throw new PassportDataDecryptionException("Invalid data padding length");

int actualDataLength = read - paddingLength;
if (actualDataLength < 1)
throw new PassportDataDecryptionException("Invalid data");

await destination.WriteAsync(paddingBuffer, paddingLength, actualDataLength, cancellationToken)
.ConfigureAwait(false);

// 81920 is the default Stream.CopyTo buffer size
// The overload without the buffer size does not accept a cancellation token
const int defaultBufferSize = 81920;
await shaStream.CopyToAsync(destination, defaultBufferSize, cancellationToken)
.ConfigureAwait(false);

byte[] paddedDataHash = sha256.Hash;
for (int i = 0; i < hash.Length; i++)
{
byte[] paddingBuffer = new byte[256];
int read = await shaStream.ReadAsync(paddingBuffer, 0, 256, cancellationToken)
.ConfigureAwait(false);

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

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

await destination.WriteAsync(paddingBuffer, paddingLength, read - paddingLength, cancellationToken)
.ConfigureAwait(false);

// 81920 is the default Stream.CopyTo buffer size
// The overload without the buffer size does not accept a cancellation token
await shaStream.CopyToAsync(destination, 81920, cancellationToken)
.ConfigureAwait(false);

byte[] paddedDataHash = sha256.Hash;
for (int i = 0; i < 32; i++)
{
if (hash[i] != paddedDataHash[i])
throw new PassportDataDecryptionException("Data hash mismatch");
}
if (hash[i] != paddedDataHash[i])
throw new PassportDataDecryptionException("Data hash mismatch");
}
}
}
catch (CryptographicException ce)
when (ce.Message.Equals("The input data is not a complete block.", StringComparison.Ordinal))
{
// The length check in this case can not be performed before decryption
// since we could be dealing with a stream that does not support seeking
throw new PassportDataDecryptionException("Invalid data length");
}
}

private static byte[] DecryptDataBytes(byte[] data, byte[] secret, byte[] hash)
Expand Down Expand Up @@ -250,13 +246,13 @@ private static byte[] DecryptDataBytes(byte[] data, byte[] secret, byte[] hash)
#region Step 3: remove padding to get the actual data

{
int paddingLength = dataWithPadding[0];
byte paddingLength = dataWithPadding[0];
if (paddingLength < 32)
{
throw new PassportDataDecryptionException("Invalid data padding length");
}

int actualDataLength = dataWithPadding.Length - paddingLength;
if (actualDataLength < 1)
throw new PassportDataDecryptionException("Invalid data");

decryptedData = new byte[actualDataLength];
Array.Copy(dataWithPadding, paddingLength, decryptedData, 0, actualDataLength);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ public static async Task<File> DownloadAndDecryptPassportFileAsync(
cancellationToken
).ConfigureAwait(false);

encryptedContentStream.Position = 0;

await new Decrypter().DecryptFileAsync(
encryptedContentStream,
fileCredentials,
Expand Down
19 changes: 0 additions & 19 deletions test/IntegrationTests/Framework/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,5 @@ public static class Constants

public const string TestCaseDiscoverer =
AssemblyName + "." + nameof(Framework) + "." + nameof(XunitExtensions) + "." + nameof(RetryFactDiscoverer);

public static class TestCollections
{
public const string PersonalDetails = "Personal details";

public const string ResidentialAddress = "Residential address";

public const string DriverLicense = "Driver license";

public const string PhoneAndEmail = "Phone and Email";

public const string RentalAgreementAndBill = "Rental agreement and utility bill";

public const string IdentityCardErrors = "Identity card errors";

public const string PassportRegistrationErrors = "Passport registration errors";

public const string UnspecifiedError = "Unspecified error";
}
}
}
23 changes: 13 additions & 10 deletions test/IntegrationTests/Framework/TestCollectionOrderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,19 @@ public class TestCollectionOrderer : ITestCollectionOrderer
{
private readonly string[] _orderedCollections =
{
Constants.TestCollections.PersonalDetails,
Constants.TestCollections.ResidentialAddress,
Constants.TestCollections.DriverLicense,

Constants.TestCollections.PhoneAndEmail,
Constants.TestCollections.RentalAgreementAndBill,

Constants.TestCollections.IdentityCardErrors,
Constants.TestCollections.PassportRegistrationErrors,
Constants.TestCollections.UnspecifiedError,
// single scope
"Personal details",
"Residential address",
"Driver license",

// multiple scopes
"Phone and email",
"Identity card and utility bill",

// setting data errors
"Identity card errors",
"Passport registration errors",
"Unspecified error",
};

public IEnumerable<ITestCollection> OrderTestCollections(IEnumerable<ITestCollection> testCollections)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// ReSharper disable CheckNamespace

using System;
using System.Net.Http;
using System.Security.Cryptography;
using System.Threading.Tasks;
using IntegrationTests.Framework;
Expand All @@ -14,23 +15,29 @@
using Telegram.Bot.Types.Passport;
using Telegram.Bot.Types.ReplyMarkups;
using Xunit;
using Xunit.Abstractions;

namespace IntegrationTests
{
[Collection(Constants.TestCollections.RentalAgreementAndBill)]
[Collection("Identity card and utility bill")]
[TestCaseOrderer(Constants.TestCaseOrderer, Constants.AssemblyName)]
public class RentalAgreementAndBillTests : IClassFixture<EntityFixture<Update>>
public class IdentityCardAndUtilityBillTests : IClassFixture<EntityFixture<Update>>
{
private ITelegramBotClient BotClient => _fixture.BotClient;

private readonly TestsFixture _fixture;

private readonly EntityFixture<Update> _classFixture;

public RentalAgreementAndBillTests(TestsFixture fixture, EntityFixture<Update> classFixture)
private readonly ITestOutputHelper _output;

public IdentityCardAndUtilityBillTests(
TestsFixture fixture, EntityFixture<Update> classFixture, ITestOutputHelper output
)
{
_fixture = fixture;
_classFixture = classFixture;
_output = output;
}

[OrderedFact("Should generate passport authorization request link")]
Expand Down Expand Up @@ -265,7 +272,8 @@ await BotClient.GetInfoAndDownloadFileAsync(
Assert.NotEmpty(content);
}

[OrderedFact("Should decrypt reverse side photo in 'identity_card' element")]
[OrderedFact("Should decrypt reverse side photo in 'identity_card' element from HTTP response " +
"and write it to a file on disk")]
public async Task Should_decreypt_identity_card_element_reverseside()
{
Update update = _classFixture.Entity;
Expand All @@ -276,22 +284,28 @@ public async Task Should_decreypt_identity_card_element_reverseside()
IDecrypter decrypter = new Decrypter();
Credentials credentials = decrypter.DecryptCredentials(key, passportData.Credentials);

byte[] encryptedContent;
using (System.IO.MemoryStream stream = new System.IO.MemoryStream(idCardEl.ReverseSide.FileSize))
string botToken = ConfigurationProvider.TestConfigurations.ApiToken;
File encFileInfo = await BotClient.GetFileAsync(idCardEl.ReverseSide.FileId);

HttpClient http = new HttpClient();
System.IO.Stream encFileStream = await http.GetStreamAsync(
$"https://api.telegram.org/file/bot{botToken}/{encFileInfo.FilePath}"
);
string destFilePath = System.IO.Path.GetTempFileName();

using (encFileStream)
using (System.IO.Stream reverseSideFile = System.IO.File.OpenWrite(destFilePath))
{
await BotClient.GetInfoAndDownloadFileAsync(
idCardEl.ReverseSide.FileId,
stream
await decrypter.DecryptFileAsync(
encFileStream,
credentials.SecureData.IdentityCard.ReverseSide,
reverseSideFile
);
encryptedContent = stream.ToArray();
}

byte[] content = decrypter.DecryptFile(
encryptedContent,
credentials.SecureData.IdentityCard.ReverseSide
);
Assert.InRange(reverseSideFile.Length, encFileInfo.FileSize - 256, encFileInfo.FileSize + 256);
}

Assert.NotEmpty(content);
_output.WriteLine("Reverse side photo is written to file \"{0}\".", destFilePath);
}

[OrderedFact("Should decrypt selfie photo in 'identity_card' element")]
Expand Down Expand Up @@ -346,7 +360,7 @@ public async Task Should_decrypt_utility_bill_element_file()
billFileCreds,
decryptedFile
);
Assert.Equal(billScanFile.FileSize, decryptedFile.Length);
Assert.InRange(decryptedFile.Length, billScanFile.FileSize - 256, billScanFile.FileSize + 256);
}

Assert.NotEmpty(encryptedFileInfo.FilePath);
Expand Down Expand Up @@ -377,7 +391,7 @@ public async Task Should_decrypt_utility_bill_element_translation()
billTranslationFileCreds,
decryptedFile
);
Assert.Equal(translationFile.FileSize, decryptedFile.Length);
Assert.InRange(decryptedFile.Length, translationFile.FileSize - 256, translationFile.FileSize + 256);
}

Assert.NotEmpty(encryptedFileInfo.FilePath);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

namespace IntegrationTests
{
[Collection(Constants.TestCollections.PhoneAndEmail)]
[Collection("Phone and email")]
[TestCaseOrderer(Constants.TestCaseOrderer, Constants.AssemblyName)]
public class PhoneAndEmailTests : IClassFixture<EntityFixture<Update>>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

namespace IntegrationTests
{
[Collection(Constants.TestCollections.IdentityCardErrors)]
[Collection("Identity card errors")]
[TestCaseOrderer(Constants.TestCaseOrderer, Constants.AssemblyName)]
public class IdentityCardErrorTests : IClassFixture<IdentityCardErrorTests.Fixture>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

namespace IntegrationTests
{
[Collection(Constants.TestCollections.PassportRegistrationErrors)]
[Collection("Passport registration errors")]
[TestCaseOrderer(Constants.TestCaseOrderer, Constants.AssemblyName)]
public class PassportRegistrationErrorTests : IClassFixture<PassportRegistrationErrorTests.Fixture>
{
Expand Down Expand Up @@ -75,7 +75,8 @@ await BotClient.SendTextMessageAsync(

RSA key = EncryptionKey.ReadAsRsa();
IDecrypter decrypter = new Decrypter();
Credentials credentials = decrypter.DecryptCredentials(key, passportUpdate.Message.PassportData.Credentials);
Credentials credentials =
decrypter.DecryptCredentials(key, passportUpdate.Message.PassportData.Credentials);

Assert.Equal("Test nonce for passport registration", credentials.Nonce);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

namespace IntegrationTests
{
[Collection(Constants.TestCollections.UnspecifiedError)]
[Collection("Unspecified error")]
[TestCaseOrderer(Constants.TestCaseOrderer, Constants.AssemblyName)]
public class UnspecifiedErrorTests : IClassFixture<UnspecifiedErrorTests.Fixture>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

namespace IntegrationTests
{
[Collection(Constants.TestCollections.DriverLicense)]
[Collection("Driver license")]
[TestCaseOrderer(Constants.TestCaseOrderer, Constants.AssemblyName)]
public class DriverLicenseTests : IClassFixture<EntityFixture<Update>>
{
Expand Down Expand Up @@ -247,6 +247,7 @@ public async Task Should_decreypt_reverse_side_file()
element.ReverseSide.FileId,
encryptedContent
);
encryptedContent.Position = 0;

await decrypter.DecryptFileAsync(
encryptedContent,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
namespace IntegrationTests
{
/// <summary>
/// Tests for request personal details using Telegram Passport v1.1
/// Tests for request personal details
/// </summary>
[Collection(Constants.TestCollections.PersonalDetails)]
[Collection("Personal details")]
[TestCaseOrderer(Constants.TestCaseOrderer, Constants.AssemblyName)]
public class PersonalDetailsTests : IClassFixture<EntityFixture<Update>>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

namespace IntegrationTests
{
[Collection(Constants.TestCollections.ResidentialAddress)]
[Collection("Residential address")]
[TestCaseOrderer(Constants.TestCaseOrderer, Constants.AssemblyName)]
public class ResidentialAddressTests : IClassFixture<EntityFixture<Update>>
{
Expand Down
Loading

0 comments on commit 77f9cff

Please sign in to comment.