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 { /// /// Facilitates HTTPS communication with the Threema Message API. /// 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(); } } /// /// Lookup credits for an ID. /// /// credits or null 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; } /// /// Lookup an ID by email address. The email address will be hashed before /// being sent to the server. /// /// the email address /// the ID, or null if not found public string LookupEmail(string email) { try { Dictionary getParams = MakeRequestParams(); byte[] emailHash = CryptTool.HashEmail(email); return DoGet(new Uri(this.apiUrl + "lookup/email_hash/" + DataUtils.ByteArrayToHexString(emailHash)), getParams); } catch (FileNotFoundException) { return null; } } /// /// Lookup a public key by ID. /// /// The ID whose public key is desired /// The corresponding public key, or null if not found public byte[] LookupKey(string id) { byte[] key = this.publicKeyStore.GetPublicKey(id); if (key == null) { try { Dictionary 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; } /// /// Lookup the capabilities of a ID /// /// The ID whose capabilities should be checked /// The capabilities, or null if not found 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; } /// /// Lookup an ID by phone number. The phone number will be hashed before /// being sent to the server. /// /// the phone number in E.164 format /// the ID, or null if not found public string LookupPhone(string phoneNumber) { try { Dictionary getParams = MakeRequestParams(); byte[] phoneHash = CryptTool.HashPhoneNo(phoneNumber); return DoGet(new Uri(this.apiUrl + "lookup/phone_hash/" + DataUtils.ByteArrayToHexString(phoneHash)), getParams); } catch (FileNotFoundException) { return null; } } /// /// Download a file given its blob ID. /// /// The blob ID of the file /// Encrypted file data public byte[] DownloadFile(byte[] blobId) { return this.DownloadFile(blobId, null); } /// /// Download a file given its blob ID. /// /// The blob ID of the file /// An object that will receive progress information, or null /// Encrypted file data 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; } /// /// Upload a file. /// /// The result of the file encryption (i.e. encrypted file data) /// the result of the upload 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); } /// /// Send an end-to-end encrypted message. /// /// recipient ID /// nonce used for encryption (24 bytes) /// encrypted message data (max. 4000 bytes) /// message ID public string SendE2EMessage(string to, byte[] nonce, byte[] box) { Dictionary 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); } /// /// Send a text message with server-side encryption. /// /// recipient ID /// message text (max. 3500 bytes) /// message ID public string SendTextMessageSimple(string to, string text) { Dictionary 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 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 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 MakeRequestParams() { Dictionary postParams = new Dictionary(); postParams.Add("from", apiIdentity); postParams.Add("secret", secret); return postParams; } private String MakeUrlEncoded(Dictionary parameters) { StringBuilder s = new StringBuilder(); foreach (KeyValuePair 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; } } }