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 { /// /// Contains static methods to do various Threema cryptography related tasks. /// 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; /// /// Encrypt a text message. /// /// the text to be encrypted (max. 3500 bytes) /// the private key of the sending ID /// the public key of the receiving ID /// public static EncryptResult EncryptTextMessage(String text, byte[] senderPrivateKey, byte[] recipientPublicKey) { return EncryptMessage(new TextMessage(text), senderPrivateKey, recipientPublicKey); } /// /// Encrypt an image message. /// /// result of the image encryption /// result of the upload /// the private key of the sending ID /// the public key of the receiving ID /// encrypted result public static EncryptResult EncryptImageMessage(EncryptResult encryptResult, UploadResult uploadResult, byte[] senderPrivateKey, byte[] recipientPublicKey) { return EncryptMessage( new ImageMessage(uploadResult.BlobId, encryptResult.Size, encryptResult.Nonce), senderPrivateKey, recipientPublicKey); } /// /// Encrypt a file message. /// /// result of the file data encryption /// result of the upload /// MIME type of the file /// File name /// Size of the file, in bytes /// result of thumbnail upload /// Private key of sender /// Public key of recipient /// Result of the file message encryption (not the same as the file data encryption!) 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); } /// /// Decrypt an NaCl box using the recipient's private key and the sender's public key. /// /// The box to be decrypted /// The private key of the recipient /// The public key of the sender /// The nonce that was used for encryption /// The decrypted data, or null if decryption failed public static byte[] Decrypt(byte[] box, byte[] privateKey, byte[] publicKey, byte[] nonce) { return Sodium.PublicKeyBox.Open(box, nonce, privateKey, publicKey); } /// /// Decrypt symmetrically encrypted file data. /// /// The encrypted file data /// The symmetric key that was used for encryption /// The decrypted file data, or null if decryption failed 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); } /// /// Decrypt symmetrically encrypted file thumbnail data. /// /// The encrypted thumbnail data /// The symmetric key that was used for encryption /// The decrypted thumbnail data, or null if decryption failed 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); } /// /// Decrypt a message. /// /// the box to be decrypted /// the private key of the receiving ID /// the public key of the sending ID /// the nonce that was used for the encryption /// decrypted message (text or delivery receipt) 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 messageIds = new LinkedList(); 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); } /// /// Generate a new key pair. /// /// is used to return the generated private key (length must be SealedPublicKeyBox.RecipientSecretKeyBytes) /// is used to return the generated public key (length must be SealedPublicKeyBox.RecipientPublicKeyBytes) 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; } /// /// Encrypt data using NaCl asymmetric ("box") encryption. /// /// the data to be encrypted /// is used to return the generated private key (length must be SealedPublicKeyBox.RecipientSecretKeyBytes) /// is used to return the generated public key (length must be SealedPublicKeyBox.RecipientPublicKeyBytes) /// 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); } /// /// Encrypt file data using NaCl symmetric encryption with a random key. /// /// the file contents to be encrypted /// the encryption result including the random key 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); } /// /// Encrypt file thumbnail data using NaCl symmetric encryption with a random key. /// /// data the file contents to be encrypted /// /// the encryption result including the random key 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); } /// /// Hashes an email address for identity lookup. /// /// email the email address /// the raw hash 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; } } /// /// Hashes a phone number for identity lookup. /// /// phoneNo the phone number /// the raw hash 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; } } /// /// Generate a random nonce. /// /// random nonce public static byte[] RandomNonce() { byte[] nonce = new byte[ThreemaMessage.NONCEBYTES]; new Random().NextBytes(nonce); return nonce; } /// /// Return the public key that corresponds with a given private key. /// /// The private key whose public key should be derived /// The corresponding public key. public static byte[] DerivePublicKey(byte[] privateKey) { Sodium.KeyPair keyPair = Sodium.PublicKeyBox.GenerateKeyPair(privateKey); return keyPair.PublicKey; } } }