Baseline: Ausgangszustand vor Modularisierung

Erster Commit des bestehenden monolithischen WinForms-Copytraders,
inklusive der Alt-Backups (*.bak), damit diese dauerhaft in der
Historie rekonstruierbar bleiben. Threema-Lib unter libs/ wurde
vendored (nested .git entfernt).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
bergm
2026-07-01 13:16:16 +02:00
co-authored by Claude Opus 4.8
commit 475d396f80
147 changed files with 25455 additions and 0 deletions
@@ -0,0 +1,402 @@
using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using IcgSoftware.Threema.CoreMsgApi.Exceptions;
using IcgSoftware.Threema.CoreMsgApi.Results;
using Microsoft.Extensions.Configuration;
namespace IcgSoftware.Threema.CoreMsgApi
{
/// <summary>
/// Facilitates HTTPS communication with the Threema Message API.
/// </summary>
public class APIConnector
{
public const string DEFAULTAPIURL = "https://msgapi.threema.ch/";
private readonly string apiUrl;
private readonly PublicKeyStore publicKeyStore;
private readonly string apiIdentity;
private readonly string secret;
public APIConnector(string apiIdentity, string secret, PublicKeyStore publicKeyStore) :
this(apiIdentity, secret, APIConnector.DEFAULTAPIURL, publicKeyStore)
{
}
public APIConnector(string apiIdentity, string secret, string apiUrl, PublicKeyStore publicKeyStore)
{
this.apiIdentity = apiIdentity;
this.secret = secret;
this.apiUrl = apiUrl;
if (publicKeyStore != null)
{
this.publicKeyStore = publicKeyStore;
}
else
{
this.publicKeyStore = this.GetSQLiteDbProvider();
}
}
/// <summary>
/// Lookup credits for an ID.
/// </summary>
/// <returns>credits or null</returns>
public int? LookupCredits()
{
string res = DoGet(new Uri(this.apiUrl + "credits"),
MakeRequestParams());
if(res != null)
{
int credits;
int.TryParse(res, out credits);
return credits;
}
return null;
}
/// <summary>
/// Lookup an ID by email address. The email address will be hashed before
/// being sent to the server.
/// </summary>
/// <param name="email">the email address</param>
/// <returns>the ID, or null if not found</returns>
public string LookupEmail(string email)
{
try
{
Dictionary<string, string> getParams = MakeRequestParams();
byte[] emailHash = CryptTool.HashEmail(email);
return DoGet(new Uri(this.apiUrl + "lookup/email_hash/" + DataUtils.ByteArrayToHexString(emailHash)), getParams);
}
catch (FileNotFoundException)
{
return null;
}
}
/// <summary>
/// Lookup a public key by ID.
/// </summary>
/// <param name="id">The ID whose public key is desired</param>
/// <returns>The corresponding public key, or null if not found</returns>
public byte[] LookupKey(string id)
{
byte[] key = this.publicKeyStore.GetPublicKey(id);
if (key == null)
{
try
{
Dictionary<string, string> getParams = MakeRequestParams();
string pubkeyHex = DoGet(new Uri(this.apiUrl + "pubkeys/" + id), getParams);
key = DataUtils.HexStringToByteArray(pubkeyHex);
this.publicKeyStore.SetPublicKey(id, key);
}
catch (FileNotFoundException)
{
return null;
}
}
return key;
}
/// <summary>
/// Lookup the capabilities of a ID
/// </summary>
/// <param name="threemaId">The ID whose capabilities should be checked</param>
/// <returns>The capabilities, or null if not found</returns>
public CapabilityResult LookupKeyCapability(string threemaId)
{
string res = DoGet(new Uri(this.apiUrl + "capabilities/" + threemaId),
MakeRequestParams());
if (res != null)
{
return new CapabilityResult(threemaId, res.Split(','));
}
return null;
}
/// <summary>
/// Lookup an ID by phone number. The phone number will be hashed before
/// being sent to the server.
/// </summary>
/// <param name="phoneNumber">the phone number in E.164 format</param>
/// <returns>the ID, or null if not found</returns>
public string LookupPhone(string phoneNumber)
{
try
{
Dictionary<string, string> getParams = MakeRequestParams();
byte[] phoneHash = CryptTool.HashPhoneNo(phoneNumber);
return DoGet(new Uri(this.apiUrl + "lookup/phone_hash/" + DataUtils.ByteArrayToHexString(phoneHash)), getParams);
}
catch (FileNotFoundException)
{
return null;
}
}
/// <summary>
/// Download a file given its blob ID.
/// </summary>
/// <param name="blobId">The blob ID of the file</param>
/// <returns>Encrypted file data</returns>
public byte[] DownloadFile(byte[] blobId)
{
return this.DownloadFile(blobId, null);
}
/// <summary>
/// Download a file given its blob ID.
/// </summary>
/// <param name="blobId">The blob ID of the file</param>
/// <param name="progressListener">An object that will receive progress information, or null</param>
/// <returns>Encrypted file data</returns>
public byte[] DownloadFile(byte[] blobId, IProgressListener progressListener)
{
string queryString = MakeUrlEncoded(MakeRequestParams());
Uri blobUrl = new Uri(string.Format(this.apiUrl + "blobs/{0}?{1}",
DataUtils.ByteArrayToHexString(blobId), queryString));
byte[] blob;
WebRequest request = WebRequest.CreateHttp(blobUrl);
request.Method = "GET";
request.Timeout = 20 * 1000;
using (WebResponse response = request.GetResponse())
using (Stream stream = response.GetResponseStream())
{
blob = DataUtils.StreamToBytes(stream, progressListener);
stream.Close();
response.Close();
}
return blob;
}
/// <summary>
/// Upload a file.
/// </summary>
/// <param name="fileEncryptionResult">The result of the file encryption (i.e. encrypted file data)</param>
/// <returns>the result of the upload</returns>
public UploadResult UploadFile(EncryptResult fileEncryptionResult)
{
string attachmentName = "blob";
string attachmentFileName = "blob.file";
string crlf = "\r\n";
string twoHyphens = "--";
char[] chars = "-_1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".ToCharArray();
string boundary = string.Empty;
Random rand = new Random();
int count = rand.Next(11) + 30;
for (int i = 0; i < count; i++)
{
boundary += chars[rand.Next(chars.Length)];
}
byte[] header = Encoding.UTF8.GetBytes(twoHyphens + boundary + crlf +
"Content-Disposition: form-data; name=\"" + attachmentName + "\";filename=\"" + attachmentFileName + "\"" + crlf + crlf
);
byte[] footer = Encoding.UTF8.GetBytes(crlf + twoHyphens + boundary + twoHyphens + crlf);
byte[] postData = new byte[header.Length + fileEncryptionResult.Result.Length + footer.Length];
header.CopyTo(postData, 0);
fileEncryptionResult.Result.CopyTo(postData, header.Length);
footer.CopyTo(postData, header.Length + fileEncryptionResult.Result.Length);
string queryString = MakeUrlEncoded(MakeRequestParams());
Uri url = new Uri(this.apiUrl + "upload_blob?" + queryString);
WebRequest request = WebRequest.CreateHttp(url);
request.Method = "POST";
if (!WebHeaderCollection.IsRestricted("Connection"))
{
request.Headers.Set(HttpRequestHeader.Connection, "Keep-Alive");
}
if (!WebHeaderCollection.IsRestricted("CacheControl"))
{
request.Headers.Add(HttpRequestHeader.CacheControl, "no-cache");
}
request.ContentType = "multipart/form-data;boundary=" + boundary;
request.ContentLength = postData.Length;
using (Stream stream = request.GetRequestStream())
{
stream.Write(postData, 0, postData.Length);
stream.Close();
}
string responseData;
HttpStatusCode responseCode = GetResponse(request, out responseData);
return new UploadResult((int)responseCode, responseData != null ? DataUtils.HexStringToByteArray(responseData) : null);
}
/// <summary>
/// Send an end-to-end encrypted message.
/// </summary>
/// <param name="to">recipient ID</param>
/// <param name="nonce">nonce used for encryption (24 bytes)</param>
/// <param name="box">encrypted message data (max. 4000 bytes)</param>
/// <returns>message ID</returns>
public string SendE2EMessage(string to, byte[] nonce, byte[] box)
{
Dictionary<string, string> postParams = MakeRequestParams();
postParams.Add("to", to);
postParams.Add("nonce", DataUtils.ByteArrayToHexString(nonce));
postParams.Add("box", DataUtils.ByteArrayToHexString(box));
return DoPost(new Uri(this.apiUrl + "send_e2e"), postParams);
}
/// <summary>
/// Send a text message with server-side encryption.
/// </summary>
/// <param name="to">recipient ID</param>
/// <param name="text">message text (max. 3500 bytes)</param>
/// <returns>message ID</returns>
public string SendTextMessageSimple(string to, string text)
{
Dictionary<string, string> postParams = MakeRequestParams();
postParams.Add("to", to);
postParams.Add("text", text);
return DoPost(new Uri(this.apiUrl + "send_simple"), postParams);
}
private string DoGet(Uri url, Dictionary<string, string> getParams)
{
if (getParams != null)
{
string queryString = MakeUrlEncoded(getParams);
url = new Uri(url.ToString() + "?" + queryString);
}
WebRequest request = WebRequest.CreateHttp(url);
request.Method = "GET";
string responseData;
HttpStatusCode responseCode = GetResponse(request, out responseData);
if (responseCode != HttpStatusCode.OK)
{
throw new HttpListenerException((int)responseCode);
}
return responseData;
}
private string DoPost(Uri url, Dictionary<string, string> postParams)
{
ASCIIEncoding encoding = new ASCIIEncoding();
byte[] postData = encoding.GetBytes(MakeUrlEncoded(postParams));
WebRequest request = WebRequest.CreateHttp(url);
request.Method = "POST";
request.Headers.Add(HttpRequestHeader.AcceptCharset, "utf-8");
request.ContentType = "application/x-www-form-urlencoded";
request.ContentLength = postData.Length;
using (Stream stream = request.GetRequestStream())
{
stream.Write(postData, 0, postData.Length);
stream.Close();
}
string responseData;
HttpStatusCode responseCode = GetResponse(request, out responseData);
if (responseCode != HttpStatusCode.OK)
{
throw new HttpListenerException((int)responseCode);
}
return responseData;
}
private HttpStatusCode GetResponse(WebRequest request, out string responseData)
{
responseData = null;
WebResponse response = request.GetResponse();
HttpStatusCode status = ((HttpWebResponse)response).StatusCode;
if (status == HttpStatusCode.OK)
{
using (Stream stream = response.GetResponseStream())
using (StreamReader reader = new StreamReader(stream))
{
responseData = reader.ReadToEnd();
reader.Close();
stream.Close();
}
}
response.Close();
return status;
}
private Dictionary<string,string> MakeRequestParams()
{
Dictionary<string, string> postParams = new Dictionary<string, string>();
postParams.Add("from", apiIdentity);
postParams.Add("secret", secret);
return postParams;
}
private String MakeUrlEncoded(Dictionary<string, string> parameters)
{
StringBuilder s = new StringBuilder();
foreach (KeyValuePair<String,String> param in parameters)
{
if (s.Length > 0)
{
s.Append('&');
}
s.Append(param.Key);
s.Append('=');
s.Append(DataUtils.Utf8Endcode(WebUtility.UrlEncode(param.Value)));
}
return s.ToString();
}
private PublicKeyStore GetSQLiteDbProvider()
{
PublicKeyStore store = null;
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
IConfigurationRoot configuration = builder.Build();
string connectionString = configuration.GetConnectionString("SQLiteConnectionString");
if (!String.IsNullOrWhiteSpace(connectionString))
{
store = new PublicKeyStoreDb(connectionString);
}
else
{
store = new PublicKeyStoreNone();
}
return store;
}
}
}
@@ -0,0 +1,133 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using IcgSoftware.Threema.CoreMsgApi.Messages;
using IcgSoftware.Threema.CoreMsgApi.Results;
namespace IcgSoftware.Threema.CoreMsgApi.Com
{
[ComVisible(true)]
[ProgId("Threema.MsgApi.Com.CryptTool")]
[Guid("0B98D653-CD23-4827-9C5B-AEC0DCFBD142")]
public class CryptToolWrapper : ICryptToolWrapper
{
/// <summary>
/// Wrapper to encrypt text <see cref="Threema.MsgApi.CryptTool.EncryptTextMessage"/>
/// </summary>
/// <param name="text">Text to encrypt</param>
/// <param name="senderPrivateKey">Sender private key as hex-string</param>
/// <param name="recipientPublicKey">Recipient public key as hex-string</param>
/// <returns>Array with encrypted text, nonce and size</returns>
public ArrayList EncryptTextMessage(string text, string senderPrivateKey, string recipientPublicKey)
{
byte[] privateKey = GetKey(senderPrivateKey, Key.KeyType.PRIVATE);
byte[] publicKey = GetKey(recipientPublicKey, Key.KeyType.PUBLIC);
string textEncoded = DataUtils.Utf8Endcode(text);
EncryptResult encryptResult = CryptTool.EncryptTextMessage(textEncoded, privateKey, publicKey);
var result = new ArrayList();
result.Add(DataUtils.ByteArrayToHexString(encryptResult.Result));
result.Add(DataUtils.ByteArrayToHexString(encryptResult.Nonce));
result.Add(encryptResult.Size.ToString());
return result;
}
/// <summary>
/// Wrapper to decrypt box <see cref="Threema.MsgApi.CryptTool.DecryptMessage"/>
/// </summary>
/// <param name="box">Encrypted box as hex-straing</param>
/// <param name="recipientPrivateKey">Recipient private key as hex-string</param>
/// <param name="senderPublicKey">Sender public key as hex-string</param>
/// <param name="nonce">Nonce as hex-string</param>
/// <returns>Array with type and decrypted message</returns>
public ArrayList DecryptMessage(string box, string recipientPrivateKey, string senderPublicKey, string nonce)
{
byte[] privateKey = GetKey(recipientPrivateKey, Key.KeyType.PRIVATE);
byte[] publicKey = GetKey(senderPublicKey, Key.KeyType.PUBLIC);
byte[] nonceBytes = DataUtils.HexStringToByteArray(nonce);
byte[] boxBytes = DataUtils.HexStringToByteArray(box);
ThreemaMessage message = CryptTool.DecryptMessage(boxBytes, privateKey, publicKey, nonceBytes);
var result = new ArrayList();
result.Add(message.GetTypeCode().ToString());
result.Add(message.ToString());
return result;
}
/// <summary>
/// Wrapper to hash email <see cref="Threema.MsgApi.CryptTool.HashEmail"/>
/// </summary>
/// <param name="email">Email adress</param>
/// <returns>Hash of email adress as hex-string</returns>
public string HashEmail(string email)
{
byte[] emailHash = CryptTool.HashEmail(email);
return DataUtils.ByteArrayToHexString(emailHash);
}
/// <summary>
/// Wrapper to hash email <see cref="Threema.MsgApi.CryptTool.HashPhoneNo"/>
/// </summary>
/// <param name="phoneNo">Phone number</param>
/// <returns>Hash of phone number as hex-string</returns>
public string HashPhoneNo(string phoneNo)
{
byte[] phoneHash = CryptTool.HashPhoneNo(phoneNo);
return DataUtils.ByteArrayToHexString(phoneHash);
}
/// <summary>
/// Wrapper to generate key pair <see cref="Threema.MsgApi.CryptTool.GenerateKeyPair"/>
/// </summary>
/// <param name="privateKeyPath">Full path name of private key file</param>
/// <param name="publicKeyPath">Full path name of public key file</param>
public void GenerateKeyPair(string privateKeyPath, string publicKeyPath)
{
byte[] privateKey = new byte[32]; //NaCl.SECRETKEYBYTES
byte[] publicKey = new byte[32]; //NaCl.PUBLICKEYBYTES
CryptTool.GenerateKeyPair(ref privateKey, ref publicKey);
// Write both keys to file
DataUtils.WriteKeyFile(privateKeyPath, new Key(Key.KeyType.PRIVATE, privateKey));
DataUtils.WriteKeyFile(publicKeyPath, new Key(Key.KeyType.PUBLIC, publicKey));
}
/// <summary>
/// Wrapper to derive public key <see cref="CryptTool.DerivePublicKey"/>
/// </summary>
/// <param name="privateKey">private key as file path or hex-string</param>
/// <returns>Public key as hex-string</returns>
public string DerivePublicKey(string privateKey)
{
byte[] privateKeyBytes = GetKey(privateKey, Key.KeyType.PRIVATE);
byte[] publicKey = CryptTool.DerivePublicKey(privateKeyBytes);
return new Key(Key.KeyType.PUBLIC, publicKey).Encode();
}
private byte[] GetKey(string argument, string expectedKeyType)
{
Key key;
if (File.Exists(argument))
{
key = DataUtils.ReadKeyFile(argument, expectedKeyType);
}
else
{
key = IcgSoftware.Threema.CoreMsgApi.Key.DecodeKey(argument, expectedKeyType);
}
return key.key;
}
}
}
@@ -0,0 +1,23 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using IcgSoftware.Threema.CoreMsgApi.Results;
namespace IcgSoftware.Threema.CoreMsgApi.Com
{
[ComVisible(true)]
[Guid("4F376DEB-6F78-460A-836F-B38371712EFF")]
public interface ICryptToolWrapper
{
ArrayList EncryptTextMessage(string text, string senderPrivateKey, string recipientPublicKey);
ArrayList DecryptMessage(string box, string recipientPrivateKey, string senderPublicKey, string nonce);
string HashEmail(string email);
string HashPhoneNo(string phoneNo);
void GenerateKeyPair(string privateKeyPath, string publicKeyPath);
string DerivePublicKey(string privateKey);
}
}
@@ -0,0 +1,26 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace IcgSoftware.Threema.CoreMsgApi.Com
{
[ComVisible(true)]
[Guid("E9E32936-CCAB-4297-BAB6-7F3B5939F3D4")]
public interface IMessageToolWrapper
{
string SendTextMessageSimple(string to, string from, string secret, string text, string apiUrl = APIConnector.DEFAULTAPIURL);
string SendTextMessage(string to, string from, string secret, string privateKey, string text, string apiUrl = APIConnector.DEFAULTAPIURL);
string SendImageMessage(string to, string from, string secret, string privateKey, string imageFilePath, string apiUrl = APIConnector.DEFAULTAPIURL);
string SendFileMessage(string to, string from, string secret, string privateKey, string file, string thumbnail = null, string apiUrl = APIConnector.DEFAULTAPIURL);
string LookupEmail(string email, string from, string secret, string apiUrl = APIConnector.DEFAULTAPIURL);
string LookupPhone(string phoneNo, string from, string secret, string apiUrl = APIConnector.DEFAULTAPIURL);
string LookupKey(string threemaId, string from, string secret, string apiUrl = APIConnector.DEFAULTAPIURL);
ArrayList LookupKeyCapability(string threemaId, string from, string secret, string apiUrl = APIConnector.DEFAULTAPIURL);
int? LookupCredits(string from, string secret, string apiUrl = APIConnector.DEFAULTAPIURL);
ArrayList ReceiveMessage(string id, string from, string secret, string privateKey, string messageId, string nonce, string box, string outputFolder = null, string apiUrl = APIConnector.DEFAULTAPIURL);
}
}
@@ -0,0 +1,221 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using IcgSoftware.Threema.CoreMsgApi.Helpers;
using IcgSoftware.Threema.CoreMsgApi.Results;
namespace IcgSoftware.Threema.CoreMsgApi.Com
{
[ComVisible(true)]
[ProgId("Threema.MsgApi.Com.MessageTool")]
[Guid("097F1D26-B38E-4501-845D-6DE7DBFB5EAA")]
public class MessageToolWrapper : IMessageToolWrapper
{
/// <summary>
/// Wrapper to send simple message <see cref="Threema.MsgApi.APIConnector.SendTextMessageSimple"/>
/// </summary>
/// <param name="to">Recipient id</param>
/// <param name="from">Sender id</param>
/// <param name="secret">Sender sercret</param>
/// <param name="text">Text message</param>
/// <param name="apiUrl">Optional api url</param>
/// <returns>Message id</returns>
public string SendTextMessageSimple(string to, string from, string secret, string text, string apiUrl = APIConnector.DEFAULTAPIURL)
{
APIConnector apiConnector = this.CreateConnector(from, secret, apiUrl);
return apiConnector.SendTextMessageSimple(to, DataUtils.Utf8Endcode(text));
}
/// <summary>
/// Wrapper to send text message E2E <see cref="Threema.MsgApi.E2EHelper.SendTextMessage"/>
/// </summary>
/// <param name="to">Recipient id</param>
/// <param name="from">Sender id</param>
/// <param name="secret">Sender sercret</param>
/// <param name="privateKey">Sender private key</param>
/// <param name="text">Text message</param>
/// <param name="apiUrl">Optional api url</param>
/// <returns>Message id</returns>
public string SendTextMessage(string to, string from, string secret, string privateKey, string text, string apiUrl = APIConnector.DEFAULTAPIURL)
{
byte[] privateKeyBytes = GetKey(privateKey, Key.KeyType.PRIVATE);
E2EHelper e2EHelper = new E2EHelper(this.CreateConnector(from, secret, apiUrl), privateKeyBytes);
return e2EHelper.SendTextMessage(to, DataUtils.Utf8Endcode(text));
}
/// <summary>
/// Wrapper to send image message E2E <see cref="Threema.MsgApi.E2EHelper.SendImageMessage"/>
/// </summary>
/// <param name="to">Recipient id</param>
/// <param name="from">Sender id</param>
/// <param name="secret">Sender sercret</param>
/// <param name="privateKey">Sender private key</param>
/// <param name="imageFilePath">File path to image</param>
/// <param name="apiUrl">Optional api url</param>
/// <returns>Message id</returns>
public string SendImageMessage(string to, string from, string secret, string privateKey, string imageFilePath, string apiUrl = APIConnector.DEFAULTAPIURL)
{
byte[] privateKeyBytes = GetKey(privateKey, Key.KeyType.PRIVATE);
E2EHelper e2EHelper = new E2EHelper(this.CreateConnector(from, secret, apiUrl), privateKeyBytes);
return e2EHelper.SendImageMessage(to, imageFilePath);
}
/// <summary>
/// Wrapper to send file message E2E <see cref="Threema.MsgApi.E2EHelper.SendFileMessage"/>
/// </summary>
/// <param name="to">Recipient id</param>
/// <param name="from">Sender id</param>
/// <param name="secret">Sender sercret</param>
/// <param name="privateKey">Sender private key</param>
/// <param name="file">File path to file</param>
/// <param name="thumbnail">File path to thumbnail</param>
/// <param name="apiUrl">Optional api url</param>
/// <returns>Message id</returns>
public string SendFileMessage(string to, string from, string secret, string privateKey, string file, string thumbnail = null, string apiUrl = APIConnector.DEFAULTAPIURL)
{
byte[] privateKeyBytes = GetKey(privateKey, Key.KeyType.PRIVATE);
FileInfo fileInfo = file != null ? new FileInfo(file) : null;
FileInfo thumbnailInfo = thumbnail != null ? new FileInfo(thumbnail) : null;
E2EHelper e2EHelper = new E2EHelper(this.CreateConnector(from, secret, apiUrl), privateKeyBytes);
return e2EHelper.SendFileMessage(to, fileInfo, thumbnailInfo);
}
/// <summary>
/// Wrapper to id lookup via email <see cref="Threema.MsgApi.APIConnector.LookupEmail"/>
/// </summary>
/// <param name="email">Email for lookup</param>
/// <param name="from">Sender id</param>
/// <param name="secret">Sender secret</param>
/// <param name="apiUrl">Optional api url</param>
/// <returns>id</returns>
public string LookupEmail(string email, string from, string secret, string apiUrl = APIConnector.DEFAULTAPIURL)
{
APIConnector apiConnector = this.CreateConnector(from, secret, apiUrl);
return apiConnector.LookupEmail(email);
}
/// <summary>
/// Wrapper to id lookup via phone number <see cref="Threema.MsgApi.APIConnector.LookupPhone"/>
/// </summary>
/// <param name="phoneNo">Phone number for lookup</param>
/// <param name="from">Sender id</param>
/// <param name="secret">Sender secret</param>
/// <param name="apiUrl">Optional api url</param>
/// <returns>id</returns>
public string LookupPhone(string phoneNo, string from, string secret, string apiUrl = APIConnector.DEFAULTAPIURL)
{
APIConnector apiConnector = this.CreateConnector(from, secret, apiUrl);
return apiConnector.LookupPhone(phoneNo);
}
/// <summary>
/// Wrapper to lookup/fetch public key <see cref="Threema.MsgApi.APIConnector.LookupKey"/>
/// </summary>
/// <param name="threemaId">Id for lookup</param>
/// <param name="from">Sender id</param>
/// <param name="secret">Sender secret</param>
/// <param name="apiUrl">Optional api url</param>
/// <returns>public key has hex-string</returns>
public string LookupKey(string threemaId, string from, string secret, string apiUrl = APIConnector.DEFAULTAPIURL)
{
APIConnector apiConnector = this.CreateConnector(from, secret, apiUrl);
byte[] publicKey = apiConnector.LookupKey(threemaId);
if (publicKey != null)
{
return new Key(Key.KeyType.PUBLIC, publicKey).Encode();
}
return null;
}
/// <summary>
/// Wrapper to lookup capabilities <see cref="Threema.MsgApi.APIConnector.LookupKeyCapability"/>
/// </summary>
/// <param name="threemaId">Id for lookup</param>
/// <param name="from">Sender id</param>
/// <param name="secret">Sender secret</param>
/// <param name="apiUrl">Optional api url</param>
/// <returns>Array with capatilities</returns>
public System.Collections.ArrayList LookupKeyCapability(string threemaId, string from, string secret, string apiUrl = APIConnector.DEFAULTAPIURL)
{
CapabilityResult capabilities = this.CreateConnector(from, secret, apiUrl)
.LookupKeyCapability(threemaId);
var result = new ArrayList();
capabilities.Capabilities.ToList().ForEach(c => result.Add(c));
return result;
}
/// <summary>
/// Wrapper to lookup credits <see cref="Threema.MsgApi.APIConnector.LookupCredits"/>
/// </summary>
/// <param name="from">From id</param>
/// <param name="secret">From secret</param>
/// <param name="apiUrl">Optional api url</param>
/// <returns>credits or null</returns>
public int? LookupCredits(string from, string secret, string apiUrl = APIConnector.DEFAULTAPIURL)
{
return this.CreateConnector(from, secret, apiUrl).LookupCredits();
}
/// <summary>
/// Wrapper to receive message and download files <see cref="Threema.MsgApi.Helpers.E2EHelper.ReceiveMessage"/>
/// </summary>
/// <param name="id">Sender id</param>
/// <param name="from">From id</param>
/// <param name="secret">From secret</param>
/// <param name="privateKey">From private key</param>
/// <param name="messageId">Message id</param>
/// <param name="nonce">Nonce as hex-string</param>
/// <param name="box">Box message as hex-string</param>
/// <param name="outputFolder">Optional path to output folder</param>
/// <param name="apiUrl">Optional api url</param>
/// <returns>Array with message-type, message-id and message</returns>
public ArrayList ReceiveMessage(string id, string from, string secret, string privateKey, string messageId, string nonce, string box, string outputFolder = null, string apiUrl = APIConnector.DEFAULTAPIURL)
{
byte[] privateKeyBytes = GetKey(privateKey, Key.KeyType.PRIVATE);
byte[] nonceBytes = DataUtils.HexStringToByteArray(nonce);
E2EHelper e2EHelper = new E2EHelper(this.CreateConnector(from, secret, apiUrl), privateKeyBytes);
byte[] boxBytes = DataUtils.HexStringToByteArray(box);
ReceiveMessageResult res = e2EHelper.ReceiveMessage(id, messageId, boxBytes, nonceBytes, outputFolder);
ArrayList result = new ArrayList();
result.Add(res.Message.GetTypeCode().ToString());
result.Add(res.MessageId);
result.Add(res.Message.ToString());
return result;
}
private APIConnector CreateConnector(string gatewayId, string secret, string apiUrl)
{
return new APIConnector(gatewayId, secret, apiUrl, new PublicKeyStoreNone());
}
private byte[] GetKey(string argument, string expectedKeyType)
{
Key key;
if (File.Exists(argument))
{
key = DataUtils.ReadKeyFile(argument, expectedKeyType);
}
else
{
key = IcgSoftware.Threema.CoreMsgApi.Key.DecodeKey(argument, expectedKeyType);
}
return key.key;
}
}
}
@@ -0,0 +1,380 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using IcgSoftware.Threema.CoreMsgApi.Exceptions;
using IcgSoftware.Threema.CoreMsgApi.Messages;
using IcgSoftware.Threema.CoreMsgApi.Results;
namespace IcgSoftware.Threema.CoreMsgApi
{
/// <summary>
/// Contains static methods to do various Threema cryptography related tasks.
/// </summary>
public class CryptTool
{
// HMAC-SHA256 keys for email/mobile phone hashing
private static readonly byte[] EMAIL_HMAC_KEY = new byte[] {(byte)0x30,(byte)0xa5,(byte)0x50,(byte)0x0f,(byte)0xed,(byte)0x97,(byte)0x01,(byte)0xfa,(byte)0x6d,(byte)0xef,(byte)0xdb,(byte)0x61,(byte)0x08,(byte)0x41,(byte)0x90,(byte)0x0f,(byte)0xeb,(byte)0xb8,(byte)0xe4,(byte)0x30,(byte)0x88,(byte)0x1f,(byte)0x7a,(byte)0xd8,(byte)0x16,(byte)0x82,(byte)0x62,(byte)0x64,(byte)0xec,(byte)0x09,(byte)0xba,(byte)0xd7};
private static readonly byte[] PHONENO_HMAC_KEY = new byte[] {(byte)0x85,(byte)0xad,(byte)0xf8,(byte)0x22,(byte)0x69,(byte)0x53,(byte)0xf3,(byte)0xd9,(byte)0x6c,(byte)0xfd,(byte)0x5d,(byte)0x09,(byte)0xbf,(byte)0x29,(byte)0x55,(byte)0x5e,(byte)0xb9,(byte)0x55,(byte)0xfc,(byte)0xd8,(byte)0xaa,(byte)0x5e,(byte)0xc4,(byte)0xf9,(byte)0xfc,(byte)0xd8,(byte)0x69,(byte)0xe2,(byte)0x58,(byte)0x37,(byte)0x07,(byte)0x23};
private static readonly byte[] FILE_NONCE = new byte[] {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01};
private static readonly byte[] FILE_THUMBNAIL_NONCE = new byte[] {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02};
private const int SYMMKEYBYTES = 32;
/// <summary>
/// Encrypt a text message.
/// </summary>
/// <param name="text">the text to be encrypted (max. 3500 bytes)</param>
/// <param name="senderPrivateKey">the private key of the sending ID</param>
/// <param name="recipientPublicKey">the public key of the receiving ID</param>
/// <returns></returns>
public static EncryptResult EncryptTextMessage(String text, byte[] senderPrivateKey, byte[] recipientPublicKey)
{
return EncryptMessage(new TextMessage(text), senderPrivateKey, recipientPublicKey);
}
/// <summary>
/// Encrypt an image message.
/// </summary>
/// <param name="encryptResult">result of the image encryption</param>
/// <param name="uploadResult">result of the upload</param>
/// <param name="senderPrivateKey">the private key of the sending ID</param>
/// <param name="recipientPublicKey">the public key of the receiving ID</param>
/// <returns>encrypted result</returns>
public static EncryptResult EncryptImageMessage(EncryptResult encryptResult, UploadResult uploadResult, byte[] senderPrivateKey, byte[] recipientPublicKey)
{
return EncryptMessage(
new ImageMessage(uploadResult.BlobId,
encryptResult.Size,
encryptResult.Nonce),
senderPrivateKey,
recipientPublicKey);
}
/// <summary>
/// Encrypt a file message.
/// </summary>
/// <param name="encryptResult">result of the file data encryption</param>
/// <param name="uploadResult">result of the upload</param>
/// <param name="mimeType">MIME type of the file</param>
/// <param name="fileName">File name</param>
/// <param name="fileSize">Size of the file, in bytes</param>
/// <param name="uploadResultThumbnail">result of thumbnail upload</param>
/// <param name="senderPrivateKey">Private key of sender</param>
/// <param name="recipientPublicKey">Public key of recipient</param>
/// <returns>Result of the file message encryption (not the same as the file data encryption!)</returns>
public static EncryptResult EncryptFileMessage(EncryptResult encryptResult,
UploadResult uploadResult,
String mimeType,
String fileName,
int fileSize,
UploadResult uploadResultThumbnail,
byte[] senderPrivateKey, byte[] recipientPublicKey)
{
return EncryptMessage(
new FileMessage(uploadResult.BlobId,
encryptResult.Secret,
mimeType,
fileName,
fileSize,
uploadResultThumbnail != null ? uploadResultThumbnail.BlobId : null),
senderPrivateKey,
recipientPublicKey);
}
private static EncryptResult EncryptMessage(ThreemaMessage threemaMessage, byte[] privateKey, byte[] publicKey)
{
// determine random amount of PKCS7 padding
int padbytes = new Random().Next(254) + 1;
byte[] messageBytes;
try
{
messageBytes = threemaMessage.GetData();
}
catch
{
return null;
}
// prepend type byte (0x02) to message data
byte[] data = new byte[1 + messageBytes.Length + padbytes];
data[0] = (byte)threemaMessage.GetTypeCode();
messageBytes.CopyTo(data, 1);
// append padding
for (int i = 0; i < padbytes; i++)
{
data[i + 1 + messageBytes.Length] = (byte)padbytes;
}
return Encrypt(data, privateKey, publicKey);
}
/// <summary>
/// Decrypt an NaCl box using the recipient's private key and the sender's public key.
/// </summary>
/// <param name="box">The box to be decrypted</param>
/// <param name="privateKey">The private key of the recipient</param>
/// <param name="publicKey">The public key of the sender</param>
/// <param name="nonce">The nonce that was used for encryption</param>
/// <returns>The decrypted data, or null if decryption failed</returns>
public static byte[] Decrypt(byte[] box, byte[] privateKey, byte[] publicKey, byte[] nonce)
{
return Sodium.PublicKeyBox.Open(box, nonce, privateKey, publicKey);
}
/// <summary>
/// Decrypt symmetrically encrypted file data.
/// </summary>
/// <param name="fileData">The encrypted file data</param>
/// <param name="secret">The symmetric key that was used for encryption</param>
/// <returns>The decrypted file data, or null if decryption failed</returns>
public static byte[] DecryptFileData(byte[] fileData, byte[] secret)
{
byte[] box = new byte[fileData.Length + 16];
fileData.CopyTo(box, 16);
return Sodium.SecretBox.Open(box, FILE_NONCE, secret);
}
/// <summary>
/// Decrypt symmetrically encrypted file thumbnail data.
/// </summary>
/// <param name="fileData">The encrypted thumbnail data</param>
/// <param name="secret">The symmetric key that was used for encryption</param>
/// <returns>The decrypted thumbnail data, or null if decryption failed</returns>
public static byte[] DecryptFileThumbnailData(byte[] fileData, byte[] secret)
{
byte[] box = new byte[fileData.Length + 16];
fileData.CopyTo(box, 16);
return Sodium.SecretBox.Open(box, FILE_THUMBNAIL_NONCE, secret);
}
/// <summary>
/// Decrypt a message.
/// </summary>
/// <param name="box">the box to be decrypted</param>
/// <param name="recipientPrivateKey">the private key of the receiving ID</param>
/// <param name="senderPublicKey">the public key of the sending ID</param>
/// <param name="nonce">the nonce that was used for the encryption</param>
/// <returns>decrypted message (text or delivery receipt)</returns>
public static ThreemaMessage DecryptMessage(byte[] box, byte[] recipientPrivateKey, byte[] senderPublicKey, byte[] nonce)
{
byte[] data = Decrypt(box, recipientPrivateKey, senderPublicKey, nonce);
if (data == null)
{
throw new DecryptionFailedException();
}
// remove padding
int padbytes = data[data.Length - 1] & 0xFF;
int realDataLength = data.Length - padbytes;
if (realDataLength < 1)
{
// Bad message padding
throw new BadMessageException();
}
// first byte of data is type
int type = data[0] & 0xFF;
switch (type)
{
case TextMessage.TYPE_CODE:
// Text message
if (realDataLength < 2)
{
throw new BadMessageException();
}
return new TextMessage(Encoding.UTF8.GetString(data.Skip(1).Take(realDataLength - 1).ToArray()));
case DeliveryReceipt.TYPE_CODE:
/* Delivery receipt */
if (realDataLength < MessageId.MESSAGE_ID_LEN + 2 || ((realDataLength - 2) % MessageId.MESSAGE_ID_LEN) != 0)
{
throw new BadMessageException();
}
DeliveryReceipt.Type receiptType = (DeliveryReceipt.Type)Enum.Parse(typeof(DeliveryReceipt.Type), Convert.ToString((int)data[1] & 0xFF));
if (receiptType == null)
{
throw new BadMessageException();
}
IEnumerable<MessageId> messageIds = new LinkedList<MessageId>();
int numMsgIds = ((realDataLength - 2) / MessageId.MESSAGE_ID_LEN);
for (int i = 0; i < numMsgIds; i++)
{
messageIds.ToList().Add(new MessageId(data, 2 + i*MessageId.MESSAGE_ID_LEN));
}
return new DeliveryReceipt(receiptType, messageIds.ToList());
case ImageMessage.TYPE_CODE:
if(realDataLength != (1 + ThreemaMessage.BLOB_ID_LEN + 4 + ThreemaMessage.NONCEBYTES))
{
throw new BadMessageException();
}
byte[] blobId = new byte[ThreemaMessage.BLOB_ID_LEN];
data.Skip(1).Take(ThreemaMessage.BLOB_ID_LEN).ToArray().CopyTo(blobId, 0);
int size = ReadSwappedInteger(data, 1 + ThreemaMessage.BLOB_ID_LEN);
byte[] fileNonce = new byte[ThreemaMessage.NONCEBYTES];
data.Skip(1 + ThreemaMessage.BLOB_ID_LEN + 4).Take(ThreemaMessage.NONCEBYTES).ToArray().CopyTo(fileNonce, 0);
return new ImageMessage(blobId, size, fileNonce);
case FileMessage.TYPE_CODE:
ASCIIEncoding encoding = new ASCIIEncoding();
return FileMessage.FromString(encoding.GetString(data.Skip(1).Take(realDataLength - 1).ToArray()));
default:
throw new UnsupportedMessageTypeException();
}
}
private static int ReadSwappedInteger(byte[] data, int offset)
{
return ((data[offset + 0] & 255) << 0) + ((data[offset + 1] & 255) << 8) + ((data[offset + 2] & 255) << 16) + ((data[offset + 3] & 255) << 24);
}
/// <summary>
/// Generate a new key pair.
/// </summary>
/// <param name="privateKey">is used to return the generated private key (length must be SealedPublicKeyBox.RecipientSecretKeyBytes)</param>
/// <param name="publicKey">is used to return the generated public key (length must be SealedPublicKeyBox.RecipientPublicKeyBytes)</param>
public static void GenerateKeyPair(ref byte[] privateKey, ref byte[] publicKey)
{
if (publicKey.Length != Sodium.SealedPublicKeyBox.RecipientPublicKeyBytes || privateKey.Length != Sodium.SealedPublicKeyBox.RecipientSecretKeyBytes)
{
throw new ArgumentException("Wrong key length");
}
Sodium.KeyPair keyPair = Sodium.PublicKeyBox.GenerateKeyPair();
privateKey = keyPair.PrivateKey;
publicKey = keyPair.PublicKey;
}
/// <summary>
/// Encrypt data using NaCl asymmetric ("box") encryption.
/// </summary>
/// <param name="data">the data to be encrypted</param>
/// <param name="privateKey">is used to return the generated private key (length must be SealedPublicKeyBox.RecipientSecretKeyBytes)</param>
/// <param name="publicKey">is used to return the generated public key (length must be SealedPublicKeyBox.RecipientPublicKeyBytes)</param>
/// <returns></returns>
public static EncryptResult Encrypt(byte[] data,byte[] privateKey, byte[] publicKey)
{
if (publicKey.Length != Sodium.SealedPublicKeyBox.RecipientPublicKeyBytes || privateKey.Length != Sodium.SealedPublicKeyBox.RecipientSecretKeyBytes)
{
throw new ArgumentException("Wrong key length");
}
byte[] nonce = RandomNonce();
byte[] box = Sodium.PublicKeyBox.Create(data, nonce, privateKey, publicKey);
return new EncryptResult(box, null, nonce);
}
/// <summary>
/// Encrypt file data using NaCl symmetric encryption with a random key.
/// </summary>
/// <param name="data">the file contents to be encrypted</param>
/// <returns>the encryption result including the random key</returns>
public static EncryptResult EncryptFileData(byte[] data)
{
//create random key
Random rnd = new Random();
byte[] encryptionKey = new byte[CryptTool.SYMMKEYBYTES];
rnd.NextBytes(encryptionKey);
//encrypt file data in-place
data = Sodium.SecretBox.Create(data, FILE_NONCE, encryptionKey);
//skip first temp 16 bytes for encryption
return new EncryptResult(data.Skip(16).ToArray(), encryptionKey, FILE_NONCE);
}
/// <summary>
/// Encrypt file thumbnail data using NaCl symmetric encryption with a random key.
/// </summary>
/// <param name="data">data the file contents to be encrypted</param>
/// <param name="encryptionKey"></param>
/// <returns>the encryption result including the random key</returns>
public static EncryptResult encryptFileThumbnailData(byte[] data, byte[] encryptionKey)
{
// encrypt file data in-place
data = Sodium.SecretBox.Create(data, FILE_THUMBNAIL_NONCE, encryptionKey);
return new EncryptResult(data, encryptionKey, FILE_THUMBNAIL_NONCE);
}
/// <summary>
/// Hashes an email address for identity lookup.
/// </summary>
/// <param name="email">email the email address</param>
/// <returns>the raw hash</returns>
public static byte[] HashEmail(string email)
{
try
{
ASCIIEncoding encoding = new ASCIIEncoding();
var hmac = new HMACSHA256(EMAIL_HMAC_KEY);
return hmac.ComputeHash(encoding.GetBytes(email.Trim())).ToArray();
}
catch (Exception ex)
{
Debug.WriteLine("Error in HashEmail(): {0}", ex.Message);
return null;
}
}
/// <summary>
/// Hashes a phone number for identity lookup.
/// </summary>
/// <param name="phoneNo">phoneNo the phone number</param>
/// <returns>the raw hash</returns>
public static byte[] HashPhoneNo(string phoneNo)
{
try
{
ASCIIEncoding encoding = new ASCIIEncoding();
var hmac = new HMACSHA256(PHONENO_HMAC_KEY);
return hmac.ComputeHash(encoding.GetBytes(Regex.Replace(phoneNo, "[^0-9]", "")));
}
catch (Exception ex)
{
Debug.WriteLine("Error in HashPhoneNo(): {0}", ex.Message);
return null;
}
}
/// <summary>
/// Generate a random nonce.
/// </summary>
/// <returns>random nonce</returns>
public static byte[] RandomNonce()
{
byte[] nonce = new byte[ThreemaMessage.NONCEBYTES];
new Random().NextBytes(nonce);
return nonce;
}
/// <summary>
/// Return the public key that corresponds with a given private key.
/// </summary>
/// <param name="privateKey">The private key whose public key should be derived</param>
/// <returns>The corresponding public key.</returns>
public static byte[] DerivePublicKey(byte[] privateKey)
{
Sodium.KeyPair keyPair = Sodium.PublicKeyBox.GenerateKeyPair(privateKey);
return keyPair.PublicKey;
}
}
}
@@ -0,0 +1,211 @@
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IcgSoftware.Threema.CoreMsgApi
{
public static class DataUtils
{
private const int BUFFER_SIZE = 16384;
/// <summary>
/// Convert a byte array into a hexadecimal string (lowercase).
/// </summary>
/// <param name="bytes">the bytes to encode</param>
/// <returns>hex encoded string</returns>
public static string ByteArrayToHexString(byte[] bytes)
{
var hex = BitConverter.ToString(bytes);
return hex.Replace("-", "");
}
/// <summary>
/// Convert a string in hexadecimal representation to a byte array.
/// </summary>
/// <param name="s">hex string</param>
/// <returns>decoded byte array</returns>
public static byte[] HexStringToByteArray(string s)
{
string sc = s.Replace("[^0-9a-fA-F]", "");
int len = sc.Length;
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2)
{
System.Diagnostics.Debug.WriteLine(sc.Substring(i, 2));
data[i / 2] = Convert.ToByte(sc.Substring(i, 2), 16);
}
return data;
}
/// <summary>
/// UTF8 encoded string.
/// </summary>
/// <param name="value">string to encode</param>
/// <returns>encoded string</returns>
public static string Utf8Endcode(string value)
{
return Encoding.UTF8.GetString(Encoding.Default.GetBytes(value));
}
/// <summary>
/// Read hexadecimal data from a file and return it as a byte array.
/// </summary>
/// <param name="file">input file</param>
/// <returns>the decoded data</returns>
public static byte[] ReadHexFile(string file)
{
byte[] data = null;
using (FileStream stream = File.OpenRead(file))
using (StreamReader reader = new StreamReader(stream))
{
data = HexStringToByteArray(reader.ReadLine().Trim());
reader.Close();
}
return data;
}
/// <summary>
/// Read an encoded key from a file and return it as a key instance.
/// </summary>
/// <param name="file">input file</param>
/// <returns>the decoded key</returns>
public static Key ReadKeyFile(string file)
{
return Key.DecodeKey(ReadLineFromFile(file));
}
/// <summary>
/// Read an encoded key from a file and return it as a key instance.
/// </summary>
/// <param name="file">input file</param>
/// <param name="expectedKeyType">validates the key type (private or public)</param>
/// <returns>the decoded key</returns>
public static Key ReadKeyFile(string file, string expectedKeyType)
{
return Key.DecodeKey(ReadLineFromFile(file), expectedKeyType);
}
/// <summary>
/// Wirte stream data to byte array.
/// </summary>
/// <param name="stream">data write to byte array</param>
/// <param name="progressListener">progress</param>
/// <returns>bytes from stream</returns>
public static byte[] StreamToBytes(Stream stream, IProgressListener progressListener)
{
if (stream == null)
{
throw new ArgumentNullException("stream must not be null.");
}
byte[] bytes;
// Content length known?
if (stream.CanSeek)
{
bytes = new byte[stream.Length];
int bytesRead = 0;
int offset = 0;
//reader.Read
while (offset < stream.Length && (bytesRead = stream.Read(bytes, offset, (int)(bytes.Length - offset))) > 0)
{
offset += bytesRead;
if (progressListener != null)
{
progressListener.updateProgress((int)(100 * offset / bytes.Length));
}
}
if (offset != (int)bytes.Length)
{
throw new IOException("Unexpected read size. current: " + offset + ", excepted: " + bytes.Length);
}
}
else
{
// Content length is unknown - need to read until EOF
byte[] buffer = new byte[BUFFER_SIZE];
using (MemoryStream outputStream = new MemoryStream())
{
try
{
//int offset = 0;
while (true)
{
int bytesRead = stream.Read(buffer, 0, buffer.Length);
if (bytesRead == 0)
{
break;
}
outputStream.Write(buffer, 0, bytesRead);
}
}
catch (ArgumentOutOfRangeException)
{
}
outputStream.Position = 0;
bytes = new byte[outputStream.Length];
outputStream.Read(bytes, 0, bytes.Length);
}
}
return bytes;
}
/// <summary>
/// Write a byte array into a file in hexadecimal format.
/// </summary>
/// <param name="file">output file</param>
/// <param name="data">the data to be written</param>
public static void WriteHexFile(string file, byte[] data)
{
using (FileStream stream = File.OpenWrite(file))
using (StreamWriter writer = new StreamWriter(stream))
{
writer.Write(ByteArrayToHexString(data));
writer.Write('\n');
writer.Close();
}
}
/// <summary>
/// Write an encoded key to a file
/// Encoded key format: type:hex_key.
/// </summary>
/// <param name="file">output file</param>
/// <param name="key">a key that will be encoded and written to a file</param>
public static void WriteKeyFile(string file, Key key)
{
using (FileStream stream = File.OpenWrite(file))
using (StreamWriter writer = new StreamWriter(stream))
{
writer.Write(key.Encode());
writer.Write('\n');
}
}
private static string ReadLineFromFile(string file)
{
string data = null;
using (FileStream stream = File.OpenRead(file))
using (StreamReader reader = new StreamReader(stream))
{
data = reader.ReadLine().Trim();
reader.Close();
}
return data;
}
}
}
@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IcgSoftware.Threema.CoreMsgApi.Exceptions
{
public class BadMessageException : Exception
{
}
}
@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IcgSoftware.Threema.CoreMsgApi.Exceptions
{
public class CoreMigrationException : Exception
{
public CoreMigrationException(string message) :
base(message)
{
}
}
}
@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IcgSoftware.Threema.CoreMsgApi.Exceptions
{
class DecryptionFailedException : Exception
{
}
}
@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IcgSoftware.Threema.CoreMsgApi.Exceptions
{
public class InvalidKeyException : Exception
{
public InvalidKeyException(string message) :
base(message)
{
}
public InvalidKeyException(string message, Exception innerException) :
base(message, innerException)
{
}
}
}
@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IcgSoftware.Threema.CoreMsgApi.Exceptions
{
public class MessageParseException : Exception
{
}
}
@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IcgSoftware.Threema.CoreMsgApi.Exceptions
{
public class NotAllowedException : Exception
{
}
}
@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IcgSoftware.Threema.CoreMsgApi.Exceptions
{
class UnsupportedMessageTypeException : Exception
{
}
}
@@ -0,0 +1,319 @@
#if CoreWinOnly
using Microsoft.Win32;
#endif
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using IcgSoftware.Threema.CoreMsgApi.Exceptions;
using IcgSoftware.Threema.CoreMsgApi.Messages;
using IcgSoftware.Threema.CoreMsgApi.Results;
namespace IcgSoftware.Threema.CoreMsgApi.Helpers
{
/// <summary>
/// Helper to handle Threema end-to-end encryption.
/// </summary>
public class E2EHelper
{
private readonly APIConnector apiConnector;
private readonly byte[] privateKey;
public E2EHelper(APIConnector apiConnector, byte[] privateKey)
{
this.apiConnector = apiConnector;
this.privateKey = privateKey;
}
/// <summary>
/// Decrypt a Message and download the blobs of the Message (e.g. image or file)
/// </summary>
/// <param name="threemaId">Threema ID of the sender</param>
/// <param name="messageId">Message ID</param>
/// <param name="box">Encrypted box data of the file/image message</param>
/// <param name="nonce">Nonce that was used for message encryption</param>
/// <param name="outputFolder">Output folder for storing decrypted images/files</param>
/// <returns>Result of message reception</returns>
public ReceiveMessageResult ReceiveMessage(string threemaId, string messageId, byte[] box, byte[] nonce, string outputFolder)
{
//fetch public key
byte[] publicKey = this.apiConnector.LookupKey(threemaId);
if(publicKey == null)
{
throw new InvalidKeyException("invalid threema id");
}
ThreemaMessage message = CryptTool.DecryptMessage(box, this.privateKey, publicKey, nonce);
if(message == null)
{
return null;
}
ReceiveMessageResult result = new ReceiveMessageResult(messageId, message);
if (message.GetType() == typeof(ImageMessage))
{
//download image
ImageMessage imageMessage = (ImageMessage)message;
byte[] fileData = this.apiConnector.DownloadFile(imageMessage.BlobId);
if(fileData == null)
{
throw new MessageParseException();
}
byte[] decryptedFileContent = CryptTool.Decrypt(fileData, privateKey, publicKey, imageMessage.Nonce);
FileInfo imageFile = new FileInfo(outputFolder + "/" + messageId + ".jpg");
using (FileStream stream = File.OpenWrite(imageFile.FullName))
{
stream.Write(decryptedFileContent, 0, decryptedFileContent.Length);
stream.Close();
}
result.Files.Add(imageFile);
}
else if (message.GetType() == typeof(FileMessage))
{
//download file
FileMessage fileMessage = (FileMessage)message;
byte[] fileData = this.apiConnector.DownloadFile(fileMessage.BlobId);
byte[] decryptedFileData = CryptTool.DecryptFileData(fileData, fileMessage.EncryptionKey);
FileInfo file = new FileInfo(outputFolder + "/" + messageId + "-" + fileMessage.FileName);
using (FileStream stream = File.OpenWrite(file.FullName))
{
stream.Write(decryptedFileData, 0, decryptedFileData.Length);
stream.Close();
}
result.Files.Add(file);
if(fileMessage.ThumbnailBlobId != null)
{
byte[] thumbnailData = this.apiConnector.DownloadFile(fileMessage.ThumbnailBlobId);
byte[] decryptedThumbnailData = CryptTool.DecryptFileThumbnailData(thumbnailData, fileMessage.EncryptionKey);
FileInfo thumbnailFile = new FileInfo(outputFolder + "/" + messageId + "-thumbnail.jpg");
using (FileStream stream = File.OpenWrite(thumbnailFile.FullName))
{
stream.Write(decryptedThumbnailData, 0, decryptedThumbnailData.Length);
stream.Close();
}
result.Files.Add(thumbnailFile);
}
}
return result;
}
/// <summary>
/// Encrypt a file message and send it to the given recipient.
/// The thumbnailMessagePath can be null.
/// </summary>
/// <param name="threemaId">target Threema ID</param>
/// <param name="fileMessageFile">the file to be sent</param>
/// <param name="thumbnailMessageFile">file for thumbnail; if not set, no thumbnail will be sent</param>
/// <returns>generated message ID</returns>
public string SendFileMessage(string threemaId, FileInfo fileMessageFile, FileInfo thumbnailMessageFile)
{
//fetch public key
byte[] publicKey = this.apiConnector.LookupKey(threemaId);
if (publicKey == null)
{
throw new InvalidKeyException("invalid threema id");
}
//check capability of a key
CapabilityResult capabilityResult = this.apiConnector.LookupKeyCapability(threemaId);
if (capabilityResult == null || !capabilityResult.CanImage)
{
throw new NotAllowedException();
}
if (fileMessageFile == null)
{
throw new ArgumentException("fileMessageFile must not be null.");
}
if (!fileMessageFile.Exists)
{
throw new FileNotFoundException(fileMessageFile.FullName);
}
byte[] fileData;
using (Stream stream = File.OpenRead(fileMessageFile.FullName))
{
fileData = new byte[stream.Length];
stream.Read(fileData, 0, (int)stream.Length);
stream.Close();
}
if (fileData == null)
{
throw new IOException("invalid file");
}
//encrypt the image
EncryptResult encryptResult = CryptTool.EncryptFileData(fileData);
//upload the image
UploadResult uploadResult = apiConnector.UploadFile(encryptResult);
if(!uploadResult.IsSuccess)
{
throw new IOException("could not upload file (upload response " + uploadResult.ResponseCode + ")");
}
UploadResult uploadResultThumbnail = null;
if (thumbnailMessageFile != null && thumbnailMessageFile.Exists)
{
byte[] thumbnailData;
using (Stream stream = File.OpenRead(thumbnailMessageFile.FullName))
{
thumbnailData = new byte[stream.Length];
stream.Read(thumbnailData, 0, (int)stream.Length);
stream.Close();
}
if (thumbnailData == null)
{
throw new IOException("invalid thumbnail file");
}
//encrypt the thumbnail
EncryptResult encryptResultThumbnail = CryptTool.encryptFileThumbnailData(fileData, encryptResult.Secret);
//upload the thumbnail
uploadResultThumbnail = this.apiConnector.UploadFile(encryptResultThumbnail);
}
//send it
EncryptResult fileMessage = CryptTool.EncryptFileMessage(
encryptResult,
uploadResult,
GetMIMEType(fileMessageFile),
fileMessageFile.Name,
(int) fileMessageFile.Length,
uploadResultThumbnail,
privateKey, publicKey);
return this.apiConnector.SendE2EMessage(
threemaId,
fileMessage.Nonce,
fileMessage.Result);
}
/// <summary>
/// Encrypt an image message and send it to the given recipient.
/// </summary>
/// <param name="threemaId">threemaId target Threema ID</param>
/// <param name="imageFilePath">path to read image data from</param>
/// <returns>generated message ID</returns>
public string SendImageMessage(string threemaId, string imageFilePath)
{
//fetch public key
byte[] publicKey = this.apiConnector.LookupKey(threemaId);
if (publicKey == null)
{
throw new InvalidKeyException("invalid threema id");
}
//check capability of a key
CapabilityResult capabilityResult = this.apiConnector.LookupKeyCapability(threemaId);
if (capabilityResult == null || !capabilityResult.CanImage)
{
throw new NotAllowedException();
}
byte[] fileData = File.ReadAllBytes(imageFilePath);
if (fileData == null)
{
throw new IOException("invalid file");
}
//encrypt the image
EncryptResult encryptResult = CryptTool.Encrypt(fileData, this.privateKey, publicKey);
//upload the image
UploadResult uploadResult = apiConnector.UploadFile(encryptResult);
if (!uploadResult.IsSuccess)
{
throw new IOException("could not upload file (upload response " + uploadResult.ResponseCode + ")");
}
//send it
EncryptResult imageMessage = CryptTool.EncryptImageMessage(encryptResult, uploadResult, this.privateKey, publicKey);
return apiConnector.SendE2EMessage(
threemaId,
imageMessage.Nonce,
imageMessage.Result);
}
/// <summary>
/// Encrypt a text message and send it to the given recipient.
/// </summary>
/// <param name="threemaId">target Threema ID</param>
/// <param name="text">the text to send</param>
/// <returns>generated message ID</returns>
public string SendTextMessage(string threemaId, string text)
{
//fetch public key
byte[] publicKey = this.apiConnector.LookupKey(threemaId);
if (publicKey == null)
{
throw new InvalidKeyException("invalid threema id");
}
EncryptResult res = CryptTool.EncryptTextMessage(text, this.privateKey, publicKey);
return this.apiConnector.SendE2EMessage(threemaId, res.Nonce, res.Result);
}
/// <summary>
/// Get mime type of the file extension via registry.
/// </summary>
/// <param name="file">mime type of this file</param>
/// <returns>mime type</returns>
private string GetMIMEType(FileInfo file)
{
#if CoreWinOnly
if (file == null)
{
throw new ArgumentException("file must not be null.");
}
string mimeType = "application/unknown";
//Using Microsoft.Win32.Registry. But you lose portability and can't run the application on Linux and MacOS anymore. Implement GetMIMEType in another way.
RegistryKey regKey = Registry.ClassesRoot.OpenSubKey(
file.Extension.ToLower()
);
if (regKey != null)
{
object contentType = regKey.GetValue("Content Type");
if (contentType != null)
{
mimeType = contentType.ToString();
}
}
return mimeType;
#else
throw new CoreMigrationException("Using Microsoft.Win32.Registry. But you lose portability and can't run the application on Linux and MacOS anymore. Implement GetMIMEType in another way.");
#endif
}
}
}
@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IcgSoftware.Threema.CoreMsgApi
{
public interface IProgressListener
{
/// <summary>
/// Update the progress of an upload/download process.
/// </summary>
/// <param name="progress">in percent (0..100)</param>
void updateProgress(int progress);
}
}
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DefineConstants>TRACE;DEBUG;NETCOREAPP;NETCOREAPP2_1;CoreWinOnly</DefineConstants>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.1.1" />
<PackageReference Include="Microsoft.Win32.Registry" Version="4.5.0" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="Sodium.Core" Version="1.2.0" />
</ItemGroup>
</Project>
@@ -0,0 +1,91 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using IcgSoftware.Threema.CoreMsgApi.Exceptions;
namespace IcgSoftware.Threema.CoreMsgApi
{
/// <summary>
/// Encapsulates an asymmetric key, either public or private.
/// </summary>
public class Key
{
public static readonly char separator = ':';
public static class KeyType
{
public const string PRIVATE = "private";
public const string PUBLIC = "public";
}
public byte[] key;
public string type;
public Key(string type, byte[] key)
{
this.key = key;
this.type = type;
}
/// <summary>
/// Decodes and validates an encoded key.
/// Encoded key format: type:hex_key
/// </summary>
/// <param name="encodedKey">an encoded key</param>
/// <returns></returns>
public static Key DecodeKey(string encodedKey)
{
// Split key and check length
string[] keyArray = encodedKey.Split(Key.separator);
if (keyArray.Length != 2)
{
throw new InvalidKeyException("Does not contain a valid key format");
}
// Unpack key
string keyType = keyArray[0];
string keyContent = keyArray[1];
// Is this a valid hex key?
if (!Regex.IsMatch(keyContent, "[0-9a-fA-F]{64}"))
{
throw new InvalidKeyException("Does not contain a valid key");
}
return new Key(keyType, DataUtils.HexStringToByteArray(keyContent));
}
/// <summary>
/// Decodes and validates an encoded key.
/// Encoded key format: type:hex_key
/// </summary>
/// <param name="encodedKey">an encoded key</param>
/// <param name="expectedKeyType">the expected type of the key</param>
/// <returns></returns>
public static Key DecodeKey(String encodedKey, String expectedKeyType)
{
Key key = DecodeKey(encodedKey);
// Check key type
if (!key.type.Equals(expectedKeyType))
{
throw new InvalidKeyException("Expected key type: " + expectedKeyType + ", got: " + key.type);
}
return key;
}
/// <summary>
/// Encodes a key.
/// </summary>
/// <returns>an encoded key</returns>
public String Encode()
{
return this.type + Key.separator + DataUtils.ByteArrayToHexString(this.key);
}
}
}
@@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IcgSoftware.Threema.CoreMsgApi
{
class MessageId
{
public const int MESSAGE_ID_LEN = 8;
private readonly byte[] messageId;
public MessageId(byte[] messageId)
{
if (messageId.Length != MESSAGE_ID_LEN)
{
throw new ArgumentException("Bad message ID length");
}
this.messageId = messageId;
}
public MessageId(byte[] data, int offset)
{
if ((offset + MESSAGE_ID_LEN) > data.Length)
{
throw new ArgumentException("Bad message ID buffer length");
}
this.messageId = new byte[MESSAGE_ID_LEN];
//System.arraycopy(data, offset, this.messageId, 0, MESSAGE_ID_LEN);
data.Skip(offset).Take(MESSAGE_ID_LEN).ToArray().CopyTo(this.messageId, 0);
}
public byte[] GetMessageId
{
get { return messageId; }
}
public override string ToString() {
return DataUtils.ByteArrayToHexString(messageId);
}
}
}
@@ -0,0 +1,101 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IcgSoftware.Threema.CoreMsgApi.Messages
{
class DeliveryReceipt : ThreemaMessage
{
public const int TYPE_CODE = 0x80;
public enum Type
{
RECEIVED = 1,
READ = 2,
USER_ACK = 3
}
private readonly Type receiptType;
private readonly List<MessageId> ackedMessageIds;
public DeliveryReceipt(Type receiptType, List<MessageId> ackedMessageIds) {
this.receiptType = receiptType;
this.ackedMessageIds = ackedMessageIds;
}
public Type ReceiptType
{
get { return receiptType; }
}
public List<MessageId> AckedMessageIds
{
get { return ackedMessageIds; }
}
public override int GetTypeCode() {
return TYPE_CODE;
}
public override byte[] GetData()
{
//Not implemented yet
return new byte[0];
}
public override string ToString()
{
StringBuilder sb = new StringBuilder("Delivery receipt (");
sb.Append(receiptType);
sb.Append("): ");
int i = 0;
ackedMessageIds.ForEach(messageId =>
{
if (i != 0)
{
sb.Append(", ");
}
sb.Append(messageId);
i++;
});
return sb.ToString();
}
/**
* A delivery receipt type. The following types are defined:
*
* <ul>
* <li>RECEIVED: the message has been received and decrypted on the recipient's device</li>
* <li>READ: the message has been shown to the user in the chat view
* (note that this status can be disabled)</li>
* <li>USER_ACK: the user has explicitly acknowledged the message (usually by
* long-pressing it and choosing the "acknowledge" option)</li>
* </ul>
*/
/*
public enum Type {
RECEIVED(1), READ(2), USER_ACK(3);
private final int code;
Type(int code) {
this.code = code;
}
public int getCode() {
return code;
}
public static Type get(int code) {
for (Type t : values()) {
if (t.code == code)
return t;
}
return null;
}
}
*/
}
}
@@ -0,0 +1,147 @@
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using IcgSoftware.Threema.CoreMsgApi.Exceptions;
namespace IcgSoftware.Threema.CoreMsgApi.Messages
{
class FileMessage : ThreemaMessage
{
public const int TYPE_CODE = 0x17;
private const string KEY_BLOB_ID = "b";
private const string KEY_THUMBNAIL_BLOB_ID = "t";
private const string KEY_ENCRYPTION_KEY = "k";
private const string KEY_MIME_TYPE = "m";
private const string KEY_FILE_NAME = "n";
private const string KEY_FILE_SIZE = "s";
private const string KEY_TYPE = "i";
private readonly byte[] blobId;
private readonly byte[] encryptionKey;
private readonly string mimeType;
private readonly string fileName;
private readonly int fileSize;
private readonly byte[] thumbnailBlobId;
public FileMessage(byte[] blobId, byte[] encryptionKey, String mimeType, String fileName, int fileSize, byte[] thumbnailBlobId)
{
this.blobId = blobId;
this.encryptionKey = encryptionKey;
this.mimeType = mimeType;
this.fileName = fileName;
this.fileSize = fileSize;
this.thumbnailBlobId = thumbnailBlobId;
}
public byte[] BlobId
{
get { return this.blobId; }
}
public byte[] EncryptionKey
{
get { return this.encryptionKey; }
}
public string MimeType
{
get { return this.mimeType; }
}
public string FileName
{
get { return this.fileName; }
}
public int FileSize
{
get { return this.fileSize; }
}
public byte[] ThumbnailBlobId
{
get { return this.thumbnailBlobId; }
}
public override int GetTypeCode()
{
return TYPE_CODE;
}
public override string ToString()
{
return string.Format("file message {0}", this.fileName);
}
public override byte[] GetData()
{
JObject jo = new JObject();
try
{
jo.Add(KEY_BLOB_ID, JToken.FromObject(DataUtils.ByteArrayToHexString(this.blobId)));
if (this.thumbnailBlobId != null)
{
jo.Add(KEY_THUMBNAIL_BLOB_ID, JToken.FromObject(DataUtils.ByteArrayToHexString(this.thumbnailBlobId)));
}
jo.Add(KEY_ENCRYPTION_KEY, JToken.FromObject(DataUtils.ByteArrayToHexString(this.encryptionKey)));
jo.Add(KEY_MIME_TYPE, JToken.FromObject(this.mimeType));
jo.Add(KEY_FILE_NAME, JToken.FromObject(this.fileName));
jo.Add(KEY_FILE_SIZE, JToken.FromObject(this.fileSize));
jo.Add(KEY_TYPE, JToken.FromObject(0));
}
catch (Exception)
{
throw new BadMessageException();
}
return Encoding.UTF8.GetBytes(jo.ToString());
}
public static FileMessage FromString(string json)
{
try
{
JObject jo = JObject.Parse(json);
byte[] encryptionKey = DataUtils.HexStringToByteArray(jo[KEY_ENCRYPTION_KEY].Value<string>());
string mimeType = jo[KEY_MIME_TYPE].Value<string>();
int fileSize = jo[KEY_FILE_SIZE].Value<int>();
byte[] blobId = DataUtils.HexStringToByteArray(jo[KEY_BLOB_ID].Value<string>());
string fileName;
byte[] thumbnailBlobId = null;
//optional field
if (jo.Children().Any(e => e.Path.EndsWith(KEY_THUMBNAIL_BLOB_ID)))
{
thumbnailBlobId = DataUtils.HexStringToByteArray(jo[KEY_THUMBNAIL_BLOB_ID].Value<string>());
}
if (jo.Children().Any(e => e.Path.EndsWith(KEY_FILE_NAME)))
{
fileName = jo[KEY_FILE_NAME].Value<string>();
}
else
{
fileName = "unnamed";
}
return new FileMessage(
blobId,
encryptionKey,
mimeType,
fileName,
fileSize,
thumbnailBlobId
);
}
catch
{
throw new BadMessageException();
}
}
}
}
@@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IcgSoftware.Threema.CoreMsgApi.Messages
{
class ImageMessage : ThreemaMessage
{
public const int TYPE_CODE = 0x02;
private readonly byte[] blobId;
private readonly int size;
private readonly byte[] nonce;
public ImageMessage(byte[] blobId, int size, byte[] nonce)
{
this.blobId = blobId;
this.size = size;
this.nonce = nonce;
}
public byte[] BlobId
{
get { return this.blobId; }
}
public int Size
{
get { return this.size; }
}
public byte[] Nonce
{
get { return this.nonce; }
}
public override int GetTypeCode()
{
return TYPE_CODE;
}
public override byte[] GetData()
{
byte[] data = new byte[BLOB_ID_LEN + 4 + ThreemaMessage.NONCEBYTES];
int pos = 0;
//System.arraycopy(this.blobId, 0, data, pos, BLOB_ID_LEN);
this.blobId.CopyTo(data, 0);
pos += BLOB_ID_LEN;
//EndianUtils.writeSwappedInteger(data, pos, this.size);
byte[] size = BitConverter.GetBytes(this.size);
size.CopyTo(data, pos);
pos += 4;
//System.arraycopy(this.nonce, 0, data, pos, ThreemaMessage.NONCEBYTES);
this.nonce.CopyTo(data, pos);
return data;
}
public override string ToString()
{
return string.Format("blob {0}", DataUtils.ByteArrayToHexString(this.blobId));
}
}
}
@@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IcgSoftware.Threema.CoreMsgApi.Messages
{
/// <summary>
/// A text message that can be sent/received with end-to-end encryption via Threema.
/// </summary>
public class TextMessage : ThreemaMessage
{
public const int TYPE_CODE = 0x01;
private readonly string text;
public TextMessage(String text)
{
this.text = text;
}
public string Text { get { return text; } }
public override int GetTypeCode() {
return TYPE_CODE;
}
public override byte[] GetData()
{
return Encoding.UTF8.GetBytes(text);
}
public override string ToString()
{
return text;
}
}
}
@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IcgSoftware.Threema.CoreMsgApi.Messages
{
/// <summary>
/// Abstract base class of messages that can be sent with end-to-end encryption via Threema.
/// </summary>
public abstract class ThreemaMessage
{
public const int NONCEBYTES = 24;
public const int BLOB_ID_LEN = 16;
/// <summary>
/// Get message's raw content
/// </summary>
/// <returns></returns>
public abstract byte[] GetData();
/// <summary>
/// Get message's type code
/// </summary>
/// <returns></returns>
public abstract int GetTypeCode();
}
}
@@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IcgSoftware.Threema.CoreMsgApi
{
/// <summary>
/// Stores and caches public keys for Threema users. Extend this class to provide your
/// own storage implementation, e.g. in a file or database.
/// </summary>
public abstract class PublicKeyStore
{
private readonly static object lockCache = new object();
private readonly Dictionary<string, byte[]> cache = new Dictionary<string, byte[]>();
/// <summary>
/// Get the public key for a given Threema ID. The cache is checked first; if it
/// is not found in the cache, fetchPublicKey() is called.
/// </summary>
/// <param name="threemaId">The Threema ID whose public key should be obtained</param>
/// <returns>The public key, or null if not found</returns>
public byte[] GetPublicKey(string threemaId)
{
lock (lockCache)
{
byte[] pk = null;
if (this.cache.Keys.Contains(threemaId))
{
pk = this.cache[threemaId];
}
else
{
pk = this.FetchPublicKey(threemaId);
if (pk != null)
{
this.cache.Add(threemaId, pk);
}
}
return pk;
}
}
/// <summary>
/// Store the public key for a given Threema ID in the cache, and the underlying store.
/// </summary>
/// <param name="threemaId">The Threema ID whose public key should be stored</param>
/// <param name="publicKey">The corresponding public key</param>
public void SetPublicKey(string threemaId, byte[] publicKey)
{
if(publicKey != null)
{
lock (lockCache)
{
this.cache.Add(threemaId, publicKey);
this.Save(threemaId, publicKey);
}
}
}
/// <summary>
/// Fetch the public key for the given Threema ID from the store. Override to provide
/// your own implementation to read from the store.
/// </summary>
/// <param name="threemaId">The Threema ID whose public key should be obtained</param>
/// <returns>The public key, or null if not found</returns>
abstract protected byte[] FetchPublicKey(string threemaId);
/// <summary>
/// Save the public key for a given Threema ID in the store. Override to provide
/// your own implementation to write to the store.
/// </summary>
/// <param name="threemaId">The Threema ID whose public key should be stored</param>
/// <param name="publicKey">The corresponding public key</param>
abstract protected void Save(string threemaId, byte[] publicKey);
}
}
@@ -0,0 +1,176 @@
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Data.Common;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using IcgSoftware.Threema.CoreMsgApi.Exceptions;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Configuration;
namespace IcgSoftware.Threema.CoreMsgApi
{
class PublicKeyStoreDb : PublicKeyStore
{
private readonly string connectionString;
public PublicKeyStoreDb(string connectionString)
{
if (string.IsNullOrEmpty(connectionString))
{
throw new ArgumentException("connectionString must be not null or empty.");
}
this.connectionString = connectionString;
this.CreateDatabase();
}
/// <summary>
/// Fetch public key in store for particular threema id
/// </summary>
/// <param name="threemaId">Threema id to fetch</param>
/// <returns>Public key</returns>
protected override byte[] FetchPublicKey(string threemaId)
{
using (DbConnection connection = GetConnection(this.connectionString))
{
connection.Open();
string sql = "SELECT threema_id, key FROM public_key WHERE threema_id = @threemaId";
var command = connection.CreateCommand();
command.CommandType = CommandType.Text;
command.CommandText = sql;
var paramThreemaId = command.CreateParameter();
paramThreemaId.ParameterName = "threemaId";
paramThreemaId.Value = threemaId;
command.Parameters.Add(paramThreemaId);
byte[] publicKey = null;
using (var reader = command.ExecuteReader(CommandBehavior.SingleRow))
{
if (reader.HasRows)
{
if (reader.Read())
{
publicKey = DataUtils.HexStringToByteArray(reader[1].ToString());
}
}
reader.Close();
}
return publicKey;
}
}
/// <summary>
/// Save threema id and public key into store
/// </summary>
/// <param name="threemaId">Threema id</param>
/// <param name="publicKey">public key</param>
protected override void Save(string threemaId, byte[] publicKey)
{
using (DbConnection connection = GetConnection(this.connectionString))
{
connection.Open();
string sql = "INSERT INTO public_key (threema_id, key) VALUES (@threemaId, @key)";
var command = connection.CreateCommand();
command.CommandType = CommandType.Text;
command.CommandText = sql;
var paramThreemaId = command.CreateParameter();
paramThreemaId.ParameterName = "threemaId";
paramThreemaId.Value = threemaId;
var paramKey = command.CreateParameter();
paramKey.ParameterName = "key";
paramKey.Value = DataUtils.ByteArrayToHexString(publicKey);
command.Parameters.Add(paramThreemaId);
command.Parameters.Add(paramKey);
command.ExecuteNonQuery();
connection.Close();
}
}
/// <summary>
/// Create database and table for public key store
/// </summary>
private void CreateDatabase()
{
using (DbConnection connection = GetConnection(this.connectionString))
{
connection.Open();
string sql = "CREATE TABLE IF NOT EXISTS public_key (id INTEGER PRIMARY KEY AUTOINCREMENT, threema_id VARCHAR(8) NOT NULL UNIQUE, key VARCHAR(64) NOT NULL)";
var command = connection.CreateCommand();
command.CommandText = sql;
command.ExecuteNonQuery();
connection.Close();
}
}
/// <summary>
/// Get connetion is configured in App.config
/// </summary>
/// <param name="connectionString">Connect String</param>
/// <returns>Db Connection</returns>
private DbConnection GetConnection(string connectionString)
{
//throw new CoreMigrationException("SQLite is not supported");
return new SqliteConnection(connectionString);
//string providerName = "System.Data.SQLite";
//var dbProvider = Microsoft.Data.Sqlite.SqliteFactory.Instance;
/*
string providerName = null;
DbConnectionStringBuilder connectionStringBuilder = new DbConnectionStringBuilder { ConnectionString = connectionString };
if (connectionStringBuilder.ContainsKey("provider"))
{
providerName = connectionStringBuilder["provider"].ToString();
}
else
{
ConnectionStringSettings connectionStringSetting = ConfigurationManager
.ConnectionStrings
.Cast<ConnectionStringSettings>()
.FirstOrDefault(x => x.ConnectionString == connectionString);
if (connectionStringSetting != null)
{
providerName = connectionStringSetting.ProviderName;
}
}
if (providerName != null)
{
bool providerExists = DbProviderFactories
.GetFactoryClasses()
.Rows.Cast<DataRow>()
.Any(r => r[2].Equals(providerName));
if (providerExists)
{
DbProviderFactory factory = DbProviderFactories.GetFactory(providerName);
DbConnection dbConnection = factory.CreateConnection();
dbConnection.ConnectionString = connectionString;
return dbConnection;
}
}
return null;
*/
}
}
}
@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IcgSoftware.Threema.CoreMsgApi
{
public class PublicKeyStoreNone : PublicKeyStore
{
protected override byte[] FetchPublicKey(string threemaId)
{
return null;
}
protected override void Save(string threemaId, byte[] publicKey)
{
//do nothing
}
}
}
@@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IcgSoftware.Threema.CoreMsgApi.Results
{
/// <summary>
/// Result of a capability lookup
/// </summary>
public class CapabilityResult
{
private readonly string key;
private readonly string[] capabilities;
public CapabilityResult(string key, string[] capabilities)
{
this.key = key;
this.capabilities = capabilities;
}
public string Key
{
get { return key; }
}
/// <summary>
/// Get all capabilities as a string array.
/// </summary>
public string[] Capabilities
{
get { return capabilities; }
}
/// <summary>
/// Check whether the Threema ID can receive text
/// </summary>
public bool CanText
{
get { return this.Can("text"); }
}
/// <summary>
/// Check whether the Threema ID can receive images
/// </summary>
public bool CanImage
{
get { return this.Can("image"); }
}
/// <summary>
/// Check whether the Threema ID can receive videos
/// </summary>
public bool CanVideo
{
get { return this.Can("video"); }
}
/// <summary>
/// Check whether the Threema ID can receive audio
/// </summary>
public bool CanAudio
{
get { return this.Can("audio"); }
}
/// <summary>
/// Check whether the Threema ID can receive files
/// </summary>
public bool CanFile
{
get { return this.Can("file"); }
}
public override string ToString()
{
StringBuilder b = new StringBuilder();
b.Append(this.key).Append(": ");
for (int n = 0; n < this.capabilities.Length; n++) {
if (n > 0)
{
b.Append(",");
}
b.Append(this.capabilities[n]);
}
return b.ToString();
}
private bool Can(string key)
{
return this.capabilities.Any(k => k.Equals(key));
}
}
}
@@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IcgSoftware.Threema.CoreMsgApi.Results
{
public class EncryptResult
{
private readonly byte[] result;
private readonly byte[] secret;
private readonly byte[] nonce;
public EncryptResult(byte[] result, byte[] secret, byte[] nonce)
{
this.result = result;
this.secret = secret;
this.nonce = nonce;
}
/// <summary>
/// the encrypted data
/// </summary>
public byte[] Result
{
get { return this.result; }
}
/// <summary>
/// the size (in bytes) of the encrypted data
/// </summary>
public int Size
{
get { return this.result.Length; }
}
/// <summary>
/// the nonce that was used for encryption
/// </summary>
public byte[] Nonce
{
get { return this.nonce; }
}
/// <summary>
/// the secret that was used for encryption (only for symmetric encryption, e.g. files)
/// </summary>
public byte[] Secret
{
get { return secret; }
}
}
}
@@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using IcgSoftware.Threema.CoreMsgApi.Messages;
namespace IcgSoftware.Threema.CoreMsgApi.Results
{
public class ReceiveMessageResult
{
private readonly string messageId;
private readonly ThreemaMessage message;
protected List<FileInfo> files = new List<FileInfo>();
protected List<string> errors = new List<string>();
public ReceiveMessageResult(string messageId, ThreemaMessage message)
{
this.messageId = messageId;
this.message = message;
}
public List<FileInfo> Files
{
get { return this.files; }
}
public List<string> Errors
{
get { return this.errors; }
}
public ThreemaMessage Message
{
get { return this.message; }
}
public string MessageId
{
get { return messageId; }
}
}
}
@@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace IcgSoftware.Threema.CoreMsgApi.Results
{
public class UploadResult
{
private readonly int responseCode;
private readonly byte[] blobId;
public UploadResult(int responseCode, byte[] blobId)
{
this.responseCode = responseCode;
this.blobId = blobId;
}
/// <summary>
/// the blob ID that has been created
/// </summary>
public byte[] BlobId
{
get { return this.blobId; }
}
/// <summary>
/// whether the upload succeeded
/// </summary>
public bool IsSuccess
{
get { return this.responseCode == 200; }
}
/// <summary>
/// the response code of the upload
/// </summary>
public int ResponseCode
{
get { return this.responseCode; }
}
}
}
@@ -0,0 +1,79 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27428.2043
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IcgSoftware.Threema.CoreMsgApi", "IcgSoftware.Threema.CoreMsgApi\IcgSoftware.Threema.CoreMsgApi.csproj", "{B5799291-7E0A-4B42-AAC8-378535E1E579}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IcgSoftware.Threema.CoreMsgApi.Test", "IcgSoftware.Threema.CoreMsgApi.Test\IcgSoftware.Threema.CoreMsgApi.Test.csproj", "{05B5EB0A-5D3B-4CAD-9C68-3292A0F17FBB}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IcgSoftware.Threema.CoreMsgApi.Console", "IcgSoftware.Threema.CoreMsgApi.Console\IcgSoftware.Threema.CoreMsgApi.Console.csproj", "{909129FB-810C-4B5D-AD92-774525BD3D77}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IcgSoftware.Threema.CoreWebApp", "IcgSoftware.Threema.CoreWebApp\IcgSoftware.Threema.CoreWebApp.csproj", "{68345BDF-E4BA-4F41-8424-F02F613529FB}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{B5799291-7E0A-4B42-AAC8-378535E1E579}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B5799291-7E0A-4B42-AAC8-378535E1E579}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B5799291-7E0A-4B42-AAC8-378535E1E579}.Debug|x64.ActiveCfg = Debug|Any CPU
{B5799291-7E0A-4B42-AAC8-378535E1E579}.Debug|x64.Build.0 = Debug|Any CPU
{B5799291-7E0A-4B42-AAC8-378535E1E579}.Debug|x86.ActiveCfg = Debug|Any CPU
{B5799291-7E0A-4B42-AAC8-378535E1E579}.Debug|x86.Build.0 = Debug|Any CPU
{B5799291-7E0A-4B42-AAC8-378535E1E579}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B5799291-7E0A-4B42-AAC8-378535E1E579}.Release|Any CPU.Build.0 = Release|Any CPU
{B5799291-7E0A-4B42-AAC8-378535E1E579}.Release|x64.ActiveCfg = Release|Any CPU
{B5799291-7E0A-4B42-AAC8-378535E1E579}.Release|x64.Build.0 = Release|Any CPU
{B5799291-7E0A-4B42-AAC8-378535E1E579}.Release|x86.ActiveCfg = Release|Any CPU
{B5799291-7E0A-4B42-AAC8-378535E1E579}.Release|x86.Build.0 = Release|Any CPU
{05B5EB0A-5D3B-4CAD-9C68-3292A0F17FBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{05B5EB0A-5D3B-4CAD-9C68-3292A0F17FBB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{05B5EB0A-5D3B-4CAD-9C68-3292A0F17FBB}.Debug|x64.ActiveCfg = Debug|Any CPU
{05B5EB0A-5D3B-4CAD-9C68-3292A0F17FBB}.Debug|x64.Build.0 = Debug|Any CPU
{05B5EB0A-5D3B-4CAD-9C68-3292A0F17FBB}.Debug|x86.ActiveCfg = Debug|Any CPU
{05B5EB0A-5D3B-4CAD-9C68-3292A0F17FBB}.Debug|x86.Build.0 = Debug|Any CPU
{05B5EB0A-5D3B-4CAD-9C68-3292A0F17FBB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{05B5EB0A-5D3B-4CAD-9C68-3292A0F17FBB}.Release|Any CPU.Build.0 = Release|Any CPU
{05B5EB0A-5D3B-4CAD-9C68-3292A0F17FBB}.Release|x64.ActiveCfg = Release|Any CPU
{05B5EB0A-5D3B-4CAD-9C68-3292A0F17FBB}.Release|x64.Build.0 = Release|Any CPU
{05B5EB0A-5D3B-4CAD-9C68-3292A0F17FBB}.Release|x86.ActiveCfg = Release|Any CPU
{05B5EB0A-5D3B-4CAD-9C68-3292A0F17FBB}.Release|x86.Build.0 = Release|Any CPU
{909129FB-810C-4B5D-AD92-774525BD3D77}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{909129FB-810C-4B5D-AD92-774525BD3D77}.Debug|Any CPU.Build.0 = Debug|Any CPU
{909129FB-810C-4B5D-AD92-774525BD3D77}.Debug|x64.ActiveCfg = Debug|Any CPU
{909129FB-810C-4B5D-AD92-774525BD3D77}.Debug|x64.Build.0 = Debug|Any CPU
{909129FB-810C-4B5D-AD92-774525BD3D77}.Debug|x86.ActiveCfg = Debug|Any CPU
{909129FB-810C-4B5D-AD92-774525BD3D77}.Debug|x86.Build.0 = Debug|Any CPU
{909129FB-810C-4B5D-AD92-774525BD3D77}.Release|Any CPU.ActiveCfg = Release|Any CPU
{909129FB-810C-4B5D-AD92-774525BD3D77}.Release|Any CPU.Build.0 = Release|Any CPU
{909129FB-810C-4B5D-AD92-774525BD3D77}.Release|x64.ActiveCfg = Release|Any CPU
{909129FB-810C-4B5D-AD92-774525BD3D77}.Release|x64.Build.0 = Release|Any CPU
{909129FB-810C-4B5D-AD92-774525BD3D77}.Release|x86.ActiveCfg = Release|Any CPU
{909129FB-810C-4B5D-AD92-774525BD3D77}.Release|x86.Build.0 = Release|Any CPU
{68345BDF-E4BA-4F41-8424-F02F613529FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{68345BDF-E4BA-4F41-8424-F02F613529FB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{68345BDF-E4BA-4F41-8424-F02F613529FB}.Debug|x64.ActiveCfg = Debug|Any CPU
{68345BDF-E4BA-4F41-8424-F02F613529FB}.Debug|x64.Build.0 = Debug|Any CPU
{68345BDF-E4BA-4F41-8424-F02F613529FB}.Debug|x86.ActiveCfg = Debug|Any CPU
{68345BDF-E4BA-4F41-8424-F02F613529FB}.Debug|x86.Build.0 = Debug|Any CPU
{68345BDF-E4BA-4F41-8424-F02F613529FB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{68345BDF-E4BA-4F41-8424-F02F613529FB}.Release|Any CPU.Build.0 = Release|Any CPU
{68345BDF-E4BA-4F41-8424-F02F613529FB}.Release|x64.ActiveCfg = Release|Any CPU
{68345BDF-E4BA-4F41-8424-F02F613529FB}.Release|x64.Build.0 = Release|Any CPU
{68345BDF-E4BA-4F41-8424-F02F613529FB}.Release|x86.ActiveCfg = Release|Any CPU
{68345BDF-E4BA-4F41-8424-F02F613529FB}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {DDF51293-E36E-4B16-A826-B74150F6F825}
EndGlobalSection
EndGlobal
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 pt-icg
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+19
View File
@@ -0,0 +1,19 @@
# Threema MsgApi .NET Core
This project is a .NET Core fork of the [Threema Message API SDK-NET](https://gateway.threema.ch/en/developer/sdk-net).
The Threema Message API is an interface that can be used from within customer-specific software to send and receive messages via [Threema Gateway](https://gateway.threema.ch/en).
### Configuration for Testing
Configure in application.json:
- Threema
- PrivateKey
- ThreemaId
- Secret
- Remove entry "SQLiteConnectionString" or set entry for using public key store.
@@ -0,0 +1,332 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# JetBrains Rider
.idea/
*.sln.iml
# CodeRush
.cr/
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/