diff options
author | Karen Martikyan <kamartikyan@gmail.com> | 2020-10-04 23:36:47 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-10-04 23:36:47 +0300 |
commit | 2c4ccc27d314bdf1d544cabdff7e71c3ac4e4a3c (patch) | |
tree | fac620ced83ba8bde5c4949ef05d11dafd519ca8 /Duplicati/Library/Backend | |
parent | a994ce8f7d75445e661c1a90f310b158d60377ee (diff) |
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 <karen.martikyan@digitain.com>
Diffstat (limited to 'Duplicati/Library/Backend')
7 files changed, 204 insertions, 77 deletions
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 @@ <WarningLevel>4</WarningLevel> </PropertyGroup> <ItemGroup> + <Reference Include="SharpAESCrypt, Version=1.3.3.0, Culture=neutral, PublicKeyToken=null"> + <HintPath>..\..\..\..\packages\SharpAESCrypt.exe.1.3.3\lib\netstandard2.0\SharpAESCrypt.exe</HintPath> + </Reference> <Reference Include="System" /> <Reference Include="System.Core" /> <Reference Include="System.Net.Http" /> @@ -48,7 +51,7 @@ </ItemGroup> <ItemGroup> <Compile Include="ChannelFileInfo.cs" /> - <Compile Include="InMemorySessionStore.cs" /> + <Compile Include="EncryptedFileSessionStore.cs" /> <Compile Include="StreamReadHelper.cs" /> <Compile Include="TelegramBackend.cs" /> <Compile Include="Properties\AssemblyInfo.cs" /> 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<string, byte[]> m_userIdLastSessionMap = new ConcurrentDictionary<string, byte[]>(); + + 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<string, byte[]> m_phoneSessionBytesMap = new ConcurrentDictionary<string, byte[]>(); - private readonly ConcurrentDictionary<string, string> m_phonePhoneCodeHashMap = new ConcurrentDictionary<string, string>(); - - 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 { /// <summary> - /// Private helper class to fix a bug with the StreamReader + /// Private helper class to fix a bug with the StreamReader /// </summary> - internal class StreamReadHelper : Utility.OverrideableStream + internal class StreamReadHelper : OverrideableStream { /// <summary> - /// 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 /// </summary> private bool m_empty; /// <summary> - /// Basic initialization, just pass the stream to the super class + /// Basic initialization, just pass the stream to the super class /// </summary> /// <param name="stream"></param> - public StreamReadHelper(System.IO.Stream stream) : base(stream) + public StreamReadHelper(Stream stream) : base(stream) { } /// <summary> - /// 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 /// </summary> /// <param name="buffer">The buffer to place data in</param> /// <param name="offset">The offset into the buffer to start at</param> @@ -27,7 +30,7 @@ /// <returns>The number of bytes read</returns> 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<TLAbsDocumentAttribute> { fileNameAttribute }, cancelToken).GetAwaiter().GetResult(); + m_telegramClient.SendUploadedDocument(inputPeerChannel, file, remotename, "application/zip", new TLVector<TLAbsDocumentAttribute> {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<int> { fileInfo.MessageId } + Id = new TLVector<int> {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<ChannelFileInfo>(); 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 @@ <?xml version="1.0" encoding="utf-8"?> <packages> + <package id="SharpAESCrypt.exe" version="1.3.3" targetFramework="net471" /> <package id="TLSharp" version="0.1.0.574" targetFramework="net471" /> </packages>
\ No newline at end of file |