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; }
}
}
}