source/Threema/MsgApi/Tools/CryptTool.php
<?php
/**
* @author Threema GmbH
* @copyright Copyright (c) 2015-2016 Threema GmbH
*/
namespace Threema\MsgApi\Tools;
use Threema\Core\Exception;
use Threema\Core\KeyPair;
use Threema\Core\AssocArray;
use Threema\MsgApi\Commands\Results\UploadFileResult;
use Threema\MsgApi\Exceptions\BadMessageException;
use Threema\MsgApi\Exceptions\DecryptionFailedException;
use Threema\MsgApi\Exceptions\UnsupportedMessageTypeException;
use Threema\MsgApi\Messages\DeliveryReceipt;
use Threema\MsgApi\Messages\FileMessage;
use Threema\MsgApi\Messages\ImageMessage;
use Threema\MsgApi\Messages\TextMessage;
use Threema\MsgApi\Messages\ThreemaMessage;
/**
* Interface CryptTool
* Contains static methods to do various Threema cryptography related tasks.
*
* @package Threema\MsgApi\Tool
*/
abstract class CryptTool {
const TYPE_SODIUM = 'sodium';
const TYPE_SALT = 'salt';
const FILE_NONCE = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01";
const FILE_THUMBNAIL_NONCE = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02";
/**
* @var CryptTool
*/
private static $instance = null;
/**
* Prior libsodium
*
* @return CryptTool
*/
public static function getInstance() {
if(null === self::$instance) {
foreach(array(
function() {
return self::createInstance(self::TYPE_SODIUM);
},
function() {
return self::createInstance(self::TYPE_SALT);
}) as $instanceGenerator) {
$i = $instanceGenerator->__invoke();
if(null !== $i) {
self::$instance = $i;
break;
}
}
}
return self::$instance;
}
/**
* @param string $type
* @return null|CryptTool null on unknown type
*/
public static function createInstance($type) {
switch($type) {
case self::TYPE_SODIUM:
$instance = new CryptToolSodium();
if(false === $instance->isSupported()) {
//try to instance old version of sodium wrapper
/** @noinspection PhpDeprecationInspection */
$instance = new CryptToolSodiumDep();
}
return $instance->isSupported() ? $instance :null;
case self::TYPE_SALT:
$instance = new CryptToolSalt();
return $instance->isSupported() ? $instance :null;
default:
return null;
}
}
const MESSAGE_ID_LEN = 8;
const BLOB_ID_LEN = 16;
const IMAGE_FILE_SIZE_LEN = 4;
const IMAGE_NONCE_LEN = 24;
const EMAIL_HMAC_KEY = "\x30\xa5\x50\x0f\xed\x97\x01\xfa\x6d\xef\xdb\x61\x08\x41\x90\x0f\xeb\xb8\xe4\x30\x88\x1f\x7a\xd8\x16\x82\x62\x64\xec\x09\xba\xd7";
const PHONENO_HMAC_KEY = "\x85\xad\xf8\x22\x69\x53\xf3\xd9\x6c\xfd\x5d\x09\xbf\x29\x55\x5e\xb9\x55\xfc\xd8\xaa\x5e\xc4\xf9\xfc\xd8\x69\xe2\x58\x37\x07\x23";
protected function __construct() {}
protected function __clone() {}
/**
* Encrypt a text message.
*
* @param string $text the text to be encrypted (max. 3500 bytes)
* @param string $senderPrivateKey the private key of the sending ID
* @param string $recipientPublicKey the public key of the receiving ID
* @param string $nonce the nonce to be used for the encryption (usually 24 random bytes)
* @return string encrypted box
*/
final public function encryptMessageText($text, $senderPrivateKey, $recipientPublicKey, $nonce) {
/* prepend type byte (0x01) to message data */
$textBytes = "\x01" . $text;
/* determine random amount of PKCS7 padding */
$padbytes = $this->generatePadBytes();
/* append padding */
$textBytes .= str_repeat(chr($padbytes), $padbytes);
return $this->makeBox($textBytes, $nonce, $senderPrivateKey, $recipientPublicKey);
}
/**
* @param UploadFileResult $uploadFileResult the result of the upload
* @param EncryptResult $encryptResult the result of the image encryption
* @param string $senderPrivateKey the private key of the sending ID (as binary)
* @param string $recipientPublicKey the public key of the receiving ID (as binary)
* @param string $nonce the nonce to be used for the encryption (usually 24 random bytes)
* @return string
*/
final public function encryptImageMessage(
UploadFileResult $uploadFileResult,
EncryptResult $encryptResult,
$senderPrivateKey,
$recipientPublicKey,
$nonce) {
$message = "\x02" . $this->hex2bin($uploadFileResult->getBlobId());
$message .= pack('V', $encryptResult->getSize());
$message .= $encryptResult->getNonce();
/* determine random amount of PKCS7 padding */
$padbytes = $this->generatePadBytes();
/* append padding */
$message .= str_repeat(chr($padbytes), $padbytes);
return $this->makeBox($message, $nonce, $senderPrivateKey, $recipientPublicKey);
}
final public function encryptFileMessage(UploadFileResult $uploadFileResult,
EncryptResult $encryptResult,
UploadFileResult $thumbnailUploadFileResult = null,
FileAnalysisResult $fileAnalysisResult,
$senderPrivateKey,
$recipientPublicKey,
$nonce) {
$messageContent = array(
'b' => $uploadFileResult->getBlobId(),
'k' => $this->bin2hex($encryptResult->getKey()),
'm' => $fileAnalysisResult->getMimeType(),
'n' => $fileAnalysisResult->getFileName(),
's' => $fileAnalysisResult->getSize(),
'i' => 0
);
if($thumbnailUploadFileResult !== null && strlen($thumbnailUploadFileResult->getBlobId()) > 0) {
$messageContent['t'] = $thumbnailUploadFileResult->getBlobId();
}
$message = "\x17" . json_encode($messageContent);
/* determine random amount of PKCS7 padding */
$padbytes = $this->generatePadBytes();
/* append padding */
$message .= str_repeat(chr($padbytes), $padbytes);
return $this->makeBox($message, $nonce, $senderPrivateKey, $recipientPublicKey);
}
/**
* make a box
*
* @param string $data
* @param string $nonce
* @param string $senderPrivateKey
* @param string $recipientPublicKey
* @return string encrypted box
*/
abstract protected function makeBox($data, $nonce, $senderPrivateKey, $recipientPublicKey);
/**
* make a secret box
*
* @param $data
* @param $nonce
* @param $key
* @return mixed
*/
abstract protected function makeSecretBox($data, $nonce, $key);
/**
* decrypt a box
*
* @param string $box as binary
* @param string $recipientPrivateKey as binary
* @param string $senderPublicKey as binary
* @param string $nonce as binary
* @return string
*/
abstract protected function openBox($box, $recipientPrivateKey, $senderPublicKey, $nonce);
/**
* decrypt a secret box
*
* @param string $box as binary
* @param string $nonce as binary
* @param string $key as binary
* @return string as binary
*/
abstract protected function openSecretBox($box, $nonce, $key);
/**
* @param string $box
* @param string $recipientPrivateKey
* @param string $senderPublicKey
* @param string $nonce
* @return ThreemaMessage the decrypted message
* @throws BadMessageException
* @throws DecryptionFailedException
* @throws UnsupportedMessageTypeException
*/
final public function decryptMessage($box, $recipientPrivateKey, $senderPublicKey, $nonce) {
$data = $this->openBox($box, $recipientPrivateKey, $senderPublicKey, $nonce);
if (null === $data || strlen($data) == 0) {
throw new DecryptionFailedException();
}
/* remove padding */
$padbytes = ord($data[strlen($data)-1]);
$realDataLength = strlen($data) - $padbytes;
if ($realDataLength < 1) {
throw new BadMessageException();
}
$data = substr($data, 0, $realDataLength);
/* first byte of data is type */
$type = ord($data[0]);
$pos = 1;
$piece = function($length) use(&$pos, $data) {
$d = substr($data, $pos, $length);
$pos += $length;
return $d;
};
switch ($type) {
case TextMessage::TYPE_CODE:
/* Text message */
if ($realDataLength < 2) {
throw new BadMessageException();
}
return new TextMessage(substr($data, 1));
case DeliveryReceipt::TYPE_CODE:
/* Delivery receipt */
if ($realDataLength < (self::MESSAGE_ID_LEN-2) || (($realDataLength - 2) % self::MESSAGE_ID_LEN) != 0) {
throw new BadMessageException();
}
$receiptType = ord($data[1]);
$messageIds = str_split(substr($data, 2), self::MESSAGE_ID_LEN);
return new DeliveryReceipt($receiptType, $messageIds);
case ImageMessage::TYPE_CODE:
/* Image Message */
if ($realDataLength != 1 + self::BLOB_ID_LEN + self::IMAGE_FILE_SIZE_LEN + self::IMAGE_NONCE_LEN) {
throw new BadMessageException();
}
$blobId = $piece->__invoke(self::BLOB_ID_LEN);
$length = $piece->__invoke(self::IMAGE_FILE_SIZE_LEN);
$nonce = $piece->__invoke(self::IMAGE_NONCE_LEN);
return new ImageMessage($this->bin2hex($blobId), $this->bin2hex($length), $nonce);
case FileMessage::TYPE_CODE:
/* Image Message */
$decodeResult = json_decode(substr($data, 1), true);
if(null === $decodeResult || false === $decodeResult) {
throw new BadMessageException();
}
$values = AssocArray::byJsonString(substr($data, 1), array('b', 't', 'k', 'm', 'n', 's'));
if(null === $values) {
throw new BadMessageException();
}
return new FileMessage(
$values->getValue('b'),
$values->getValue('t'),
$values->getValue('k'),
$values->getValue('m'),
$values->getValue('n'),
$values->getValue('s'));
default:
throw new UnsupportedMessageTypeException();
}
}
/**
* Generate a new key pair.
*
* @return KeyPair the new key pair
*/
abstract public function generateKeyPair();
/**
* Hashes an email address for identity lookup.
*
* @param string $email the email address
* @return string the email hash (hex)
*/
final public function hashEmail($email) {
$emailClean = strtolower(trim($email));
return hash_hmac('sha256', $emailClean, self::EMAIL_HMAC_KEY);
}
/**
* Hashes an phone number address for identity lookup.
*
* @param string $phoneNo the phone number (in E.164 format, no leading +)
* @return string the phone number hash (hex)
*/
final public function hashPhoneNo($phoneNo) {
$phoneNoClean = preg_replace("/[^0-9]/", "", $phoneNo);
return hash_hmac('sha256', $phoneNoClean, self::PHONENO_HMAC_KEY);
}
abstract protected function createRandom($size);
/**
* Generate a random nonce.
*
* @return string random nonce
*/
final public function randomNonce() {
return $this->createRandom(\Salt::box_NONCE);
}
/**
* Generate a symmetric key
* @return mixed
*/
final public function symmetricKey() {
return $this->createRandom(32);
}
/**
* Derive the public key
*
* @param string $privateKey as binary
* @return string as binary
*/
abstract public function derivePublicKey($privateKey);
/**
* Check if implementation supported
* @return bool
*/
abstract public function isSupported();
/**
* Validate crypt tool
*
* @return bool
* @throws Exception
*/
abstract public function validate();
/**
* @param $data
* @return EncryptResult
*/
public final function encryptFile($data) {
$key = $this->symmetricKey();
$box = $this->makeSecretBox($data, self::FILE_NONCE, $key);
return new EncryptResult($box, $key, self::FILE_NONCE, strlen($box));
}
/**
* @param string $data as binary
* @param string $key as binary
* @return null|string
*/
public final function decryptFile($data, $key) {
$result = $this->openSecretBox($data, self::FILE_NONCE, $key);
return false === $result ? null : $result;
}
/**
* @param string $data
* @param string $key
* @return EncryptResult
*/
public final function encryptFileThumbnail($data, $key) {
$box = $this->makeSecretBox($data, self::FILE_THUMBNAIL_NONCE, $key);
return new EncryptResult($box, $key, self::FILE_THUMBNAIL_NONCE, strlen($box));
}
public final function decryptFileThumbnail($data, $key) {
$result = $this->openSecretBox($data, self::FILE_THUMBNAIL_NONCE, $key);
return false === $result ? null : $result;
}
/**
* @param string $imageData
* @param string $privateKey as binary
* @param string $publicKey as binary
* @return EncryptResult
*/
public final function encryptImage($imageData, $privateKey, $publicKey) {
$nonce = $this->randomNonce();
$box = $this->makeBox(
$imageData,
$nonce,
$privateKey,
$publicKey
);
return new EncryptResult($box, null, $nonce, strlen($box));
}
/**
* @param string $data as binary
* @param string $publicKey as binary
* @param string $privateKey as binary
* @param string $nonce as binary
* @return string
*/
public final function decryptImage($data, $publicKey, $privateKey, $nonce) {
return $this->openBox($data,
$privateKey,
$publicKey,
$nonce);
}
/**
* determine random amount of PKCS7 padding
* @return int
*/
private function generatePadBytes() {
$padbytes = 0;
while($padbytes < 1 || $padbytes > 255) {
$padbytes = ord($this->createRandom(1));
}
return $padbytes;
}
public function __toString() {
return 'CryptTool '.$this->getName();
}
/**
* Converts a binary string to an hexdecimal string.
*
* This is the same as PHP's bin2hex() implementation, but it is resistant to
* timing attacks.
*
* @param string $binaryString The binary string to convert
* @return string
*/
public function bin2hex($binaryString)
{
return bin2hex($binaryString);
}
/**
* Converts an hexdecimal string to a binary string.
*
* This is the same as PHP's hex2bin() implementation, but it is resistant to
* timing attacks.
* Note that the default implementation does not support $ignore currrently and will
* throw an error. Only when libsodium >= 0.22 is used, this is supported.
*
* @param string $hexString The hex string to convert
* @param string|null $ignore (optional) Characters to ignore
* @throws \Threema\Core\Exception
* @return string
*/
public function hex2bin($hexString, $ignore = null)
{
if ($ignore !== null) {
throw new Exception('$ignore parameter is not supported');
}
return hex2bin($hexString);
}
/**
* Compares two strings in a secure way.
*
* This is the same as PHP's strcmp() implementation, but it is resistant to
* timing attacks.
*
* @link https://paragonie.com/book/pecl-libsodium/read/03-utilities-helpers.md#compare
* @param string $str1 The first string
* @param string $str2 The second string
* @return bool
*/
public function stringCompare($str1, $str2)
{
if (function_exists('hash_equals')) {
return hash_equals($str1, $str2);
} else {
// check variable type manually
if (!is_string($str1) || !is_string($str2)) {
return false;
}
// fast comparison: check string length
if (strlen($str1) != strlen($str2)) {
return false;
}
# PHP implementation of hash_equals
# partly taken from https://github.com/symfony/polyfill-php56/blob/master/Php56.php#L45-L51
#
# Note that this is really slow!!
#
$ret = 0;
$length = strlen($str1);
for ($i = 0; $i < $length; ++$i) {
$ret |= ord($str1[$i]) ^ ord($str2[$i]);
}
return 0 === $ret;
}
}
/**
* Unsets/removes a variable.
*
* Note: the PHP implementation here provides no security, but if you use
* Libsodium, the variable will be deleted in a better way.
*
* @param string $var A variable, passed by reference
*/
public function removeVar(&$var)
{
// overwrite var (128x0), quite certainly not secure at all
$var = '00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000';
$var = null;
// actually this does not erase the content of the variable
unset($var);
}
/**
* Name of the CryptTool
* @return string
*/
abstract public function getName();
/**
* Description of the CryptTool
* @return string
*/
abstract public function getDescription();
}