├── .gitignore ├── .project ├── README.md ├── composer.json ├── libs ├── GPG.php └── GPG │ ├── AES.php │ ├── Cipher.php │ ├── Expanded_Key.php │ ├── Public_Key.php │ ├── Utility.php │ └── globals.php └── tests ├── gpg ├── EncryptTest.php └── KeyTest.php └── runtests.sh /.gitignore: -------------------------------------------------------------------------------- 1 | /.buildpath 2 | /.settings 3 | .DS_Store -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | php-gpg 4 | 5 | 6 | 7 | 8 | 9 | org.eclipse.wst.validation.validationbuilder 10 | 11 | 12 | 13 | 14 | org.eclipse.dltk.core.scriptbuilder 15 | 16 | 17 | 18 | 19 | 20 | org.eclipse.php.core.PHPNature 21 | 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | php-gpg 2 | ======= 3 | 4 | **This project is currently not actively maintained, please take a look at https://github.com/singpolyma/openpgp-php as alternative.** 5 | 6 | php-gpg is a pure PHP implementation of GPG/PGP (currently supports encryption only). The library does not require PGP/GPG binaries and should run on any platform that supports PHP. 7 | 8 | This library is useful for encrypting data before it is sent over an insecure protocol (for example email). Messages encrypted with this library are compatible and can be decrypted by standard GPG/PGP clients. 9 | 10 | Features/Limitations 11 | -------------------- 12 | 13 | * Supports RSA, DSA public key length of 2,4,8,16,512,1024,2048 or 4096 14 | * Currently supports only encrypt 15 | * Encrypted messages are integrity protected (php-gpg 1.6.0+) 16 | 17 | Hey You! If you have a good understanding of public key encryption and want to implement signing or decryption your pull request would be welcome. 18 | 19 | Composer 20 | ----- 21 | If you use PHP < 7.0, please install random_compat with Composer. 22 | 23 | Just run `php composer.phar install` or `composer install` in the directory of the composer.json file. 24 | 25 | Usage 26 | ----- 27 | 28 | ```php 29 | require __DIR__ . '/vendor/autoload.php'; 30 | require_once 'libs/GPG.php'; 31 | 32 | $gpg = new GPG(); 33 | 34 | // create an instance of a GPG public key object based on ASCII key 35 | $pub_key = new GPG_Public_Key($public_key_ascii); 36 | 37 | // using the key, encrypt your plain text using the public key 38 | $encrypted = $gpg->encrypt($pub_key,$plain_text_string); 39 | 40 | echo $encrypted; 41 | 42 | ``` 43 | 44 | License 45 | ------- 46 | 47 | GPL http://www.gnu.org/copyleft/gpl.html 48 | 49 | I'd like to release this under a more permissive license, but since PGP & GPG itself are GPL, I think this library is likely bound to the terms of GPL as well. 50 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "paragonie/random_compat": "1.1.*" 4 | }, 5 | "name": "jasonhinkle/php-gpg", 6 | "description": "GPG / PGP port written in pure PHP with no binary dependencies", 7 | "type": "library", 8 | "keywords": ["gpg"], 9 | "license": "GPL", 10 | "authors": [ 11 | { 12 | "name": "Jason Hinkle" 13 | } 14 | ], 15 | "autoload": { 16 | "classmap": ["libs/GPG.php"] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /libs/GPG.php: -------------------------------------------------------------------------------- 1 | encrypt($pub_key,$plain_text_string); 26 | */ 27 | class GPG 28 | { 29 | 30 | private $width = 16; 31 | private $el = array(3, 5, 9, 17, 513, 1025, 2049, 4097); 32 | private $version = "1.6.4"; 33 | 34 | private function gpg_encrypt($key, $text) { 35 | 36 | $i = 0; 37 | $len = safeStrlen($text); 38 | $iblock = array_fill(0, $this->width, 0); 39 | $rblock = array_fill(0, $this->width, 0); 40 | $ct = array_fill(0, $this->width + 2, 0); 41 | 42 | $cipher = ""; 43 | 44 | if($len % $this->width) { 45 | for($i = ($len % $this->width); $i < $this->width; $i++) $text .= "\0"; 46 | } 47 | 48 | $ekey = new Expanded_Key($key); 49 | 50 | for($i = 0; $i < $this->width; $i++) { 51 | $iblock[$i] = 0; 52 | $rblock[$i] = GPG_Utility::c_random(); 53 | } 54 | 55 | $strLen = safeStrlen($text); 56 | 57 | for($n = 0; $n < $strLen; $n += $this->width) { 58 | $iblock = GPG_AES::encrypt($iblock, $ekey); 59 | for($i = 0; $i < $this->width; $i++) { 60 | $iblock[$i] ^= ord($text[$n + $i]); 61 | $cipher .= chr($iblock[$i]); 62 | } 63 | } 64 | 65 | return substr($cipher, 0, $len); 66 | } 67 | 68 | private function gpg_header($tag, $len) 69 | { 70 | $h = ""; 71 | if ($len < 0x100) { 72 | $h .= chr($tag); 73 | $h .= chr($len); 74 | } else if ($len < 0x10000) { 75 | $tag+=1; 76 | $h .= chr($tag); 77 | $h .= $this->writeNumber($len, 2); 78 | } else { 79 | $tag+=2; 80 | $h .= chr($tag); 81 | $h .= $this->writeNumber($len, 4); 82 | } 83 | return $h; 84 | } 85 | 86 | private function writeNumber($n, $bytes) 87 | { 88 | // credits for this function go to OpenPGP.js 89 | $b = ''; 90 | for ($i = 0; $i < $bytes; $i++) { 91 | $b .= chr(($n >> (8 * ($bytes - $i - 1))) & 0xff); 92 | } 93 | return $b; 94 | } 95 | 96 | private function gpg_session($key_id, $key_type, $session_key, $public_key) 97 | { 98 | 99 | $mod = array(); 100 | $exp = array(); 101 | $enc = ""; 102 | 103 | $s = base64_decode($public_key); 104 | $l = floor((ord($s[0]) * 256 + ord($s[1]) + 7) / 8); 105 | $mod = mpi2b(substr($s, 0, $l + 2)); 106 | if($key_type) { 107 | $grp = array(); 108 | $y = array(); 109 | $B = array(); 110 | $C = array(); 111 | 112 | $l2 = floor((ord($s[$l + 2]) * 256 + ord($s[$l + 3]) + 7) / 8) + 2; 113 | $grp = mpi2b(substr($s, $l + 2, $l2)); 114 | $y = mpi2b(substr($s, $l + 2 + $l2)); 115 | $exp[0] = $this->el[GPG_Utility::c_random() & 7]; 116 | $B = bmodexp($grp, $exp, $mod); 117 | $C = bmodexp($y, $exp, $mod); 118 | } else { 119 | $exp = mpi2b(substr($s, $l + 2)); 120 | } 121 | 122 | $c = 0; 123 | $lsk = strlen($session_key); 124 | for($i = 0; $i < $lsk; $i++) $c += ord($session_key[$i]); 125 | $c &= 0xffff; 126 | 127 | $lm = ($l - 2) * 8 + 2; 128 | $m = chr($lm / 256) . chr($lm % 256) . 129 | chr(2) . GPG_Utility::s_random($l - $lsk - 6, 1) . "\0" . 130 | chr(7) . $session_key . 131 | chr($c / 256) . chr($c & 0xff); 132 | 133 | if($key_type) { 134 | $enc = b2mpi($B) . b2mpi(bmod(bmul(mpi2b($m), $C), $mod)); 135 | return $this->gpg_header(0x84,strlen($enc) + 10) . 136 | chr(3) . $key_id . chr(16) . $enc; 137 | } else { 138 | $enc = b2mpi(bmodexp(mpi2b($m), $exp, $mod)); 139 | return $this->gpg_header(0x84, strlen($enc) + 10) . 140 | chr(3) . $key_id . chr(1) . $enc; 141 | } 142 | } 143 | 144 | private function gpg_literal($text) 145 | { 146 | if (strpos($text, "\r\n") === false) 147 | $text = str_replace("\n", "\r\n", $text); 148 | 149 | return chr(11 | 0xC0) . chr(255) . $this->writeNumber(safeStrlen($text) + 10, 4) . "t" . chr(4) . "file\0\0\0\0" . $text; 150 | } 151 | 152 | private function gpg_data($key, $text) 153 | { 154 | $prefix = GPG_Utility::s_random($this->width, 0); 155 | $prefix .= substr($prefix, -2); 156 | $mdc="\xD3\x14".hash('sha1', $prefix.$this->gpg_literal($text)."\xD3\x14", true); 157 | $enc = $this->gpg_encrypt($key, $prefix.$this->gpg_literal($text).$mdc); 158 | return chr(0x12 | 0xC0) . chr(255) . $this->writeNumber(1+strlen($enc), 4) . chr(1) . $enc; 159 | } 160 | 161 | /** 162 | * GPG Encypts a message to the provided public key 163 | * 164 | * @param GPG_Public_Key $pk 165 | * @param string $plaintext 166 | * @param string $versionHeader 167 | * @return string encrypted text 168 | */ 169 | function encrypt($pk, $plaintext, $versionHeader=NULL) 170 | { 171 | // normalize the public key 172 | $key_id = $pk->GetKeyId(); 173 | $key_type = $pk->GetKeyType(); 174 | $public_key = $pk->GetPublicKey(); 175 | 176 | $session_key = GPG_Utility::s_random($this->width, 0); 177 | $key_id = GPG_Utility::hex2bin($key_id); 178 | $cp = $this->gpg_session($key_id, $key_type, $session_key, $public_key) . 179 | $this->gpg_data($session_key, $plaintext); 180 | 181 | $code = base64_encode($cp); 182 | $code = wordwrap($code, 64, "\n", 1); 183 | 184 | if($versionHeader===NULL) $versionHeader="Version: VerySimple PHP-GPG v" . $this->version . "\n\n"; 185 | else if (safeStrlen($versionHeader)>0)$versionHeader="Version: " . $versionHeader . "\n\n"; 186 | 187 | return 188 | "-----BEGIN PGP MESSAGE-----\n" . 189 | $versionHeader . 190 | $code . "\n=" . base64_encode(GPG_Utility::crc24($cp)) . 191 | "\n-----END PGP MESSAGE-----\n"; 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /libs/GPG/AES.php: -------------------------------------------------------------------------------- 1 | rounds; 30 | $b0 = $b[0]; 31 | $b1 = $b[1]; 32 | $b2 = $b[2]; 33 | $b3 = $b[3]; 34 | 35 | for($r = 0; $r < $rounds - 1; $r++) { 36 | $t0 = $b0 ^ $ctx->rk[$r][0]; 37 | $t1 = $b1 ^ $ctx->rk[$r][1]; 38 | $t2 = $b2 ^ $ctx->rk[$r][2]; 39 | $t3 = $b3 ^ $ctx->rk[$r][3]; 40 | 41 | $b0 = $T1[$t0 & 255] ^ $T2[($t1 >> 8) & 255] ^ $T3[($t2 >> 16) & 255] ^ $T4[GPG_Utility::zshift($t3, 24)]; 42 | $b1 = $T1[$t1 & 255] ^ $T2[($t2 >> 8) & 255] ^ $T3[($t3 >> 16) & 255] ^ $T4[GPG_Utility::zshift($t0, 24)]; 43 | $b2 = $T1[$t2 & 255] ^ $T2[($t3 >> 8) & 255] ^ $T3[($t0 >> 16) & 255] ^ $T4[GPG_Utility::zshift($t1, 24)]; 44 | $b3 = $T1[$t3 & 255] ^ $T2[($t0 >> 8) & 255] ^ $T3[($t1 >> 16) & 255] ^ $T4[GPG_Utility::zshift($t2, 24)]; 45 | } 46 | 47 | $r = $rounds - 1; 48 | 49 | $t0 = $b0 ^ $ctx->rk[$r][0]; 50 | $t1 = $b1 ^ $ctx->rk[$r][1]; 51 | $t2 = $b2 ^ $ctx->rk[$r][2]; 52 | $t3 = $b3 ^ $ctx->rk[$r][3]; 53 | 54 | $b[0] = GPG_Cipher::F1($t0, $t1, $t2, $t3) ^ $ctx->rk[$rounds][0]; 55 | $b[1] = GPG_Cipher::F1($t1, $t2, $t3, $t0) ^ $ctx->rk[$rounds][1]; 56 | $b[2] = GPG_Cipher::F1($t2, $t3, $t0, $t1) ^ $ctx->rk[$rounds][2]; 57 | $b[3] = GPG_Cipher::F1($t3, $t0, $t1, $t2) ^ $ctx->rk[$rounds][3]; 58 | 59 | return GPG_Utility::unpack_octets($b); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /libs/GPG/Cipher.php: -------------------------------------------------------------------------------- 1 | > 0x8) & 0xff]) << 0x8) | 328 | (GPG_Utility::B1($T1[($x2 >> 0x10) & 0xff]) << 0x10) | (GPG_Utility::B1($T1[GPG_Utility::zshift($x3, 0x18)]) << 0x18); 329 | } 330 | 331 | 332 | } 333 | -------------------------------------------------------------------------------- /libs/GPG/Expanded_Key.php: -------------------------------------------------------------------------------- 1 | = 0; $j--) $tk[$j] = $k[$j]; 56 | 57 | $r = 0; 58 | $t = 0; 59 | for($j = 0; ($j < $kc) && ($r < $rounds + 1); ) { 60 | for(; ($j < $kc) && ($t < 4); $j++, $t++) { 61 | $keySched[$r][$t] = $tk[$j]; 62 | } 63 | if($t == 4) { 64 | $r++; 65 | $t = 0; 66 | } 67 | } 68 | 69 | while($r < $rounds + 1) { 70 | $temp = $tk[$kc - 1]; 71 | 72 | $tk[0] ^= hexdec($S[GPG_Utility::B1($temp)]) | (hexdec($S[GPG_Utility::B2($temp)]) << 0x8) | 73 | (hexdec($S[GPG_Utility::B3($temp)]) << 0x10) | (hexdec($S[GPG_Utility::B0($temp)]) << 0x18); 74 | $tk[0] ^= $RCON[$rconpointer++]; 75 | 76 | if ($kc != 8) { 77 | for($j = 1; $j < $kc; $j++) $tk[$j] ^= $tk[$j - 1]; 78 | } else { 79 | for($j = 1; $j < $kc / 2; $j++) $tk[$j] ^= $tk[$j - 1]; 80 | 81 | $temp = $tk[$kc / 2 - 1]; 82 | $tk[$kc / 2] ^= hexdec($S[GPG_Utility::B0($temp)]) | (hexdec($S[GPG_Utility::B1($temp)]) << 0x8) | 83 | (hexdec($S[GPG_Utility::B2($temp)] << 0x10)) | (hexdec($S[GPG_Utility::B3($temp)]) << 0x18); 84 | 85 | for($j = $kc / 2 + 1; $j < $kc; $j++) $tk[$j] ^= $tk[$j - 1]; 86 | } 87 | 88 | for($j = 0; ($j < $kc) && ($r < $rounds + 1); ) { 89 | for(; ($j < $kc) && ($t < 4); $j++, $t++) { 90 | $keySched[$r][$t] = $tk[$j]; 91 | } 92 | if($t == 4) { 93 | $r++; 94 | $t = 0; 95 | } 96 | } 97 | } 98 | 99 | $this->rounds = $rounds; 100 | $this->rk = $keySched; 101 | return $this; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /libs/GPG/Public_Key.php: -------------------------------------------------------------------------------- 1 | version != -1 && $this->GetKeyType() != PK_TYPE_UNKNOWN; 32 | } 33 | 34 | function GetKeyType() 35 | { 36 | if (!strcmp($this->type, "ELGAMAL")) return PK_TYPE_ELGAMAL; 37 | if (!strcmp($this->type, "RSA")) return PK_TYPE_RSA; 38 | return PK_TYPE_UNKNOWN; 39 | } 40 | 41 | function GetFingerprint() 42 | { 43 | return strtoupper( trim(chunk_split($this->fp, 4, ' ')) ); 44 | } 45 | 46 | function GetKeyId() 47 | { 48 | return (strlen($this->key_id) == 16) ? strtoupper($this->key_id) : '0000000000000000'; 49 | } 50 | 51 | function GetPublicKey() 52 | { 53 | return str_replace("\n", "", $this->public_key); 54 | } 55 | 56 | function __construct($asc) { 57 | $found = 0; 58 | 59 | // normalize line breaks 60 | $asc = str_replace("\r\n", "\n", $asc); 61 | 62 | if (strpos($asc, "-----BEGIN PGP PUBLIC KEY BLOCK-----\n") === false) 63 | throw new Exception("Missing header block in Public Key"); 64 | 65 | if (strpos($asc, "\n\n") === false) 66 | throw new Exception("Missing body delimiter in Public Key"); 67 | 68 | if (strpos($asc, "\n-----END PGP PUBLIC KEY BLOCK-----") === false) 69 | throw new Exception("Missing footer block in Public Key"); 70 | 71 | // get rid of everything except the base64 encoded key 72 | $headerbody = explode("\n\n", str_replace("\n-----END PGP PUBLIC KEY BLOCK-----", "", $asc), 2); 73 | $asc = trim($headerbody[1]); 74 | 75 | 76 | $len = 0; 77 | $s = base64_decode($asc); 78 | $sa = str_split($s); 79 | 80 | for($i = 0; $i < strlen($s);) { 81 | $tag = ord($sa[$i++]); 82 | 83 | // echo 'TAG=' . $tag . '/'; 84 | 85 | if(($tag & 128) == 0) break; 86 | 87 | if($tag & 64) { 88 | $tag &= 63; 89 | $len = ord($sa[$i++]); 90 | if ($len > 191 && $len < 224) $len = (($len - 192) << 8) + ord($sa[$i++]); 91 | else if ($len == 255){ 92 | $oc1=isset($sa[++$i])?$sa[$i]:NULL; 93 | $oc2=isset($sa[++$i])?$sa[$i]:NULL; 94 | $oc3=isset($sa[++$i])?$sa[$i]:NULL; 95 | $oc4=isset($sa[++$i])?$sa[$i]:NULL; 96 | 97 | if($oc1!==NULL)$len = ord($oc1)* pow(2,24); 98 | else $len = (0* pow(2,24)); 99 | 100 | if($oc2!==NULL)$len += ord($oc2)* pow(2,16); 101 | else $len += (0* pow(2,16)); 102 | 103 | if($oc3!==NULL)$len += ord($oc3)* pow(2,8); 104 | else $len += (0* pow(2,8)); 105 | 106 | if($oc4!==NULL)$len += ord($oc4); 107 | else $len += (0); 108 | } 109 | // else if ($len == 255) $len = (ord($sa[$i++]) << 24) + (ord($sa[$i++]) << 16) + (ord($sa[$i++]) << 8) + ord($sa[$i++]); 110 | else if ($len > 223 && $len < 255) $len = (1 << ($len & 0x1f)); 111 | } else { 112 | $len = $tag & 3; 113 | $tag = ($tag >> 2) & 15; 114 | if ($len == 0) $len = ord($sa[$i++]); 115 | else if($len == 1) $len = (ord($sa[$i++]) << 8) + ord($sa[$i++]); 116 | else if($len == 2) { 117 | $oc1=isset($sa[++$i])?$sa[$i]:NULL; 118 | $oc2=isset($sa[++$i])?$sa[$i]:NULL; 119 | $oc3=isset($sa[++$i])?$sa[$i]:NULL; 120 | $oc4=isset($sa[++$i])?$sa[$i]:NULL; 121 | 122 | if($oc1!==NULL)$len = ord($oc1)* pow(2,24); 123 | else $len = (0* pow(2,24)); 124 | 125 | if($oc2!==NULL)$len += ord($oc2)* pow(2,16); 126 | else $len += (0* pow(2,16)); 127 | 128 | if($oc3!==NULL)$len += ord($oc3)* pow(2,8); 129 | else $len += (0* pow(2,8)); 130 | 131 | if($oc4!==NULL)$len += ord($oc4); 132 | else $len += (0); 133 | // $len = (ord($sa[$i++])* pow(2,24)) + (ord($sa[$i++]) * pow(2,16)) + (0 * pow(2,8)) + 0; 134 | } 135 | 136 | else $len = strlen($s) - 1; 137 | } 138 | 139 | // echo $tag . ' '; 140 | 141 | if ($tag == 6 || $tag == 14) { 142 | $k = $i; 143 | $version = ord($sa[$i++]); 144 | $found = 1; 145 | $this->version = $version; 146 | 147 | $time = (ord($sa[$i++]) << 24) + (ord($sa[$i++]) << 16) + (ord($sa[$i++]) << 8) + ord($sa[$i++]); 148 | 149 | if($version == 2 || $version == 3) $valid = ord($sa[$i++]) << 8 + ord($sa[$i++]); 150 | 151 | $algo = ord($sa[$i++]); 152 | 153 | if($algo == 1 || $algo == 2) { 154 | $m = $i; 155 | $lm = floor((ord($sa[$i]) * 256 + ord($sa[$i + 1]) + 7) / 8); 156 | $i += $lm + 2; 157 | 158 | $mod = substr($s, $m, $lm + 2); 159 | $le = floor((ord($sa[$i]) * 256 + ord($sa[$i+1]) + 7) / 8); 160 | $i += $le + 2; 161 | 162 | $this->public_key = base64_encode(substr($s, $m, $lm + $le + 4)); 163 | $this->type = "RSA"; 164 | 165 | if ($version == 3) { 166 | $this->fp = ''; 167 | $this->key_id = bin2hex(substr($mod, strlen($mod) - 8, 8)); 168 | } else if($version == 4) { 169 | 170 | // https://tools.ietf.org/html/rfc4880#section-12 171 | $headerPos = strpos($s, chr(0x04)); // TODO: is this always the correct starting point for the pulic key packet 'version' field? 172 | $delim = chr(0x01) . chr(0x00); // TODO: is this the correct delimiter for the end of the public key packet? 173 | $delimPos = strpos($s, $delim) + (3-$headerPos); 174 | 175 | // echo "POSITION: $delimPos\n"; 176 | 177 | // this does not work, tried it with RSA 1024 and RSA 4096 keys generated by GnuPG v2 (2.0.29) on Windows running Apache and PHP 5.6.3 178 | // $pkt = chr(0x99) . chr($delimPos >> 8) . chr($delimPos & 255) . substr($s, $headerPos, $delimPos); 179 | 180 | // this is the original signing string which seems to have only worked for key lengths of 1024 or less 181 | $pkt = chr(0x99) . chr($len >> 8) . chr($len & 255) . substr($s, $k, $len); // use this for now 182 | 183 | $fp = sha1($pkt); 184 | $this->fp = $fp; 185 | $this->key_id = substr($fp, strlen($fp) - 16, 16); 186 | 187 | // uncomment to debug the start point for the signing string 188 | // for ($ii = 5; $ii > -1; $ii--) { 189 | // $pkt = chr(0x99) . chr($ii >> 8) . chr($ii & 255) . substr($s, $headerPos, $ii); 190 | // $fp = sha1($pkt); 191 | // echo "LENGTH=" . $headerPos . '->' . $ii . " CHR(" . ord(substr($s,$ii, 1)) . ") = " . substr($fp, strlen($fp) - 16, 16) . "\n"; 192 | // } 193 | // echo "\n"; 194 | 195 | // uncomment to debug the end point for the signing string 196 | // for ($ii = strlen($s); $ii > 1; $ii--) { 197 | // $pkt = chr(0x99) . chr($ii >> 8) . chr($ii & 255) . substr($s, $headerPos, $ii); 198 | // $fp = sha1($pkt); 199 | // echo "LENGTH=" . $headerPos . '->' . $ii . " CHR(" . ord(substr($s,$ii, 1)) . ") = " . substr($fp, strlen($fp) - 16, 16) . "\n"; 200 | // } 201 | } else { 202 | throw new Exception('GPG Key Version ' . $version . ' is not supported'); 203 | } 204 | $found = 2; 205 | } else if(($algo == 16 || $algo == 20) && $version == 4) { 206 | $m = $i; 207 | 208 | $lp = floor((ord($sa[$i]) * 256 + ord($sa[$i +1]) + 7) / 8); 209 | $i += $lp + 2; 210 | 211 | $lg = floor((ord($sa[$i]) * 256 + ord($sa[$i + 1]) + 7) / 8); 212 | $i += $lg + 2; 213 | 214 | $ly = floor((ord($sa[$i]) * 256 + ord($sa[$i + 1]) + 7)/8); 215 | $i += $ly + 2; 216 | 217 | $this->public_key = base64_encode(substr($s, $m, $lp + $lg + $ly + 6)); 218 | 219 | // TODO: should this be adjusted as it was for RSA (above)..? 220 | 221 | $pkt = chr(0x99) . chr($len >> 8) . chr($len & 255) . substr($s, $k, $len); 222 | $fp = sha1($pkt); 223 | $this->fp = $fp; 224 | $this->key_id = substr($fp, strlen($fp) - 16, 16); 225 | $this->type = "ELGAMAL"; 226 | $found = 3; 227 | } else { 228 | $i = $k + $len; 229 | } 230 | } else if ($tag == 13) { 231 | $this->user = substr($s, $i, $len); 232 | $i += $len; 233 | } else { 234 | $i += $len; 235 | } 236 | } 237 | 238 | if($found < 2) { 239 | 240 | throw new Exception("Unable to parse Public Key"); 241 | // $this->version = ""; 242 | // $this->fp = ""; 243 | // $this->key_id = ""; 244 | // $this->user = ""; 245 | // $this->public_key = ""; 246 | } 247 | } 248 | 249 | function GetExpandedKey() 250 | { 251 | $ek = new Expanded_Key($this->public_key); 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /libs/GPG/Utility.php: -------------------------------------------------------------------------------- 1 | > 0x8) & 0xff); 21 | } 22 | 23 | static function B2($x) { 24 | return (($x >> 0x10) & 0xff); 25 | } 26 | 27 | static function B3($x) { 28 | return (($x >> 0x18) & 0xff); 29 | } 30 | 31 | static function zshift($x, $s) { 32 | $res = $x >> $s; 33 | 34 | $pad = 0; 35 | for ($i = 0; $i < 32 - $s; $i++) $pad += (1 << $i); 36 | 37 | return $res & $pad; 38 | } 39 | 40 | static function pack_octets($octets) 41 | { 42 | $i = 0; 43 | $j = 0; 44 | $len = count($octets); 45 | $b = array_fill(0, $len / 4, 0); 46 | 47 | if (!$octets || $len % 4) return; 48 | 49 | for ($i = 0, $j = 0; $j < $len; $j += 4) { 50 | $b[$i++] = $octets[$j] | ($octets[$j + 1] << 0x8) | ($octets[$j + 2] << 0x10) | ($octets[$j + 3] << 0x18); 51 | 52 | } 53 | 54 | return $b; 55 | } 56 | 57 | static function unpack_octets($packed) 58 | { 59 | $j = 0; 60 | $i = 0; 61 | $l = count($packed); 62 | $r = array_fill(0, $l * 4, 0); 63 | 64 | for ($j = 0; $j < $l; $j++) { 65 | $r[$i++] = GPG_Utility::B0($packed[$j]); 66 | $r[$i++] = GPG_Utility::B1($packed[$j]); 67 | $r[$i++] = GPG_Utility::B2($packed[$j]); 68 | $r[$i++] = GPG_Utility::B3($packed[$j]); 69 | } 70 | 71 | return $r; 72 | } 73 | 74 | 75 | 76 | 77 | static function hex2bin($h) 78 | { 79 | if(strlen($h) % 2) $h += "0"; 80 | 81 | $r = ""; 82 | for($i = 0; $i < strlen($h); $i += 2) { 83 | $r .= chr(intval($h[$i], 16) * 16 + intval($h[$i + 1], 16)); 84 | } 85 | 86 | return $r; 87 | } 88 | 89 | static function crc24($data) 90 | { 91 | $crc = 0xb704ce; 92 | 93 | for($n = 0; $n < strlen($data); $n++) { 94 | $crc ^= (ord($data[$n]) & 0xff) << 0x10; 95 | for($i = 0; $i < 8; $i++) { 96 | $crc <<= 1; 97 | if($crc & 0x1000000) $crc ^= 0x1864cfb; 98 | } 99 | } 100 | 101 | return 102 | chr(($crc >> 0x10) & 0xff) . 103 | chr(($crc >> 0x8) & 0xff) . 104 | chr($crc & 0xff); 105 | } 106 | 107 | static function s_random($len, $textmode) 108 | { 109 | $r = ""; 110 | for($i = 0; $i < $len;) 111 | { 112 | $t = random_int(0, 0xff); 113 | if($t == 0 && $textmode) continue; 114 | $i++; 115 | 116 | $r .= chr($t); 117 | } 118 | 119 | return $r; 120 | } 121 | 122 | static function c_random() { 123 | return random_int(0, 0xff); 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /libs/GPG/globals.php: -------------------------------------------------------------------------------- 1 | > 1; 16 | $bd = $bs >> 1; 17 | $bdm = (1 << $bd) - 1; 18 | 19 | /** 20 | */ 21 | function mpi2b($s) 22 | { 23 | global $bs; 24 | global $bx2; 25 | global $bm; 26 | global $bx; 27 | global $bd; 28 | global $bdm; 29 | 30 | $bn = 1; 31 | $r = array(0); 32 | $rn = 0; 33 | $sb = 256; 34 | $c = 0; 35 | $sn = strlen($s); 36 | if($sn < 2) { 37 | echo("string too short, not a MPI"); 38 | return 0; 39 | } 40 | 41 | $len = ($sn - 2) * 8; 42 | $bits = ord($s[0]) * 256 + ord($s[1]); 43 | if ($bits > $len || $bits < $len - 8) { 44 | echo("not a MPI, bits = $bits, len = $len"); 45 | return 0; 46 | } 47 | 48 | for ($n = 0; $n < $len; $n++) { 49 | if (($sb <<= 1) > 255) { 50 | $sb = 1; $c = ord($s[--$sn]); 51 | } 52 | if ($bn > $bm) { 53 | $bn = 1; 54 | $r[++$rn]=0; 55 | } 56 | if ($c & $sb) $r[$rn] |= $bn; 57 | $bn <<= 1; 58 | } 59 | 60 | return $r; 61 | } 62 | 63 | /** 64 | */ 65 | function b2mpi($b) 66 | { 67 | global $bs; 68 | global $bx2; 69 | global $bm; 70 | global $bx; 71 | global $bd; 72 | global $bdm; 73 | 74 | $bn = 1; 75 | $bc = 0; 76 | $r = array(0); 77 | $rb = 1; 78 | $rn = 0; 79 | $bits = count($b) * $bs; 80 | $n = 0; 81 | $rr = ""; 82 | 83 | for ($n = 0; $n < $bits; $n++) { 84 | if ($b[$bc] & $bn) $r[$rn] |= $rb; 85 | if(($rb <<= 1) > 255) { 86 | $rb = 1; $r[++$rn]=0; 87 | } 88 | if (($bn <<= 1) > $bm) { 89 | $bn=1; $bc++; 90 | } 91 | } 92 | 93 | while ($rn && $r[$rn]==0) $rn--; 94 | 95 | $bn=256; 96 | for($bits = 8; $bits > 0; $bits--) if ($r[$rn] & ($bn >>= 1)) break; 97 | $bits += $rn * 8; 98 | 99 | $rr .= chr($bits / 256 ) . chr($bits % 256); 100 | if ($bits) for($n = $rn; $n >= 0; $n--) $rr .= chr($r[$n]); 101 | 102 | return $rr; 103 | } 104 | 105 | /** 106 | */ 107 | function bmodexp($xx, $y, $m) { 108 | global $bs; 109 | global $bx2; 110 | global $bm; 111 | global $bx; 112 | global $bd; 113 | global $bdm; 114 | 115 | $r = array(1); 116 | $an = 0; 117 | $a = 0; 118 | $x = array_merge((array)$xx); 119 | $n = count($m) * 2; 120 | $mu = array_fill(0, $n + 1, 0); 121 | 122 | $mu[$n--] = 1; 123 | for(; $n >= 0; $n--) $mu[$n] = 0; 124 | $dd = new bdiv($mu, $m); 125 | $mu = $dd->q; 126 | 127 | for($n = 0; $n < count($y); $n++) { 128 | for ($a = 1, $an = 0; $an < $bs; $an++, $a <<= 1) { 129 | if ($y[$n] & $a) $r = bmod2(bmul($r, $x), $m, $mu); 130 | $x = bmod2(bmul($x, $x), $m, $mu); 131 | } 132 | } 133 | 134 | return $r; 135 | } 136 | 137 | /** 138 | */ 139 | function simplemod($i, $m) // returns the mod where m < 2^bd 140 | { 141 | $c = 0; 142 | $v = 0; 143 | for ($n = count($i) - 1; $n >= 0; $n--) 144 | { 145 | $v = $i[$n]; 146 | $c = (($v >> $bd) + ($c << $bd)) % $m; 147 | $c = (($v & $bdm) + ($c << $bd)) % $m; 148 | } 149 | 150 | return $c; 151 | } 152 | 153 | /** 154 | */ 155 | function bmod($p, $m) // binary modulo 156 | { 157 | global $bdm; 158 | 159 | if (count($m) == 1) { 160 | if(count($p) == 1) return array($p[0] % $m[0]); 161 | if($m[0] < $bdm) return array(simplemod($p, $m[0])); 162 | } 163 | 164 | $r = new bdiv($p, $m); 165 | return $r->mod; 166 | } 167 | 168 | /** 169 | */ 170 | function bmod2($x, $m, $mu) { 171 | $xl = count($x) - (count($m) << 1); 172 | if ($xl > 0) return bmod2(array_concat(array_slice($x, 0, $xl), bmod2(array_slice($x, $xl), $m, $mu)), $m, $mu); 173 | 174 | $ml1 = count($m) + 1; 175 | $ml2 = count($m) - 1; 176 | $rr = 0; 177 | 178 | $q3 = array_slice(bmul(array_slice($x, $ml2), $mu), $ml1); 179 | $r1 = array_slice($x, 0, $ml1); 180 | $r2 = array_slice(bmul($q3, $m), 0, $ml1); 181 | 182 | $r = bsub($r1, $r2); 183 | if (count($r) == 0) { 184 | $r1[$ml1] = 1; 185 | $r = bsub($r1, $r2); 186 | } 187 | for ($n = 0;; $n++) { 188 | $rr = bsub($r, $m); 189 | if(count($rr) == 0) break; 190 | $r = $rr; 191 | if($n >= 3) return bmod2($r, $m, $mu); 192 | } 193 | 194 | return $r; 195 | } 196 | 197 | /** 198 | */ 199 | function toppart($x, $start, $len) { 200 | global $bx2; 201 | 202 | $n = 0; 203 | while ($start >= 0 && $len-- > 0) $n = $n * $bx2 + $x[$start--]; 204 | 205 | return $n; 206 | } 207 | 208 | /** 209 | */ 210 | function zeros($n) { 211 | $r = array_fill(0, $n, 0); 212 | while ($n-- > 0) $r[$n] = 0; 213 | return $r; 214 | } 215 | 216 | /** 217 | * @package verysimple::Encryption 218 | */ 219 | class bdiv { 220 | var $q; 221 | var $mod; 222 | function __construct($x, $y) 223 | { 224 | global $bs; 225 | global $bx2; 226 | global $bm; 227 | global $bx; 228 | global $bd; 229 | global $bdm; 230 | 231 | $n = count($x) - 1; 232 | $t = count($y) - 1; 233 | $nmt = $n - $t; 234 | 235 | if ($n < $t || $n == $t && ($x[$n] < $y[$n] || $n > 0 && $x[$n] == $y[$n] && $x[$n - 1] < $y[$n - 1])) { 236 | $this->q = array(0); 237 | $this->mod = array($x); 238 | return; 239 | } 240 | 241 | if ($n == $t && toppart($x, $t, 2) / toppart($y, $t, 2) < 4) { 242 | $qq = 0; 243 | $xx = 0; 244 | for(;;) { 245 | $xx = bsub($x, $y); 246 | if(count($xx) == 0) break; 247 | $x = $xx; $qq++; 248 | } 249 | $this->q = array($qq); 250 | $this->mod = $x; 251 | return; 252 | } 253 | 254 | $shift2 = floor(log($y[$t]) / M_LN2) + 1; 255 | $shift = $bs - $shift2; 256 | if ($shift) { 257 | $x = array_merge((array)$x); $y = array_merge((array)$y); 258 | for($i = $t; $i > 0; $i--) $y[$i] = (($y[$i] << $shift) & $bm) | ($y[$i - 1] >> $shift2); 259 | $y[0] = ($y[0] << $shift) & $bm; 260 | if($x[$n] & (($bm << $shift2) & $bm)) { 261 | $x[++$n] = 0; $nmt++; 262 | } 263 | for($i = $n; $i > 0; $i--) $x[$i] = (($x[$i] << $shift) & $bm) | ($x[$i - 1] >> $shift2); 264 | $x[0] = ($x[0] << $shift) & $bm; 265 | } 266 | 267 | $i = 0; 268 | $j = 0; 269 | $x2 = 0; 270 | $q = zeros($nmt + 1); 271 | $y2 = array_merge(zeros($nmt), (array)$y); 272 | for (;;) { 273 | $x2 = bsub($x, $y2); 274 | if(count($x2) == 0) break; 275 | $q[$nmt]++; 276 | $x = $x2; 277 | } 278 | 279 | $yt = $y[$t]; 280 | $top =toppart($y, $t, 2); 281 | for ($i = $n; $i > $t; $i--) { 282 | $m = $i - $t - 1; 283 | if ($i >= count($x)) $q[$m] = 1; 284 | else if($x[$i] == $yt) $q[$m] = $bm; 285 | else $q[$m] = floor(toppart($x, $i, 2) / $yt); 286 | 287 | $topx = toppart($x, $i, 3); 288 | while ($q[$m] * $top > $topx) $q[$m]--; 289 | 290 | $y2 = array_slice($y2, 1); 291 | $x2 = bsub($x, bmul(array($q[$m]), $y2)); 292 | if (count($x2) == 0) { 293 | $q[$m]--; 294 | $x2 =bsub($x, bmul(array($q[$m]), $y2)); 295 | } 296 | $x = $x2; 297 | } 298 | 299 | if ($shift) { 300 | for($i = 0; $i < count($x) - 1; $i++) $x[$i] = ($x[$i] >> $shift) | (($x[$i + 1] << $shift2) & $bm); 301 | $x[count($x) - 1] >>= $shift; 302 | } 303 | $n = count($q); 304 | while ($n > 1 && $q[$n - 1] == 0) $n--; 305 | $this->q = array_slice($q, 0, $n); 306 | $n = count($x); 307 | while ($n > 1 && $x[$n - 1] == 0) $n--; 308 | $this->mod = array_slice($x, 0, $n); 309 | } 310 | } 311 | 312 | /** 313 | */ 314 | function bsub($a, $b) { 315 | global $bs; 316 | global $bx2; 317 | global $bm; 318 | global $bx; 319 | global $bd; 320 | global $bdm; 321 | 322 | $al = count($a); 323 | $bl = count($b); 324 | 325 | if ($bl > $al) return array(); 326 | if ($bl == $al) { 327 | if($b[$bl - 1] > $a[$bl - 1]) return array(); 328 | if($bl == 1) return array($a[0] - $b[0]); 329 | } 330 | 331 | $r = array_fill(0, $al, 0); 332 | $c = 0; 333 | 334 | for ($n = 0; $n < $bl; $n++) { 335 | $c += $a[$n] - $b[$n]; 336 | $r[$n] = $c & $bm; 337 | $c >>= $bs; 338 | } 339 | for (; $n < $al; $n++) { 340 | $c += $a[$n]; 341 | $r[$n] = $c & $bm; 342 | $c >>= $bs; 343 | } 344 | if ($c) return array(); 345 | 346 | if ($r[$n - 1]) return $r; 347 | while ($n > 1 && $r[$n - 1] == 0) $n--; 348 | 349 | return array_slice($r, 0, $n); 350 | } 351 | 352 | /** 353 | */ 354 | function bmul($a, $b) { 355 | global $bs; 356 | global $bx2; 357 | global $bm; 358 | global $bx; 359 | global $bd; 360 | global $bdm; 361 | 362 | $b = array_merge((array)$b, array(0)); 363 | $al = count($a); 364 | $bl = count($b); 365 | $n = 0; 366 | $nn = 0; 367 | $aa = 0; 368 | $c = 0; 369 | $m = 0; 370 | $g = 0; 371 | $gg = 0; 372 | $h = 0; 373 | $hh = 0; 374 | $ghh = 0; 375 | $ghhb = 0; 376 | 377 | $r = zeros($al + $bl + 1); 378 | 379 | for ($n = 0; $n < $al; $n++) { 380 | $aa = $a[$n]; 381 | if ($aa) { 382 | $c = 0; 383 | $hh = $aa >> $bd; $h = $aa & $bdm; 384 | $m = $n; 385 | for ($nn = 0; $nn < $bl; $nn++, $m++) { 386 | $g = $b[$nn]; $gg = $g >> $bd; $g = $g & $bdm; 387 | $ghh = $g * $hh + $h * $gg; 388 | $ghhb = $ghh >> $bd; $ghh &= $bdm; 389 | $c += $r[$m] + $h * $g + ($ghh << $bd); 390 | $r[$m] = $c & $bm; 391 | $c = ($c >> $bs) + $gg * $hh + $ghhb; 392 | } 393 | } 394 | } 395 | $n = count($r); 396 | 397 | if ($r[$n - 1]) return $r; 398 | while ($n > 1 && $r[$n - 1] == 0) $n--; 399 | 400 | return array_slice($r, 0, $n); 401 | } 402 | 403 | function safeStrlen($string) { 404 | if (function_exists('mb_strlen')) { 405 | return mb_strlen($string, '8bit'); 406 | } 407 | return strlen($string); 408 | } 409 | -------------------------------------------------------------------------------- /tests/gpg/EncryptTest.php: -------------------------------------------------------------------------------- 1 | getTestKey(); 113 | 114 | // plain text message 115 | $plain_text_string = "Whatever 90's tote bag, meggings put a bird on it cray bicycle rights vinyl semiotics Wes Anderson. Selvage Austin umami, letterpress Tumblr deep v kitsch polaroid. Trust fund messenger bag sartorial gluten-free, cred cray church-key pop-up Intelligentsia. Food truck Tumblr paleo mixtape XOXO banjo PBR&B Pinterest tofu banh mi. Portland messenger bag cornhole PBR Tonx High Life, DIY pork belly bespoke hoodie Terry Richardson dreamcatcher ethical forage. Put a bird on it slow-carb mixtape cardigan craft beer messenger bag. Aesthetic twee art party, Odd Future trust fund banjo ugh small batch semiotics. 116 | 117 | Whatever asymmetrical keffiyeh literally narwhal. Keytar Odd Future blog, wayfarers literally gluten-free beard. Authentic Cosby sweater sustainable hashtag, VHS food truck kogi seitan put a bird on it YOLO. Selvage tousled mustache, flannel craft beer try-hard McSweeney's literally four loko YOLO keytar beard synth forage. Salvia Schlitz narwhal Terry Richardson typewriter, Wes Anderson butcher wolf. Slow-carb whatever bitters, letterpress trust fund pug before they sold out food truck artisan tousled. Church-key Vice craft beer Wes Anderson artisan flexitarian, kogi YOLO hella Tonx chia Neutra. 118 | 119 | Farm-to-table actually Portland, artisan shabby chic vinyl organic seitan roof party distillery. Street art PBR&B banh mi, Tonx authentic you probably haven't heard of them fixie whatever tofu gluten-free. Gentrify locavore lo-fi umami, Thundercats salvia wolf four loko. Mixtape messenger bag gluten-free, squid American Apparel hella Shoreditch whatever selfies sriracha before they sold out. Pickled farm-to-table Intelligentsia occupy. Tumblr Etsy farm-to-table, mlkshk hella shabby chic meh jean shorts dreamcatcher fashion axe trust fund lomo Neutra. Freegan vegan narwhal tousled hoodie wolf flexitarian. 120 | 121 | Flannel sriracha XOXO, slow-carb Godard ennui tousled American Apparel street art drinking vinegar lo-fi blog. Whatever Intelligentsia cardigan, Pinterest PBR&B pop-up semiotics. Jean shorts chillwave semiotics biodiesel. McSweeney's fap cardigan messenger bag fanny pack Cosby sweater Odd Future, Pitchfork four loko Marfa keytar mlkshk. 3 wolf moon McSweeney's gluten-free, umami freegan biodiesel fingerstache aesthetic sriracha swag Echo Park. Shabby chic selfies fixie, art party XOXO four loko chambray post-ironic letterpress messenger bag. Mustache beard lo-fi, flexitarian artisan tofu freegan occupy kale chips Carles twee chia bespoke."; 122 | 123 | $gpg = new GPG(); 124 | $pub_key = new GPG_Public_Key($public_key_ascii); 125 | $encrypted = $gpg->encrypt($pub_key,$plain_text_string); 126 | 127 | $this->assertContains('-----BEGIN PGP MESSAGE-----', $encrypted, 'PGP Header Expected'); 128 | 129 | $this->assertContains('-----END PGP MESSAGE-----', $encrypted, 'PGP Footer Expected'); 130 | 131 | } 132 | 133 | } 134 | 135 | ?> -------------------------------------------------------------------------------- /tests/gpg/KeyTest.php: -------------------------------------------------------------------------------- 1 | getGnuPGTestKey(); 1413 | 1414 | $gpg = new GPG(); 1415 | $pub_key = new GPG_Public_Key($public_key_ascii); 1416 | 1417 | $this->assertEquals(PK_TYPE_RSA,$pub_key->GetKeyType(),'OpenPGP Incorrect Key Type'); 1418 | $this->assertEquals('47009B66424E9476',$pub_key->GetKeyId(),'OpenPGP Incorrect Key ID'); 1419 | $this->assertEquals('ED4F E89E 38A3 7833 3CD4 D6FA 4700 9B66 424E 9476',$pub_key->GetFingerprint(),'OpenPGP Incorrect Fingerprint'); 1420 | 1421 | } 1422 | 1423 | /** 1424 | * Test key ID 1425 | */ 1426 | function test_VerifyOpenPGPKey1() 1427 | { 1428 | // OpenPGP Test Key 1429 | $public_key_ascii = $this->getOpenPGPTestKey1(); 1430 | 1431 | $gpg = new GPG(); 1432 | $pub_key = new GPG_Public_Key($public_key_ascii); 1433 | 1434 | $this->assertEquals(PK_TYPE_RSA,$pub_key->GetKeyType(),'OpenPGP Incorrect Key Type'); 1435 | $this->assertEquals('8DCE498F6091DFD6',$pub_key->GetKeyId(),'OpenPGP Incorrect Key ID'); 1436 | $this->assertEquals('C893 35AC EDF1 6046 7534 B25E 8DCE 498F 6091 DFD6',$pub_key->GetFingerprint(),'OpenPGP Incorrect Fingerprint'); 1437 | 1438 | } 1439 | 1440 | /** 1441 | * Test key ID 1442 | */ 1443 | function test_VerifyOpenPGPKey2() 1444 | { 1445 | // OpenPGP Test Key 1446 | $public_key_ascii = $this->getOpenPGPTestKey2(); 1447 | 1448 | $gpg = new GPG(); 1449 | $pub_key = new GPG_Public_Key($public_key_ascii); 1450 | 1451 | $this->assertEquals(PK_TYPE_RSA,$pub_key->GetKeyType(),'OpenPGP Incorrect Key Type'); 1452 | $this->assertEquals('C87538697986219A',$pub_key->GetKeyId(),'OpenPGP Incorrect Key ID'); 1453 | $this->assertEquals('3C05 9D07 C624 84A4 EF2D 3651 C875 3869 7986 219A',$pub_key->GetFingerprint(),'OpenPGP Incorrect Fingerprint'); 1454 | 1455 | } 1456 | 1457 | function test_VerifyGnuPGDSAKey() 1458 | { 1459 | // OpenPGP Test Key 1460 | $public_key_ascii = $this->getGnuPGDSAKey(); 1461 | 1462 | $gpg = new GPG(); 1463 | $pub_key = new GPG_Public_Key($public_key_ascii); 1464 | 1465 | $this->assertEquals(PK_TYPE_ELGAMAL,$pub_key->GetKeyType(),'OpenPGP Incorrect Key Type'); 1466 | $this->assertEquals('76D78F0500D026C4',$pub_key->GetKeyId(),'OpenPGP Incorrect Key ID'); 1467 | $this->assertEquals('85E3 8F69 046B 44C1 EC9F B07B 76D7 8F05 00D0 26C4',$pub_key->GetFingerprint(),'OpenPGP Incorrect Fingerprint'); 1468 | 1469 | 1470 | } 1471 | 1472 | } 1473 | 1474 | ?> -------------------------------------------------------------------------------- /tests/runtests.sh: -------------------------------------------------------------------------------- 1 | ## 2 | # Test runner for Phreeze 3 | ## 4 | phpunit gpg/ --------------------------------------------------------------------------------