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:
@@ -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);
|
||||
}
|
||||
}
|
||||
+26
@@ -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);
|
||||
}
|
||||
}
|
||||
+221
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
@@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
+17
@@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+12
@@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
+21
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
@@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
+12
@@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
+12
@@ -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);
|
||||
}
|
||||
}
|
||||
+22
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+101
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+95
@@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
+44
@@ -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
|
||||
@@ -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.
|
||||
@@ -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/
|
||||
Reference in New Issue
Block a user