Отправка почты Protonmail / PGP в PHP - PullRequest
0 голосов
/ 14 апреля 2020

Я пытаюсь написать простой скрипт для отправки писем из API Protonmail, который ожидает, что письма будут закодированы PGP. Вот что у меня получилось: он успешно создает черновик, но на последнем этапе не удается отправить черновик с сообщением «недопустимый ключ тела сообщения». Где ошибка?

<?php
function send_email($to, $subject, $message, $pmconfig = 'json encoded protonmail keys', $from){
//load config
        $pmconfig = json_decode($pmconfig, true);
        if(!$pmconfig) return -6;
        if(isset($pmconfig['expires']) && $pmconfig['expires'] <= time()) return -7; //expired pm config
        if(!isset($pmconfig['senders'][$from[1]])) return -8; //invalid sender

        $ch = curl_init();
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept: application/vnd.protonmail.v1+json', 'x-pm-appversion: Web_3.16.20', 'x-pm-apiversion: 3', 'Content-Type: application/json', 'x-pm-uid: '.$pmconfig['uid'], 'Authorization: Bearer '.$pmconfig['auth']]);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5_HOSTNAME );
        curl_setopt($ch, CURLOPT_PROXY, $tor_host );
        $baseurl = 'https://protonirockerxow.onion';
//look up recipient on Proton keyserver
        curl_setopt($ch, CURLOPT_URL, $baseurl.'/api/keys?Email='.urlencode($to));
        $recipient = json_decode(curl_exec($ch), true);
        if($recipient['Code'] != 1000)
            return array_merge(['stage' => 'recipient'], $recipient);
//session key
        $messagekey = random_bytes(32);

        $senderkey = ''; //todo: parse sender key from the given public key
        $senderkeyid = '';
        if(isset($recipient['Keys'][0]['PublicKey'])){
            $recipient['Keys'][0]['PublicKey'];
            $recipientkeyid = ''; //todo: parse key from the given public key
            $recipientkeyn = '';
//session key for recipient
            $recipientkey = hex2bin(gz_key($messagekey, $recipientkeyn, $recipientkeyid));
        }
//encrypt session key using my own pgp key
        $key = hex2bin(gz_key($messagekey, $senderkey, $senderkeyid));
        $body = hex2bin(gz_enc(hex2bin(gz_sign(gz_data($message), $senderkeyid)), $messagekey)); 
//create message as draft
        curl_setopt($ch, CURLOPT_URL, $baseurl.'/api/messages');
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['Message' => ['ToList' => [['label' => $to, 'Name' => $to, 'Address' => $to, "encrypt" => false, "invalid" => false, "isContactGroup" => false, "isEO" => false, "isPgp" => false, "isPgpMime" => false, "loadCryptInfo" => false, "sign" => false, "warnings" => []]], 'CCList' => [], 'BCCList' => [], 'Subject' => $subject, 'Unread' => 0, 'MIMEType' => 'text/html', 'Flags' => 0, 'Sender' => ['Name' => $from[0], 'Address' => $from[1]], 'AddressID' => $pmconfig['senders'][$from[1]]['id'], 'Body' => armor($key.$body)], 'id' => '', 'AttachmentKeyPackets' => []]));
        $draft = json_decode(curl_exec($ch), true);
        //echo json_encode(['Message' => ['ToList' => [['label' => $to, 'Name' => $to, 'Address' => $to, "encrypt" => false, "invalid" => false, "isContactGroup" => false, "isEO" => false, "isPgp" => false, "isPgpMime" => false, "loadCryptInfo" => false, "sign" => false, "warnings" => []]], 'CCList' => [], 'BCCList' => [], 'Subject' => $subject, 'Unread' => 0, 'MIMEType' => 'text/html', 'Flags' => 0, 'Sender' => ['Name' => $from[0], 'Address' => $from[1]], 'AddressID' => $pmconfig['senders'][$from[1]]['id'], 'Body' => armor($key.$body)], 'id' => '', 'AttachmentKeyPackets' => []]);
        if($draft['Code'] != 1000)
            return array_merge(['stage' => 'draft'], $draft);


//send the message ****THIS IS THE PART THAT DOESN'T WORK****


        curl_setopt($ch, CURLOPT_URL, $baseurl.'/api/messages/'.urlencode($draft['Message']['ID']));

        if(isset($recipientkey) && !empty($recipientkey)) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['id' => $draft['Message']['ID'], 'Packages' => [['Flags' => 1, 'Addresses' => [$to => ['Type' => 1, 'Signature' => 0, 'AttachmentKeyPackets' => [], 'BodyKeyPacket' => base64_encode($recipientkey)]], 'MIMEType' => 'text/html', 'Body' => base64_encode($body), 'Type' => 1]]])); 
        else curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['id' => $draft['Message']['ID'], 'Packages' => [['Flags' => 13, 'Addresses' => [$to => ['Type' => 4, 'Signature' => 0]], 'MIMEType' => 'text/html', 'Body' => base64_encode($body), 'Type' => 4, 'AttachmentKeys' => [], 'BodyKey' => ['Algorithm' => 'aes256', 'Key' => base64_encode($messagekey)]]]]));
        $sent = json_decode(curl_exec($ch), true);
        //echo json_encode(['id' => $draft['Message']['ID'], 'Packages' => [['Flags' => 1, 'Addresses' => [$to => ['Type' => 1, 'Signature' => 0, 'AttachmentKeyPackets' => [], 'BodyKeyPacket' => base64_encode($recipientkey)]], 'MIMEType' => 'text/html', 'Body' => base64_encode($body), 'Type' => 1]]]);
        //echo json_encode(['id' => $draft['Message']['ID'], 'Packages' => [['Flags' => 1, 'Addresses' => [$to => ['Type' => 4, 'Signature' => 0]], 'MIMEType' => 'text/html', 'Body' => base64_encode($body), 'Type' => 4, 'AttachmentKeys' => [], 'BodyKey' => ['Key' => base64_encode($messagekey), 'Algorithm' => 'aes256']]]]);
        //echo base64_encode($body); die();
        curl_close($ch);
        if($sent['Code'] != 1000)
            return array_merge(['stage' => 'sending'], $sent);
        else 
            return true;
    }
//crc for armor
function crc24($data) {
    $crc = 0x00b704ce;
    for ($i = 0; $i < strlen($data); $i++) {
        $crc ^= (ord($data[$i]) & 255) << 16;
        for ($j = 0; $j < 8; $j++) {
            $crc <<= 1;
            if ($crc & 0x01000000) $crc ^= 0x01864cfb;
        }
    }
    return $crc & 0x00ffffff;   
}
function armor($data, $headers = ['Version: OpenPGP.js v4.7.2']){
    return "-----BEGIN PGP MESSAGE-----\r\n".implode("\r\n", $headers)."\r\n\r\n".trim(chunk_split(base64_encode($data), 60))."\r\n=".base64_encode(substr(pack('N', crc24($data)), 1))."\r\n-----END PGP MESSAGE-----\r\n";
}
function bitlength($data) {
    return str_pad(dechex((strlen($data) - 1) * 8 + (int)floor(log(ord($data[0]), 2)) + 1), 4, '0', STR_PAD_LEFT).bin2hex($data);
}
//builds each of the data packets in a pgp message
function gen_data($type, $version, $data){
    $type = bin2hex(chr($type));
    if(!is_null($version)) $data = bin2hex(chr($version)).$data;
    $count = strlen($data)/2;
    if($count < 192) $header = [$type, str_pad(dechex($count), 2, '0', STR_PAD_LEFT)];
    else if($count <= 8383){
        $sec = ($count-192) & 0xff;
        $header = [$type,str_pad(dechex((($count - $sec - 192) >> 8)+192), 2, '0', STR_PAD_LEFT), str_pad(dechex($sec), 2, '0', STR_PAD_LEFT)];
    }else $header = [$type, 'ff'.str_pad(dechex($count), 8, '0', STR_PAD_LEFT)];   
    //var_dump($header); var_dump($data);
    return implode($header).$data;
}
//encrypt message w/ session key
function gz_enc($data, $messagekey, $random_octet = null){
    if(is_null($random_octet)) $random_octet = random_bytes(16);
    $encoded = $random_octet.substr($random_octet, -2).gen_data(200, 2, bin2hex(gzcompress($data)).'d314');
    return gen_data(210, 1, bin2hex(openssl_encrypt($encoded.sha1($encoded, true), 'AES-256-CFB', $messagekey, OPENSSL_RAW_DATA|OPENSSL_ZERO_PADDING, '0000000000000000')));
}
//encrypt session key w/ pgp key
function gz_key($messagekey, $n, $keyid, $e = '010001'){
    $M = '09'.bin2hex($messagekey).str_pad(dechex(array_sum(unpack('C*', $messagekey)) % 65536), 4, '0', STR_PAD_LEFT);
    $in = gmp_powm(gmp_init('0002'.bin2hex(random_bytes(256-(strlen($M)/2)-3)).'00'.$M, 16), gmp_init($e, 16), gmp_init($n, 16));
    $encrypted = str_pad(gmp_export($in), 256, chr(0), STR_PAD_LEFT);  
    return gen_data(193, 3, $keyid.'01'.bitlength($encrypted));
}
function gz_data($message, $filename = 'msg.txt', $time = null, $format = null){
    if(is_null($time)) $time = time();
    if(is_null($format)) $format = (mb_detect_encoding($message, 'UTF-8', true) == 'UTF-8') ? 'u' : ctype_print($message) ? 't' : 'b';
    if($format != 'b') $message = str_replace("\n", "\r\n", str_replace("\r", "\n", str_replace("\r\n", "\n", $message)));
    return gen_data(203, null, bin2hex($format).bin2hex(strlen($filename)).bin2hex($filename).bin2hex(pack('N', $time)).bin2hex($message));
}
function gz_sign($data, $keyid, $type = 1, $alg = 1, $time = null){
    return $data;
    if(is_null($time)) $time = time();
    $prefix = hex2bin('ca23');
    $sign = hex2bin(''); //todo: generate signature

    $hash = 8; //sha256
    $nested = 1;
    $hashed = chr(2).pack('N', $time);
    $hashed = chr(strlen($hashed)).$hashed;
    $hashed = chr(strlen($hashed)).$hashed;
    $unhashed = chr(10).hex2bin($keyid);
    $unhashed = chr(strlen($unhashed)).$unhashed;
    $unhashed = chr(strlen($unhashed)).$unhashed;

    return gen_data(196, 3, bin2hex(chr($type).chr($hash).chr($alg))).$keyid.bin2hex(chr($nested)).$data.gen_data(194, 4, bin2hex(chr($type).chr($alg).chr($hash).$hashed.$unhashed.$prefix.bitlength($sign))); //chr($type).chr($alg).chr($hash).$hashed.$unhashed.hex2bin($prefix).hex2bin(bitlength($sign)));
}

, чего бы это ни стоило, я попытался использовать библиотеку в начале и столкнулся с еще большим количеством проблем, поэтому я просто взял фрагменты из нее, чтобы закончить с этим. Я почти уверен, что близок, так как черновик, который создает мой сценарий, действительно появляется и полностью читается

...