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>
This commit is contained in:
co-authored by
Karen Martikyan
parent
a994ce8f7d
commit
2c4ccc27d3
@@ -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" />
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user