Improved and bugfix handling of GnuPG for #89

This commit is contained in:
the-djmaze 2022-01-24 13:01:37 +01:00
parent 51fedab4fc
commit 1d3673f117
3 changed files with 461 additions and 306 deletions

View file

@ -25,10 +25,10 @@ trait Pgp
*/
if (80 < \strlen($homedir)) {
// First try a symbolic link
$link = \sys_get_temp_dir() . '/snappymail';
if (\is_dir($link) || \mkdir($link, 0700, true)) {
$tmpdir = \sys_get_temp_dir() . '/snappymail';
if (\is_dir($tmpdir) || \mkdir($tmpdir, 0700, true)) {
$link = $tmpdir . '/' . \md5($homedir);
if (\is_link($homedir) || \symlink($homedir, $link)) {
if (\is_link($link) || \symlink($homedir, $link)) {
$homedir = $link;
}
}
@ -49,50 +49,9 @@ trait Pgp
public function DoGnupgGetKeys() : array
{
$GPG = $this->GnuPG();
if ($GPG) {
$keys = [];
/**
* PECL GnuPG can't list private
*
* gpg --list-secret-keys
* gpg --list-public-keys
*/
foreach ($GPG->keyInfo('') as $info) {
if (!$info['disabled'] && !$info['expired'] && !$info['revoked']) {
$info['can_verify'] = $info['can_sign'];
$info['can_sign'] = $info['can_decrypt'] = false;
foreach ($info['subkeys'] as $key) {
$hasKey = $GPG->hasPrivateKey($key['keygrip']);
$info['can_sign'] = $info['can_sign'] || ($info['can_verify'] && $hasKey);
$info['can_decrypt'] = $info['can_decrypt'] || ($info['can_encrypt'] && $hasKey);
}
foreach ($info['uids'] as $uid) {
$id = $uid['email'];
if (isset($keys[$id])) {
// Public Key tasks
$keys[$id]['can_verify'] = $keys[$id]['can_verify'] || $info['can_verify'];
$keys[$id]['can_encrypt'] = $keys[$id]['can_encrypt'] || $info['can_encrypt'];
// Private Key tasks
$keys[$id]['can_sign'] = $keys[$id]['can_sign'] || $info['can_sign'];
$keys[$id]['can_decrypt'] = $keys[$id]['can_decrypt'] || $info['can_decrypt'];
} else {
$keys[$id] = [
'name' => $uid['name'],
'email' => $uid['email'],
// Public Key tasks
'can_verify' => $info['can_sign'],
'can_encrypt' => $info['can_encrypt'],
// Private Key tasks
'can_sign' => $info['can_sign'],
'can_decrypt' => $info['can_decrypt']
];
}
}
}
}
return $this->DefaultResponse(__FUNCTION__, $keys);
}
return $this->FalseResponse(__FUNCTION__);
return $GPG
? $this->DefaultResponse(__FUNCTION__, $GPG->keyInfo(''))
: $this->FalseResponse(__FUNCTION__);
}
public function DoGnupgImportKey() : array

View file

@ -8,6 +8,8 @@ class GnuPG
$homedir,
// Instance of gnupg pecl extension
$GnuPG,
// Instance of \SnappyMail\PGP\GPG
$GPG,
// Instance of PEAR Crypt_GPG
$Crypt_GPG;
@ -38,6 +40,9 @@ class GnuPG
]);
// Output is ASCII
$self->GnuPG->setarmor(1);
} else if (\SnappyMail\PGP\GPG::isSupported()) {
$self = new self;
$self->GPG = new \SnappyMail\PGP\GPG($homedir);
} else {
/**
* $binary = trim(`which gpg`) ?: trim(`which gpg2`);
@ -68,6 +73,9 @@ class GnuPG
if ($this->GnuPG) {
return $this->GnuPG->adddecryptkey($fingerprint, $passphrase);
}
if ($this->GPG) {
return $this->GPG->adddecryptkey($fingerprint, $passphrase);
}
if ($this->Crypt_GPG) {
$this->Crypt_GPG->addDecryptKey($fingerprint, $passphrase);
return true;
@ -83,6 +91,9 @@ class GnuPG
if ($this->GnuPG) {
return $this->GnuPG->addencryptkey($fingerprint);
}
if ($this->GPG) {
return $this->GPG->addencryptkey($fingerprint);
}
if ($this->Crypt_GPG) {
$this->Crypt_GPG->addEncryptKey($fingerprint);
return true;
@ -98,6 +109,9 @@ class GnuPG
if ($this->GnuPG) {
return $this->GnuPG->addsignkey($fingerprint, $passphrase);
}
if ($this->GPG) {
return $this->GPG->addsignkey($fingerprint, $passphrase);
}
if ($this->Crypt_GPG) {
$this->Crypt_GPG->addSignKey($fingerprint, $passphrase);
return true;
@ -113,6 +127,9 @@ class GnuPG
if ($this->GnuPG) {
return $this->GnuPG->cleardecryptkeys();
}
if ($this->GPG) {
return $this->GPG->cleardecryptkeys();
}
if ($this->Crypt_GPG) {
$this->Crypt_GPG->clearDecryptKeys();
return true;
@ -128,6 +145,9 @@ class GnuPG
if ($this->GnuPG) {
return $this->GnuPG->clearencryptkeys();
}
if ($this->GPG) {
return $this->GPG->clearencryptkeys();
}
if ($this->Crypt_GPG) {
$this->Crypt_GPG->clearEncryptKeys();
return true;
@ -143,6 +163,9 @@ class GnuPG
if ($this->GnuPG) {
return $this->GnuPG->clearsignkeys();
}
if ($this->GPG) {
return $this->GPG->clearsignkeys();
}
if ($this->Crypt_GPG) {
$this->Crypt_GPG->clearSignKeys();
return true;
@ -158,6 +181,9 @@ class GnuPG
if ($this->GnuPG) {
return $this->GnuPG->decrypt($text);
}
if ($this->GPG) {
return $this->GPG->decrypt($text);
}
if ($this->Crypt_GPG) {
return $this->Crypt_GPG->decrypt($encryptedData);
}
@ -172,6 +198,9 @@ class GnuPG
if ($this->GnuPG) {
return $this->GnuPG->decrypt(\file_get_contents($filename));
}
if ($this->GPG) {
return $this->GPG->decryptFile($filename);
}
if ($this->Crypt_GPG) {
return $this->Crypt_GPG->decryptFile($filename, $decryptedFile = null);
}
@ -186,6 +215,9 @@ class GnuPG
if ($this->GnuPG) {
return $this->GnuPG->decryptverify($text, $plaintext);
}
if ($this->GPG) {
return $this->GPG->decryptverify($text, $plaintext);
}
if ($this->Crypt_GPG) {
return $this->Crypt_GPG->decryptAndVerify($text, $ignoreVerifyErrors = false);
}
@ -200,6 +232,9 @@ class GnuPG
if ($this->GnuPG) {
return $this->GnuPG->decryptverify(\file_get_contents($filename), $plaintext);
}
if ($this->GPG) {
return $this->GPG->decryptverifyFile($filename, $plaintext);
}
if ($this->Crypt_GPG) {
return $this->Crypt_GPG->decryptAndVerifyFile($filename, $decryptedFile = null, $ignoreVerifyErrors = false);
}
@ -214,6 +249,9 @@ class GnuPG
if ($this->GnuPG) {
return $this->GnuPG->encrypt($plaintext);
}
if ($this->GPG) {
return $this->GPG->encrypt($plaintext);
}
if ($this->Crypt_GPG) {
return $this->Crypt_GPG->encrypt($plaintext);
}
@ -228,6 +266,9 @@ class GnuPG
if ($this->GnuPG) {
return $this->GnuPG->encrypt(\file_get_contents($filename));
}
if ($this->GPG) {
return $this->GPG->encryptFile($filename);
}
if ($this->Crypt_GPG) {
return $this->Crypt_GPG->encryptFile($filename, $encryptedFile = null);
}
@ -242,6 +283,9 @@ class GnuPG
if ($this->GnuPG) {
return $this->GnuPG->encryptsign($plaintext);
}
if ($this->GPG) {
return $this->GPG->encryptsign($plaintext);
}
if ($this->Crypt_GPG) {
return $this->Crypt_GPG->encryptAndSign($plaintext);
}
@ -256,6 +300,9 @@ class GnuPG
if ($this->GnuPG) {
return $this->GnuPG->encryptsign(\file_get_contents($filename));
}
if ($this->GPG) {
return $this->GPG->encryptsignFile($filename);
}
if ($this->Crypt_GPG) {
return $this->Crypt_GPG->encryptAndSignFile($filename, $signedFile = null);
}
@ -270,6 +317,9 @@ class GnuPG
if ($this->GnuPG) {
return $this->GnuPG->export($fingerprint);
}
if ($this->GPG) {
return $this->GPG->export($fingerprint);
}
if ($this->Crypt_GPG) {
$this->Crypt_GPG->exportPrivateKey($fingerprint, $armor = true);
$this->Crypt_GPG->exportPublicKey($fingerprint, $armor = true);
@ -286,6 +336,9 @@ class GnuPG
if ($this->GnuPG) {
return $this->GnuPG->getengineinfo();
}
if ($this->GPG) {
return $this->GPG->getengineinfo();
}
if ($this->Crypt_GPG) {
return [
'protocol' => null,
@ -305,6 +358,9 @@ class GnuPG
if ($this->GnuPG) {
return $this->GnuPG->geterror();
}
if ($this->GPG) {
return $this->GPG->geterror();
}
if ($this->Crypt_GPG) {
return true;
}
@ -319,6 +375,9 @@ class GnuPG
if ($this->GnuPG) {
return $this->GnuPG->geterrorinfo();
}
if ($this->GPG) {
return $this->GPG->geterrorinfo();
}
if ($this->Crypt_GPG) {
return true;
}
@ -333,6 +392,9 @@ class GnuPG
if ($this->GnuPG) {
return $this->GnuPG->getprotocol();
}
if ($this->GPG) {
return $this->GPG->getprotocol();
}
if ($this->Crypt_GPG) {
return true;
}
@ -347,6 +409,9 @@ class GnuPG
if ($this->GnuPG) {
return $this->GnuPG->import($keydata);
}
if ($this->GPG) {
return $this->GPG->import($keydata);
}
if ($this->Crypt_GPG) {
return $this->Crypt_GPG->importKey($keydata);
}
@ -361,6 +426,9 @@ class GnuPG
if ($this->GnuPG) {
return $this->GnuPG->import(\file_get_contents($filename));
}
if ($this->GPG) {
return $this->GPG->importFile($filename);
}
if ($this->Crypt_GPG) {
return $this->Crypt_GPG->importKeyFile($filename);
}
@ -372,32 +440,65 @@ class GnuPG
*/
public function keyInfo(string $pattern) : array
{
if ($this->GnuPG) {
return $this->GnuPG->keyinfo($pattern);
/* // v1.5 Slow and fails
return \array_merge(
$keys = [];
$GPG = $this->GnuPG ?: $this->GPG;
if ($GPG) {
// Public
$this->GnuPG->keyinfo($pattern),
foreach ($GPG->keyinfo($pattern) as $info) {
if (!$info['disabled'] && !$info['expired'] && !$info['revoked']) {
/*
$hasPrivateKey = false;
foreach ($info['subkeys'] as $key) {
$hasPrivateKey |= \is_file("{$this->homedir}/private-keys-v1.d/{$key['keygrip']}.key");
}
*/
foreach ($info['uids'] as $uid) {
$id = $uid['email'];
if (isset($keys[$id])) {
$keys[$id]['can_sign'] = $keys[$id]['can_sign'] || $info['can_sign'];
$keys[$id]['can_encrypt'] = $keys[$id]['can_encrypt'] || $info['can_encrypt'];
} else {
$keys[$id] = [
'name' => $uid['name'],
'email' => $uid['email'],
// Public Key tasks
'can_verify' => $info['can_sign'],
'can_encrypt' => $info['can_encrypt'],
// Private Key tasks
'can_sign' => false,
'can_decrypt' => false
];
}
}
}
}
// Private, read https://github.com/php-gnupg/php-gnupg/issues/5
$this->GnuPG->keyinfo($pattern, 1)
);
*/
foreach ($GPG->keyinfo($pattern, 1) as $info) {
if (!$info['disabled'] && !$info['expired'] && !$info['revoked']) {
foreach ($info['uids'] as $uid) {
$id = $uid['email'];
if (isset($keys[$id])) {
$keys[$id]['can_sign'] = $keys[$id]['can_sign'] || $info['can_sign'];
$keys[$id]['can_decrypt'] = $keys[$id]['can_decrypt'] || $info['can_encrypt'];
} else {
$keys[$id] = [
'name' => $uid['name'],
'email' => $uid['email'],
// Public Key tasks
'can_verify' => false,
'can_encrypt' => false,
// Private Key tasks
'can_sign' => $info['can_sign'],
'can_decrypt' => $info['can_encrypt']
];
}
if ($this->Crypt_GPG) {
return true;
}
return false;
}
/**
* Returns an array with information about all keys that matches the given pattern
*/
public function hasPrivateKey(string $keygrip) : bool
{
if ($this->GnuPG || $this->Crypt_GPG) {
return \is_file("{$this->homedir}/private-keys-v1.d/{$keygrip}.key");
}
return false;
}
else if ($this->Crypt_GPG) {
}
return $keys;
}
/**
@ -409,6 +510,9 @@ class GnuPG
if ($this->GnuPG) {
return $this->GnuPG->setarmor($armor ? 1 : 0);
}
if ($this->GPG) {
return $this->GPG->setarmor($armor ? 1 : 0);
}
if ($this->Crypt_GPG) {
//$armor ? \Crypt_GPG::ARMOR_ASCII : \Crypt_GPG::ARMOR_
return true;
@ -426,6 +530,9 @@ class GnuPG
if ($this->GnuPG) {
$this->GnuPG->seterrormode($errormode);
}
if ($this->GPG) {
$this->GPG->seterrormode($errormode);
}
if ($this->Crypt_GPG) {
}
}
@ -440,6 +547,9 @@ class GnuPG
if ($this->GnuPG) {
return $this->GnuPG->setsignmode($signmode);
}
if ($this->GPG) {
return $this->GPG->setsignmode($signmode);
}
if ($this->Crypt_GPG) {
return true;
}
@ -454,6 +564,9 @@ class GnuPG
if ($this->GnuPG) {
return $this->GnuPG->sign($plaintext);
}
if ($this->GPG) {
return $this->GPG->sign($plaintext);
}
if ($this->Crypt_GPG) {
return $this->Crypt_GPG->sign($data, $mode = self::SIGN_MODE_NORMAL);
}
@ -468,6 +581,9 @@ class GnuPG
if ($this->GnuPG) {
return $this->GnuPG->sign(\file_get_contents($filename));
}
if ($this->GPG) {
return $this->GPG->signFile($filename);
}
if ($this->Crypt_GPG) {
return $this->Crypt_GPG->signFile($filename, $signedFile = null, $mode = self::SIGN_MODE_NORMAL);
}
@ -482,6 +598,9 @@ class GnuPG
if ($this->GnuPG) {
return $this->GnuPG->verify($signed_text, $signature, $plaintext);
}
if ($this->GPG) {
return $this->GPG->verify($signed_text, $signature, $plaintext);
}
if ($this->Crypt_GPG) {
return $this->Crypt_GPG->verify($signed_text, $signature = '');
}
@ -496,6 +615,9 @@ class GnuPG
if ($this->GnuPG) {
return $this->GnuPG->verify(\file_get_contents($filename), $signature, $plaintext);
}
if ($this->GPG) {
return $this->GPG->verifyFile($filename, $signature, $plaintext);
}
if ($this->Crypt_GPG) {
return $this->Crypt_GPG->verifyFile($filename, $signature = '');
}

View file

@ -1,19 +1,8 @@
<?php
/**
# gpg1 --version
gpg (GnuPG) 1.4.23
Pubkey: RSA, RSA-E, RSA-S, ELG-E, DSA
Cipher: IDEA, 3DES, CAST5, BLOWFISH, AES, AES192, AES256, TWOFISH, CAMELLIA128, CAMELLIA192, CAMELLIA256
Hash: MD5, SHA1, RIPEMD160, SHA256, SHA384, SHA512, SHA224
Compression: Uncompressed, ZIP, ZLIB, BZIP2
# gpg2 --version
gpg (GnuPG) 2.2.20
libgcrypt 1.8.5
Pubkey: RSA, ELG, DSA, ECDH, ECDSA, EDDSA
Cipher: IDEA, 3DES, CAST5, BLOWFISH, AES, AES192, AES256, TWOFISH, CAMELLIA128, CAMELLIA192, CAMELLIA256
Hash: SHA1, RIPEMD160, SHA256, SHA384, SHA512, SHA224
Compression: Uncompressed, ZIP, ZLIB, BZIP2
* This class is inspired by PEAR Crypt_GPG and PECL gnupg
* It does not support gpg v1 because that is missing ECDH, ECDSA, EDDSA
* It does not support gpg < v2.2.5 as they are from before 2018
*/
namespace SnappyMail\PGP;
@ -53,13 +42,14 @@ class GPG
$debug = false;
private
$_processHandler = null,
$_errorHandlers = array(),
$_statusHandlers = array(),
$_commandBuffer,
$_message,
$_input,
$_output;
$_output,
$signKeys = array(),
$encryptKeys = array(),
$decryptKeys = array();
private
$binary,
@ -149,7 +139,8 @@ class GPG
*/
public function addDecryptKey(string $fingerprint, string $passphrase) : bool
{
return false;
$this->signKeys[$fingerprint] = $passphrase;
return true;
}
/**
@ -157,7 +148,8 @@ class GPG
*/
public function addEncryptKey(string $fingerprint) : bool
{
return false;
$this->encryptKeys[$fingerprint] = 1;
return true;
}
/**
@ -165,6 +157,7 @@ class GPG
*/
public function addSignKey(string $fingerprint, ?string $passphrase) : bool
{
$this->decryptKeys[$fingerprint] = $passphrase;
return false;
}
@ -173,7 +166,8 @@ class GPG
*/
public function clearDecryptKeys() : bool
{
return false;
$this->decryptKeys = [];
return true;
}
/**
@ -181,7 +175,8 @@ class GPG
*/
public function clearEncryptKeys() : bool
{
return false;
$this->encryptKeys = [];
return true;
}
/**
@ -189,7 +184,8 @@ class GPG
*/
public function clearSignKeys() : bool
{
return false;
$this->signKeys = [];
return true;
}
/**
@ -301,12 +297,59 @@ class GPG
return false;
}
public function addPassphrase($key, $passphrase)
{
$this->passphrases[$key] = $passphrase;
return $this;
}
protected function _importKey($input) /*: array|false*/
{
$arguments = array('--import');
$envKeys = array();
if (empty($this->passphrases)) {
$arguments[] = '--batch';
} else {
foreach ($this->passphrases as $keyId => $key) {
$envKeys[$keyId] = \is_array($key) ? $key['passphrase'] : $key;
}
}
$_ENV['PINENTRY_USER_DATA'] = \json_encode($envKeys);
$this->reset();
$this->setInput($input);
$result = $this->exec($arguments);
foreach ($result['status'] as $line) {
if (\strpos($line, 'IMPORT_RES')) {
$line = \explode(' ', \explode('IMPORT_RES ', $line)[1]);
return [
'imported' => (int) $line[2],
'unchanged' => (int) $line[4],
'newuserids' => (int) $line[5],
'newsubkeys' => (int) $line[6],
'secretimported' => (int) $line[10],
'secretunchanged' => (int) $line[11],
'newsignatures' => (int) $line[7],
'skippedkeys' => (int) $line[12],
'fingerprint' => ''
];
}
}
return false;
}
/**
* Imports a key
*/
public function import(string $keydata) /*: array|false*/
{
return false;
if (!$keydata) {
throw new \Exception('No valid input data found.');
}
return $this->_importKey($keydata);
}
/**
@ -314,104 +357,48 @@ class GPG
*/
public function importFile(string $filename) /*: array|false*/
{
$fp = \fopen($filename, 'rb');
try {
if (!$fp) {
throw new \Exception("Could not open file '{$filename}'");
}
return $this->_importKey($fp);
} finally {
$fp && \fclose($fp);
}
}
public function deleteKey(string $keyId, bool $private)
{
$key = $this->keyInfo($keyId, $private ? 1 : 0);
if (!$key) {
return false;
// throw new \Exception(($private ? 'Private' : 'Public') . ' key not found: ' . $keyId);
}
if (!$private && $this->keyInfo($keyId, 1)) {
return false;
throw new \Exception('Delete private key first: ' . $keyId);
}
$this->reset();
$result = $this->exec(array(
'--batch',
'--yes',
$private ? '--delete-secret-key' : '--delete-key',
\escapeshellarg($key[0]['subkeys'][0]['fingerprint'])
));
// $result['status'][0] = '[GNUPG:] ERROR keylist.getkey 17'
// $result['errors'][0] = 'gpg: error reading key: No public key'
// print_r($result);
return true;
}
/**
* Returns an array with information about all keys that matches the given pattern
*/
public function keyInfo(string $pattern) : array
{
return false;
}
/**
* Returns an array with information about all keys that matches the given pattern
*/
public function hasPrivateKey(string $keygrip) : bool
{
return \is_file("{$this->options['homedir']}/private-keys-v1.d/{$keygrip}.key");
}
/**
* Toggle armored output
* When true the output is ASCII
*/
public function setArmor(bool $armor = true) : bool
{
return false;
}
/**
* Sets the mode for error_reporting
* GNUPG_ERROR_WARNING, GNUPG_ERROR_EXCEPTION and GNUPG_ERROR_SILENT.
* By default GNUPG_ERROR_SILENT is used.
*/
public function setErrorMode(int $errormode) : void
{
}
/**
* Sets the mode for signing
* GNUPG_SIG_MODE_NORMAL, GNUPG_SIG_MODE_DETACH and GNUPG_SIG_MODE_CLEAR.
* By default GNUPG_SIG_MODE_CLEAR
*/
public function setSignMode(int $signmode) : bool
{
return false;
}
/**
* Signs a given text
*/
public function sign(string $plaintext) /*: string|false*/
{
return false;
}
/**
* Signs a given file
*/
public function signFile(string $filename) /*: string|false*/
{
return false;
}
/**
* Verifies a signed text
*/
public function verify(string $signed_text, string $signature, string &$plaintext = null) /*: array|false*/
{
return false;
}
/**
* Verifies a signed file
*/
public function verifyFile(string $filename, string $signature, string &$plaintext = null) /*: array|false*/
{
return false;
}
/**
* RFC 4880
* https://datatracker.ietf.org/doc/html/rfc4880#section-5.2.3.5
*/
public function signatureIssuer(string $signature) /*: array|false*/
{
if (preg_match('/-----BEGIN PGP SIGNATURE-----(.+)-----END PGP SIGNATURE-----/', $signature, $match)) {
// TODO: use https://github.com/singpolyma/openpgp-php ?
$binary = \base64_decode(\trim($match[1]));
return \strtoupper(\bin2hex(\substr($binary, 24, 8)));
}
return false;
}
/**
* This hangs with:
* gpg: can't connect to the agent: IPC connect call failed
*/
public function getPrivateKeys($keyId = '') : array
public function keyInfo(string $pattern, int $private = 0) : array
{
// According to The file 'doc/DETAILS' in the GnuPG distribution, using
// double '--with-fingerprint' also prints the fingerprint for subkeys.
@ -420,61 +407,21 @@ class GPG
'--with-fingerprint',
'--with-fingerprint',
'--fixed-list-mode',
'--list-secret-keys'
$private ? '--list-secret-keys' : '--list-public-keys'
);
if ($keyId) {
if ($pattern) {
$arguments[] = '--utf8-strings';
$arguments[] = \escapeshellarg($keyId);
$arguments[] = \escapeshellarg($pattern);
}
$output = '';
$this->reset();
$this->setOutput($output);
$this->exec($arguments);
echo $output;
return [];
$fingerprints = array();
foreach (\explode(PHP_EOL, $output) as $line) {
$lineExp = \explode(':', $line);
if ('fpr' === $lineExp[0]) {
$fingerprints[] = $lineExp[9];
}
}
return $fingerprints;
}
public function getPublicKeys($keyId = '') : array
{
// $privateKeyFingerprints = $this->getPrivateKeys($keyId);
// According to The file 'doc/DETAILS' in the GnuPG distribution, using
// double '--with-fingerprint' also prints the fingerprint for subkeys.
$arguments = array(
'--with-colons',
'--with-fingerprint',
'--with-fingerprint',
'--fixed-list-mode',
'--list-public-keys' // --list-keys ?
);
if ($keyId) {
$arguments[] = '--utf8-strings';
$arguments[] = \escapeshellarg($keyId);
}
$output = '';
$this->reset();
$this->setOutput($output);
$this->exec($arguments);
$result = $this->exec($arguments);
$keys = array();
$key = null; // current key
$subKey = null; // current sub-key
foreach (\explode(PHP_EOL, $output) as $line) {
foreach (\explode(PHP_EOL, $result['output']) as $line) {
$tokens = \explode(':', $line);
switch ($tokens[0])
@ -482,6 +429,7 @@ return [];
case 'tru':
break;
case 'sec':
case 'pub':
// new primary key means last key should be added to the array
if ($key !== null) {
@ -491,20 +439,21 @@ return [];
'disabled' => false,
'expired' => false,
'revoked' => false,
'is_secret' => false,
'is_secret' => 'ssb' === $tokens[0],
'can_sign' => \str_contains($tokens[11], 's'),
'can_encrypt' => \str_contains($tokens[11], 'e'),
'uids' => [],
'subkeys' => []
];
// Fall through to add (sub)key
case 'sub':
// Fall through to add subkey
case 'ssb': // secure subkey
case 'sub': // public subkey
$key['subkeys'][] = [
'fingerprint' => '', // fpr:::::::::....:
'keyid' => $tokens[4],
'timestamp' => $tokens[5],
'expires' => $tokens[6],
'is_secret' => false,
'is_secret' => 'ssb' === $tokens[0],
'invalid' => false,
// escaESCA
'can_encrypt' => \str_contains($tokens[11], 'e'),
@ -523,6 +472,10 @@ return [];
$key['subkeys'][\array_key_last($key['subkeys'])]['fingerprint'] = $tokens[9];
break;
case 'grp':
$key['subkeys'][\array_key_last($key['subkeys'])]['keygrip'] = $tokens[9];
break;
case 'uid':
$string = \stripcslashes($tokens[9]); // as per documentation
$name = '';
@ -570,45 +523,186 @@ return [];
return $keys;
}
private function _debug(string $msg)
/**
* Toggle armored output
* When true the output is ASCII
*/
public function setArmor(bool $armor = true) : bool
{
return false;
}
/**
* Sets the mode for error_reporting
* GNUPG_ERROR_WARNING, GNUPG_ERROR_EXCEPTION and GNUPG_ERROR_SILENT.
* By default GNUPG_ERROR_SILENT is used.
*/
public function setErrorMode(int $errormode) : void
{
}
/**
* Sets the mode for signing
* GNUPG_SIG_MODE_NORMAL, GNUPG_SIG_MODE_DETACH and GNUPG_SIG_MODE_CLEAR.
* By default GNUPG_SIG_MODE_CLEAR
*/
public function setSignMode(int $signmode) : bool
{
return false;
}
/**
* Signs a given text
*/
public function sign(string $plaintext) /*: string|false*/
{
return false;
}
/**
* Signs a given file
*/
public function signFile(string $filename) /*: string|false*/
{
return false;
}
protected function _verify($input, string $signature)
{
$this->reset();
$arguments = array('--verify');
if ('' === $signature) {
// signed or clearsigned data
$this->setInput($input);
} else {
// detached signature
$this->setInput($signature);
$this->_message =& $input;
// Signed data goes in FD_MESSAGE, detached signature data goes in FD_INPUT.
$arguments[] = '--enable-special-filenames';
$arguments[] = '- "-&' . self::FD_MESSAGE . '"';
}
$result = $this->exec($arguments);
$signatures = [];
foreach ($result['status'] as $line) {
$tokens = \explode(' ', $line);
switch ($tokens[0])
{
case 'VERIFICATION_COMPLIANCE_MODE':
case 'TRUST_FULLY':
break;
case 'EXPSIG':
case 'EXPKEYSIG':
case 'REVKEYSIG':
case 'BADSIG':
case 'ERRSIG':
case 'GOODSIG':
$signatures[] = [
'fingerprint' => '',
'validity' => 0,
'timestamp' => 0,
'status' => 'GOODSIG' === $tokens[0] ? 0 : 1,
'summary' => 'GOODSIG' === $tokens[0] ? 0 : 4,
'keyid' => $tokens[1],
'uid' => \rawurldecode(\implode(' ', \array_splice($tokens, 2))),
'valid' => false
];
break;
case 'VALIDSIG':
$last = \array_key_last($signatures);
$signatures[$last]['fingerprint'] = $tokens[1];
$signatures[$last]['timestamp'] = (int) $tokens[3];
$signatures[$last]['expires'] = (int) $tokens[4];
$signatures[$last]['version'] = (int) $tokens[5];
// $signatures[$last]['reserved'] = (int) $tokens[6];
// $signatures[$last]['pubkey-algo'] = (int) $tokens[7];
// $signatures[$last]['hash-algo'] = (int) $tokens[8];
// $signatures[$last]['sig-class'] = $tokens[9];
// $signatures[$last]['primary-fingerprint'] = $tokens[10];
$signatures[$last]['valid'] = 0;
}
}
return $signatures;
}
/**
* Verifies a signed text
*/
public function verify(string $signed_text, string $signature, string &$plaintext = null) /*: array|false*/
{
return $this->_verify($signed_text, $signature);
}
/**
* Verifies a signed file
*/
public function verifyFile(string $filename, string $signature, string &$plaintext = null) /*: array|false*/
{
$fp = \fopen($filename, 'rb');
try {
if (!$fp) {
throw new \Exception("Could not open file '{$filename}'");
}
return $this->_verify($fp, $signature);
} finally {
$fp && \fclose($fp);
}
}
/**
* Verifies a signed file
*/
public function verifyStream($fp, string $signature, string &$plaintext = null) /*: array|false*/
{
if (!$fp || !\is_resource($fp)) {
throw new \Exception('Invalid stream resource');
}
return $this->_verify($fp, $signature);
}
/**
* RFC 4880
* https://datatracker.ietf.org/doc/html/rfc4880#section-5.2.3.5
*/
public function signatureIssuer(string $signature) /*: array|false*/
{
if (preg_match('/-----BEGIN PGP SIGNATURE-----(.+)-----END PGP SIGNATURE-----/', $signature, $match)) {
// TODO: use https://github.com/singpolyma/openpgp-php ?
$binary = \base64_decode(\trim($match[1]));
return \strtoupper(\bin2hex(\substr($binary, 24, 8)));
}
return false;
}
private function _debug(string $msg) : void
{
if ($this->debug) {
echo $msg . "\n";
}
}
public function setOutput(&$output)
private function setInput(&$input) : void
{
$this->_output =& $output;
$this->_input =& $input;
}
/**
* Resets the GPG engine, preparing it for a new operation
*
* @return void
*
* @see Crypt_GPG_Engine::run()
* @see Crypt_GPG_Engine::setOperation()
*/
public function reset()
private function setOutput($output) : void
{
$this->_output = \is_resource($output) ? $output : null;
}
private function reset() : void
{
$this->_message = null;
$this->_input = null;
$this->_output = '';
$this->_output = null;
$this->_commandBuffer = '';
$this->_errorHandlers = array();
$this->_statusHandlers = array();
/*
if ($this->debug) {
$this->addStatusHandler(array($this, '_handleDebugStatus'));
$this->addErrorHandler(array($this, '_handleDebugError'));
}
$this->_processHandler = new Crypt_GPG_ProcessHandler($this);
$this->addStatusHandler(array($this->_processHandler, 'handleStatus'));
$this->addErrorHandler(array($this->_processHandler, 'handleError'));
*/
}
public function agent()
@ -617,7 +711,7 @@ return [];
// echo `gpg-agent --daemon --homedir $home 2>&1`;
}
private function exec(array $arguments)
private function exec(array $arguments) /*: array|false*/
{
if (\version_compare($this->version, '2.2.5', '<')) {
// Too old (<2018)
@ -717,9 +811,8 @@ return [];
$messageComplete = true;
}
if (\is_string($this->_output)) {
$outputBuffer =& $this->_output;
}
$status = [];
$errors = [];
// convenience variables
$fdInput = $proc_pipes[self::FD_INPUT];
@ -727,7 +820,7 @@ return [];
$fdError = $proc_pipes[self::FD_ERROR];
$fdStatus = $proc_pipes[self::FD_STATUS];
$fdCommand = $proc_pipes[self::FD_COMMAND];
$fdMessage = $this->_openPipes->get(self::FD_MESSAGE);
$fdMessage = $proc_pipes[self::FD_MESSAGE];
// select loop delay in milliseconds
$delay = 0;
@ -774,7 +867,7 @@ return [];
}
// set up output streams
if ($outputBuffer != '' && \is_resource($this->_output)) {
if ('' != $outputBuffer && $this->_output) {
$outputStreams[] = $this->_output;
}
@ -920,17 +1013,8 @@ return [];
$this->_debug('GPG error stream ready for reading');
$this->_debug('=> about to read ' . self::CHUNK_SIZE . ' bytes from GPG error');
foreach ($this->_openPipes->readPipeLines(self::FD_ERROR) as $line) {
if (!$this->_errorHandlers) {
$this->debug ? $this->_debug('ERROR ' . $line) : \trigger_error($line, E_USER_WARNING);
}
foreach ($this->_errorHandlers as $handler) {
\array_unshift($handler['args'], $line);
\call_user_func_array(
$handler['callback'],
$handler['args']
);
\array_shift($handler['args']);
}
$errors[] = $line;
$this->debug && $this->_debug("\t{$line}");
}
}
@ -940,20 +1024,11 @@ return [];
$this->_debug('=> about to read ' . self::CHUNK_SIZE . ' bytes from GPG status');
// pass lines to status handlers
foreach ($this->_openPipes->readPipeLines(self::FD_STATUS) as $line) {
if (!$this->_statusHandlers) {
$this->debug ? $this->_debug($line) : \trigger_error($line);
}
// only pass lines beginning with magic prefix
if ('[GNUPG:] ' == \substr($line, 0, 9)) {
$line = \substr($line, 9);
foreach ($this->_statusHandlers as $handler) {
\array_unshift($handler['args'], $line);
\call_user_func_array(
$handler['callback'],
$handler['args']
);
\array_shift($handler['args']);
}
$status[] = $line;
$this->debug && $this->_debug("\t{$line}");
}
}
}
@ -992,10 +1067,18 @@ return [];
$this->_debug('END PROCESSING');
$this->proc_close();
return [
'output' => $outputBuffer,
'status' => $status,
'errors' => $errors
];
}
private function proc_close()
private function proc_close() : int
{
$exitCode = 0;
// clear PINs from environment if they were set
$_ENV['PINENTRY_USER_DATA'] = null;
@ -1018,18 +1101,9 @@ return [];
if ($exitCode > 0) {
$this->_debug('=> subprocess returned an unexpected exit code: ' . $exitCode);
}
// close file handles before throwing an exception
if (\is_resource($this->_input)) {
\fclose($this->_input);
}
if (\is_resource($this->_output)) {
\fclose($this->_output);
}
$this->_processHandler && $this->_processHandler->throwException($exitCode);
}
return $exitCode;
}
private static function findBinary($name) : ?string
@ -1131,7 +1205,7 @@ class GpgProcPipes
// the pipe was seleted for writing, we assume it was EPIPE.
// There's no way to get the actual error code in PHP. See
// PHP Bug #39598. https://bugs.php.net/bug.php?id=39598
$this->pipes->close($number);
$this->close($number);
}
return $length ?: 0;
}