From 2c4ccc27d314bdf1d544cabdff7e71c3ac4e4a3c Mon Sep 17 00:00:00 2001 From: Karen Martikyan Date: Mon, 5 Oct 2020 00:36:47 +0400 Subject: [PATCH] Feature/telegram backend enc sessions (#2) * Added initial state for encrypted sessions * Bugs fixed in encrypted sessions * Working state for encrypted sessions * Fixed exposing phone number/identity * Performance problems fixed, bug fixes made Co-authored-by: Karen Martikyan --- .../Duplicati.Library.Backend.Telegram.csproj | 5 +- .../Telegram/EncryptedFileSessionStore.cs | 152 ++++++++++++++++++ .../Backend/Telegram/InMemorySessionStore.cs | 41 ----- .../Backend/Telegram/StreamReadHelper.cs | 21 +-- Duplicati/Library/Backend/Telegram/Strings.cs | 2 +- .../Backend/Telegram/TelegramBackend.cs | 59 ++++--- .../Library/Backend/Telegram/packages.config | 1 + 7 files changed, 204 insertions(+), 77 deletions(-) create mode 100644 Duplicati/Library/Backend/Telegram/EncryptedFileSessionStore.cs delete mode 100644 Duplicati/Library/Backend/Telegram/InMemorySessionStore.cs diff --git a/Duplicati/Library/Backend/Telegram/Duplicati.Library.Backend.Telegram.csproj b/Duplicati/Library/Backend/Telegram/Duplicati.Library.Backend.Telegram.csproj index d6be3457c..a28e9c213 100644 --- a/Duplicati/Library/Backend/Telegram/Duplicati.Library.Backend.Telegram.csproj +++ b/Duplicati/Library/Backend/Telegram/Duplicati.Library.Backend.Telegram.csproj @@ -36,6 +36,9 @@ 4 + + ..\..\..\..\packages\SharpAESCrypt.exe.1.3.3\lib\netstandard2.0\SharpAESCrypt.exe + @@ -48,7 +51,7 @@ - + diff --git a/Duplicati/Library/Backend/Telegram/EncryptedFileSessionStore.cs b/Duplicati/Library/Backend/Telegram/EncryptedFileSessionStore.cs new file mode 100644 index 000000000..076507662 --- /dev/null +++ b/Duplicati/Library/Backend/Telegram/EncryptedFileSessionStore.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using TLSharp.Core; + +namespace Duplicati.Library.Backend +{ + public class EncryptedFileSessionStore : ISessionStore + { + private static readonly object m_lockObj = new object(); + private static readonly uint[] m_lookup32 = CreateLookup32(); + + private readonly string m_teleDataPath; + private readonly string m_password; + private readonly SHA256 m_sha = SHA256.Create(); + private static readonly ConcurrentDictionary m_userIdLastSessionMap = new ConcurrentDictionary(); + + public EncryptedFileSessionStore(string password) + { + if (string.IsNullOrWhiteSpace(password)) + { + throw new ArgumentNullException(nameof(password)); + } + + var appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + m_teleDataPath = Path.Combine(appData, nameof(Duplicati), nameof(Telegram)); + m_password = password; + + Directory.CreateDirectory(m_teleDataPath); + } + + + public void Save(Session session) + { + var sessionId = session.SessionUserId; + var filePath = GetSessionFilePath(sessionId); + var sessionBytes = session.ToBytes(); + + if (m_userIdLastSessionMap.TryGetValue(sessionId, out var sessionCache)) + { + if (sessionCache.SequenceEqual(sessionBytes)) + { + return; + } + } + + WriteToEncryptedStorage(sessionBytes, m_password, filePath, sessionId); + } + + public Session Load(string userId) + { + if (m_userIdLastSessionMap.TryGetValue(userId, out var cachedBytes)) + { + var cachedSession = Session.FromBytes(cachedBytes, this, userId); + return cachedSession; + } + + var filePath = GetSessionFilePath(userId); + var sessionBytes = ReadFromEncryptedStorage(m_password, filePath); + + if (sessionBytes == null) + { + return null; + } + + var session = Session.FromBytes(sessionBytes, this, userId); + return session; + } + + private string GetSessionFilePath(string userId) + { + userId = userId.TrimStart('+'); + var sha = GetShortSha(userId); + var sessionFilePath = Path.Combine(m_teleDataPath, $"t_{sha}.dat"); + + return sessionFilePath; + } + + private static void WriteToEncryptedStorage(byte[] bytesToWrite, string pass, string path, string sessionId) + { + lock (m_lockObj) + { + using (var sessionMs = new MemoryStream(bytesToWrite)) + using (var file = File.Open(path, FileMode.Create, FileAccess.Write)) + { + SharpAESCrypt.SharpAESCrypt.Encrypt(pass, sessionMs, file); + } + + m_userIdLastSessionMap[sessionId] = bytesToWrite; + } + } + + private byte[] ReadFromEncryptedStorage(string pass, string path) + { + var fileInfo = new FileInfo(path); + if (fileInfo.Exists == false || fileInfo.Length == 0) + { + return null; + } + + lock (m_lockObj) + { + using (var sessionMs = new MemoryStream()) + using (var file = File.Open(path, FileMode.Open, FileAccess.Read)) + { + SharpAESCrypt.SharpAESCrypt.Decrypt(pass, file, sessionMs); + return sessionMs.ToArray(); + } + } + } + + private string GetShortSha(string input) + { + var inputBytes = Encoding.UTF8.GetBytes(input); + + var longShaBytes = m_sha.ComputeHash(inputBytes); + var longSha = ByteArrayToHexViaLookup32(longShaBytes); + var result = longSha.Substring(0, 16); + + return result; + } + + private static uint[] CreateLookup32() + { + var result = new uint[256]; + for (var i = 0; i < 256; i++) + { + var s = i.ToString("X2"); + result[i] = s[0] + ((uint)s[1] << 16); + } + + return result; + } + + private static string ByteArrayToHexViaLookup32(byte[] bytes) + { + var lookup32 = m_lookup32; + var result = new char[bytes.Length * 2]; + for (var i = 0; i < bytes.Length; i++) + { + var val = lookup32[bytes[i]]; + result[2 * i] = (char)val; + result[2 * i + 1] = (char)(val >> 16); + } + + return new string(result); + } + } +} \ No newline at end of file diff --git a/Duplicati/Library/Backend/Telegram/InMemorySessionStore.cs b/Duplicati/Library/Backend/Telegram/InMemorySessionStore.cs deleted file mode 100644 index 8b63618a0..000000000 --- a/Duplicati/Library/Backend/Telegram/InMemorySessionStore.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Collections.Concurrent; -using TLSharp.Core; - -namespace Duplicati.Library.Backend -{ - public class InMemorySessionStore : ISessionStore - { - private readonly ConcurrentDictionary m_phoneSessionBytesMap = new ConcurrentDictionary(); - private readonly ConcurrentDictionary m_phonePhoneCodeHashMap = new ConcurrentDictionary(); - - public void Save(Session session) - { - m_phoneSessionBytesMap[session.SessionUserId] = session.ToBytes(); - } - - public Session Load(string phone) - { - if (m_phoneSessionBytesMap.TryGetValue(phone, out var sessionBytes)) - { - return Session.FromBytes(sessionBytes, this, phone); - } - - return null; - } - - public void SetPhoneHash(string phone, string phoneCodeHash) - { - m_phonePhoneCodeHashMap[phone] = phoneCodeHash; - } - - public string GetPhoneHash(string phone) - { - if (m_phonePhoneCodeHashMap.TryGetValue(phone, out var result)) - { - return result; - } - - return null; - } - } -} \ No newline at end of file diff --git a/Duplicati/Library/Backend/Telegram/StreamReadHelper.cs b/Duplicati/Library/Backend/Telegram/StreamReadHelper.cs index 1821f8d84..d436bdb27 100644 --- a/Duplicati/Library/Backend/Telegram/StreamReadHelper.cs +++ b/Duplicati/Library/Backend/Telegram/StreamReadHelper.cs @@ -1,25 +1,28 @@ -namespace Duplicati.Library.Backend +using System.IO; +using Duplicati.Library.Utility; + +namespace Duplicati.Library.Backend { /// - /// Private helper class to fix a bug with the StreamReader + /// Private helper class to fix a bug with the StreamReader /// - internal class StreamReadHelper : Utility.OverrideableStream + internal class StreamReadHelper : OverrideableStream { /// - /// Once the stream has returned 0 as the read count it is disposed, - /// and subsequent read requests will throw an ObjectDisposedException + /// Once the stream has returned 0 as the read count it is disposed, + /// and subsequent read requests will throw an ObjectDisposedException /// private bool m_empty; /// - /// Basic initialization, just pass the stream to the super class + /// Basic initialization, just pass the stream to the super class /// /// - public StreamReadHelper(System.IO.Stream stream) : base(stream) + public StreamReadHelper(Stream stream) : base(stream) { } /// - /// Override the read function to make sure that we only return less than the requested amount of data if the stream is exhausted + /// Override the read function to make sure that we only return less than the requested amount of data if the stream is exhausted /// /// The buffer to place data in /// The offset into the buffer to start at @@ -27,7 +30,7 @@ /// The number of bytes read public override int Read(byte[] buffer, int offset, int count) { - int readCount = 0; + var readCount = 0; int a; while (!m_empty && count > 0) diff --git a/Duplicati/Library/Backend/Telegram/Strings.cs b/Duplicati/Library/Backend/Telegram/Strings.cs index 877d1ab4e..c622f3c67 100644 --- a/Duplicati/Library/Backend/Telegram/Strings.cs +++ b/Duplicati/Library/Backend/Telegram/Strings.cs @@ -50,7 +50,7 @@ namespace Duplicati.Library.Backend public static string ChannelName => LC.L("The channel name of the backup"); #endregion - + #region Formats public const string TELEGRAM_FLOOD = "It's required to wait {0} seconds before continuing"; diff --git a/Duplicati/Library/Backend/Telegram/TelegramBackend.cs b/Duplicati/Library/Backend/Telegram/TelegramBackend.cs index 78cd0b3d9..6ef4b8419 100644 --- a/Duplicati/Library/Backend/Telegram/TelegramBackend.cs +++ b/Duplicati/Library/Backend/Telegram/TelegramBackend.cs @@ -19,12 +19,10 @@ namespace Duplicati.Library.Backend { public class Telegram : IStreamingBackend, IBackend { - private const int MEBIBYTE_IN_BYTES = 1048576; + private TelegramClient m_telegramClient; + private readonly EncryptedFileSessionStore m_encSessionStore; - private static readonly InMemorySessionStore m_sessionStore = new InMemorySessionStore(); - private static readonly string m_logTag = Log.LogTagFromType(typeof(Telegram)); private static readonly object m_lockObj = new object(); - private static TelegramClient m_telegramClient; private readonly int m_apiId; private readonly string m_apiHash; @@ -34,6 +32,10 @@ namespace Duplicati.Library.Backend private readonly string m_phoneNumber; private TLChannel m_channelCache; + private static string m_phoneCodeHash; + private static readonly string m_logTag = Log.LogTagFromType(typeof(Telegram)); + private const int BYTES_IN_MEBIBYTE = 1048576; + public Telegram() { } @@ -89,19 +91,20 @@ namespace Duplicati.Library.Backend throw new UserInformationException(Strings.NoChannelNameError, nameof(Strings.NoChannelNameError)); } + m_encSessionStore = new EncryptedFileSessionStore($"{m_apiHash}_{m_apiId}"); InitializeTelegramClient(m_apiId, m_apiHash, m_phoneNumber); } - private static void InitializeTelegramClient(int apiId, string apiHash, string phoneNumber) + private void InitializeTelegramClient(int apiId, string apiHash, string phoneNumber) { var tmpTelegramClient = m_telegramClient; - m_telegramClient = new TelegramClient(apiId, apiHash, m_sessionStore, phoneNumber); + m_telegramClient = new TelegramClient(apiId, apiHash, m_encSessionStore, phoneNumber); tmpTelegramClient?.Dispose(); } public void Dispose() { - // Do not dispose m_telegramClient because of "session-based" authentication. + m_telegramClient?.Dispose(); } public string DisplayName { get; } = Strings.DisplayName; @@ -136,14 +139,14 @@ namespace Duplicati.Library.Backend var file = m_telegramClient.UploadFile(remotename, sr, cancelToken).GetAwaiter().GetResult(); cancelToken.ThrowIfCancellationRequested(); - var inputPeerChannel = new TLInputPeerChannel { ChannelId = channel.Id, AccessHash = (long)channel.AccessHash }; + var inputPeerChannel = new TLInputPeerChannel {ChannelId = channel.Id, AccessHash = (long)channel.AccessHash}; var fileNameAttribute = new TLDocumentAttributeFilename { FileName = remotename }; EnsureConnected(cancelToken); - m_telegramClient.SendUploadedDocument(inputPeerChannel, file, remotename, "application/zip", new TLVector { fileNameAttribute }, cancelToken).GetAwaiter().GetResult(); + m_telegramClient.SendUploadedDocument(inputPeerChannel, file, remotename, "application/zip", new TLVector {fileNameAttribute}, cancelToken).GetAwaiter().GetResult(); } }, nameof(PutAsync)); @@ -158,7 +161,7 @@ namespace Duplicati.Library.Backend var fileInfo = ListChannelFileInfos().First(fi => fi.Name == remotename); var fileLocation = fileInfo.ToFileLocation(); - var limit = MEBIBYTE_IN_BYTES; + var limit = BYTES_IN_MEBIBYTE; var currentOffset = 0; @@ -219,7 +222,7 @@ namespace Duplicati.Library.Backend ChannelId = channel.Id, AccessHash = channel.AccessHash.Value }, - Id = new TLVector { fileInfo.MessageId } + Id = new TLVector {fileInfo.MessageId} }; EnsureConnected(); @@ -232,7 +235,7 @@ namespace Duplicati.Library.Backend { var channel = GetChannel(); - var inputPeerChannel = new TLInputPeerChannel { ChannelId = channel.Id, AccessHash = channel.AccessHash.Value }; + var inputPeerChannel = new TLInputPeerChannel {ChannelId = channel.Id, AccessHash = channel.AccessHash.Value}; var result = new List(); var cMinId = (int?)0; @@ -352,8 +355,7 @@ namespace Duplicati.Library.Backend yield return chat; } - if (tlDialogs?.Dialogs?.Count < 100 || - tlDialogsSlice?.Dialogs?.Count < 100) + if (tlDialogs?.Dialogs?.Count < 100 || tlDialogsSlice?.Dialogs?.Count < 100) { yield break; } @@ -389,25 +391,21 @@ namespace Duplicati.Library.Backend try { - var phoneCodeHash = m_sessionStore.GetPhoneHash(m_phoneNumber); - if (phoneCodeHash == null) + if (m_phoneCodeHash == null) { EnsureConnected(); - phoneCodeHash = m_telegramClient.SendCodeRequestAsync(m_phoneNumber).GetAwaiter().GetResult(); - m_sessionStore.SetPhoneHash(m_phoneNumber, phoneCodeHash); + m_phoneCodeHash = m_telegramClient.SendCodeRequestAsync(m_phoneNumber).GetAwaiter().GetResult(); m_telegramClient.Session.Save(); if (string.IsNullOrEmpty(m_authCode)) { throw new UserInformationException(Strings.NoAuthCodeError, nameof(Strings.NoAuthCodeError)); } - else - { - throw new UserInformationException(Strings.WrongAuthCodeError, nameof(Strings.WrongAuthCodeError)); - } + + throw new UserInformationException(Strings.WrongAuthCodeError, nameof(Strings.WrongAuthCodeError)); } - m_telegramClient.MakeAuthAsync(m_phoneNumber, phoneCodeHash, m_authCode).GetAwaiter().GetResult(); + m_telegramClient.MakeAuthAsync(m_phoneNumber, m_phoneCodeHash, m_authCode).GetAwaiter().GetResult(); } catch (CloudPasswordNeededException) { @@ -425,17 +423,28 @@ namespace Duplicati.Library.Backend m_telegramClient.Session.Save(); } + private void EnsureConnected(CancellationToken? cancelToken = null) { var isConnected = false; - while (isConnected == false) { cancelToken?.ThrowIfCancellationRequested(); + var innerCancelTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + var innerCancelToken = innerCancelTokenSource.Token; try { - isConnected = m_telegramClient.ConnectAsync().Wait(TimeSpan.FromSeconds(5)); + var connectTask = m_telegramClient.ConnectAsync(false, innerCancelToken); + connectTask.Wait(cancelToken ?? default); + isConnected = true; + } + catch (OperationCanceledException canceledException) + { + if (canceledException.CancellationToken != innerCancelToken) + { + throw; + } } catch (Exception) { diff --git a/Duplicati/Library/Backend/Telegram/packages.config b/Duplicati/Library/Backend/Telegram/packages.config index b87a4d435..ecd8f06db 100644 --- a/Duplicati/Library/Backend/Telegram/packages.config +++ b/Duplicati/Library/Backend/Telegram/packages.config @@ -1,4 +1,5 @@  + \ No newline at end of file