Added sign, decrypt and encrypt for #89

This commit is contained in:
the-djmaze 2022-01-25 13:54:50 +01:00
parent 151708a920
commit 54feb03316
3 changed files with 289 additions and 344 deletions

View file

@ -11,41 +11,60 @@ class GnuPG
// Instance of \SnappyMail\PGP\GPG // Instance of \SnappyMail\PGP\GPG
$GPG; $GPG;
public static function isSupported() : bool function __construct(string $homedir)
{
return \class_exists('gnupg')
|| \SnappyMail\PGP\GPG::isSupported();
}
public static function getInstance(string $homedir) : ?self
{ {
$homedir = \rtrim($homedir, '/\\'); $homedir = \rtrim($homedir, '/\\');
// BSD 4.4 max length // BSD 4.4 max length
if (104 <= \strlen($homedir . '/S.gpg-agent.extra')) { if (104 <= \strlen($homedir . '/S.gpg-agent.extra')) {
throw new \Exception('socket name for S.gpg-agent.extra is too long'); throw new \Exception('socket name for S.gpg-agent.extra is too long');
} }
$this->homedir = $homedir;
// \putenv("GNUPGHOME={$homedir}");
$self = null;
// if (\version_compare(\phpversion('gnupg'), '1.5', '>=')) { // if (\version_compare(\phpversion('gnupg'), '1.5', '>=')) {
if (\class_exists('gnupg')) { if (\class_exists('gnupg')) {
$self = new self; $this->GnuPG = new \gnupg([
$self->GnuPG = new \gnupg([
// It is the file name of the executable program implementing this protocol which is usually path of the gpg executable. // It is the file name of the executable program implementing this protocol which is usually path of the gpg executable.
// 'file_name' => '/usr/bin/gpg', // 'file_name' => '/usr/bin/gpg',
// It is the directory name of the configuration directory. It also overrides GNUPGHOME environment variable that is used for the same purpose. // It is the directory name of the configuration directory. It also overrides GNUPGHOME environment variable that is used for the same purpose.
'home_dir' => $homedir 'home_dir' => $homedir
]); ]);
// Output is ASCII // Output is ASCII
$self->GnuPG->setarmor(1); $this->GnuPG->setarmor(1);
} else if (\SnappyMail\PGP\GPG::isSupported()) { } else {
$self = new self; $this->getGPG();
$self->GPG = new \SnappyMail\PGP\GPG($homedir);
} }
if ($self) { }
$self->homedir = $homedir;
// \putenv("GNUPGHOME={$homedir}"); public static function isSupported() : bool
{
return \class_exists('gnupg')
|| \SnappyMail\PGP\GPG::isSupported();
}
private static $instance;
public static function getInstance(string $homedir) : ?self
{
if (!static::$instance) {
static::$instance = new self($homedir);
} }
return $self; return static::$instance;
}
public function handler()
{
return $this->GnuPG ?: $this->GPG;
}
public function getGPG()
{
if (!$this->GPG) {
if (!\SnappyMail\PGP\GPG::isSupported()) {
throw new \Exception('GnuPG not supported');
}
$this->GPG = new \SnappyMail\PGP\GPG($this->homedir);
}
return $this->GPG;
} }
/** /**
@ -53,13 +72,7 @@ class GnuPG
*/ */
public function addDecryptKey(string $fingerprint, string $passphrase) : bool public function addDecryptKey(string $fingerprint, string $passphrase) : bool
{ {
if ($this->GnuPG) { return $this->handler()->adddecryptkey($fingerprint, $passphrase);
return $this->GnuPG->adddecryptkey($fingerprint, $passphrase);
}
if ($this->GPG) {
return $this->GPG->adddecryptkey($fingerprint, $passphrase);
}
return false;
} }
/** /**
@ -67,13 +80,7 @@ class GnuPG
*/ */
public function addEncryptKey(string $fingerprint) : bool public function addEncryptKey(string $fingerprint) : bool
{ {
if ($this->GnuPG) { return $this->handler()->addencryptkey($fingerprint);
return $this->GnuPG->addencryptkey($fingerprint);
}
if ($this->GPG) {
return $this->GPG->addencryptkey($fingerprint);
}
return false;
} }
/** /**
@ -81,13 +88,7 @@ class GnuPG
*/ */
public function addSignKey(string $fingerprint, ?string $passphrase) : bool public function addSignKey(string $fingerprint, ?string $passphrase) : bool
{ {
if ($this->GnuPG) { return $this->handler()->addsignkey($fingerprint, $passphrase);
return $this->GnuPG->addsignkey($fingerprint, $passphrase);
}
if ($this->GPG) {
return $this->GPG->addsignkey($fingerprint, $passphrase);
}
return false;
} }
/** /**
@ -95,13 +96,7 @@ class GnuPG
*/ */
public function clearDecryptKeys() : bool public function clearDecryptKeys() : bool
{ {
if ($this->GnuPG) { return $this->handler()->cleardecryptkeys();
return $this->GnuPG->cleardecryptkeys();
}
if ($this->GPG) {
return $this->GPG->cleardecryptkeys();
}
return false;
} }
/** /**
@ -109,13 +104,7 @@ class GnuPG
*/ */
public function clearEncryptKeys() : bool public function clearEncryptKeys() : bool
{ {
if ($this->GnuPG) { return $this->handler()->clearencryptkeys();
return $this->GnuPG->clearencryptkeys();
}
if ($this->GPG) {
return $this->GPG->clearencryptkeys();
}
return false;
} }
/** /**
@ -123,13 +112,7 @@ class GnuPG
*/ */
public function clearSignKeys() : bool public function clearSignKeys() : bool
{ {
if ($this->GnuPG) { return $this->handler()->clearsignkeys();
return $this->GnuPG->clearsignkeys();
}
if ($this->GPG) {
return $this->GPG->clearsignkeys();
}
return false;
} }
/** /**
@ -137,13 +120,9 @@ class GnuPG
*/ */
public function decrypt(string $text) /*: string|false */ public function decrypt(string $text) /*: string|false */
{ {
if ($this->GnuPG) { return $this->GnuPG
return $this->GnuPG->decrypt($text); ? $this->GnuPG->decrypt($text)
} : $this->GPG->decrypt($text);
if ($this->GPG) {
return $this->GPG->decrypt($text);
}
return false;
} }
/** /**
@ -151,13 +130,9 @@ class GnuPG
*/ */
public function decryptFile(string $filename) /*: string|false */ public function decryptFile(string $filename) /*: string|false */
{ {
if ($this->GnuPG) { return $this->GnuPG
return $this->GnuPG->decrypt(\file_get_contents($filename)); ? $this->GnuPG->decrypt(\file_get_contents($filename))
} : $this->GPG->decryptFile($filename);
if ($this->GPG) {
return $this->GPG->decryptFile($filename);
}
return false;
} }
/** /**
@ -165,13 +140,9 @@ class GnuPG
*/ */
public function decryptVerify(string $text, string &$plaintext) /*: array|false*/ public function decryptVerify(string $text, string &$plaintext) /*: array|false*/
{ {
if ($this->GnuPG) { return $this->GnuPG
return $this->GnuPG->decryptverify($text, $plaintext); ? $this->GnuPG->decryptverify($text, $plaintext)
} : $this->GPG->decryptverify($text, $plaintext);
if ($this->GPG) {
return $this->GPG->decryptverify($text, $plaintext);
}
return false;
} }
/** /**
@ -179,13 +150,9 @@ class GnuPG
*/ */
public function decryptVerifyFile(string $filename, string &$plaintext) /*: array|false*/ public function decryptVerifyFile(string $filename, string &$plaintext) /*: array|false*/
{ {
if ($this->GnuPG) { return $this->GnuPG
return $this->GnuPG->decryptverify(\file_get_contents($filename), $plaintext); ? $this->GnuPG->decryptverify(\file_get_contents($filename), $plaintext)
} : $this->GPG->decryptverifyFile($filename, $plaintext);
if ($this->GPG) {
return $this->GPG->decryptverifyFile($filename, $plaintext);
}
return false;
} }
/** /**
@ -193,13 +160,9 @@ class GnuPG
*/ */
public function encrypt(string $plaintext) /*: string|false*/ public function encrypt(string $plaintext) /*: string|false*/
{ {
if ($this->GnuPG) { return $this->GnuPG
return $this->GnuPG->encrypt($plaintext); ? $this->GnuPG->encrypt($plaintext)
} : $this->GPG->encrypt($plaintext);
if ($this->GPG) {
return $this->GPG->encrypt($plaintext);
}
return false;
} }
/** /**
@ -207,13 +170,9 @@ class GnuPG
*/ */
public function encryptFile(string $filename) /*: string|false*/ public function encryptFile(string $filename) /*: string|false*/
{ {
if ($this->GnuPG) { return $this->GnuPG
return $this->GnuPG->encrypt(\file_get_contents($filename)); ? $this->GnuPG->encrypt(\file_get_contents($filename))
} : $this->GPG->encryptFile($filename);
if ($this->GPG) {
return $this->GPG->encryptFile($filename);
}
return false;
} }
/** /**
@ -221,13 +180,9 @@ class GnuPG
*/ */
public function encryptSign(string $plaintext) /*: string|false*/ public function encryptSign(string $plaintext) /*: string|false*/
{ {
if ($this->GnuPG) { return $this->GnuPG
return $this->GnuPG->encryptsign($plaintext); ? $this->GnuPG->encryptsign($plaintext)
} : $this->GPG->encryptsign($plaintext);
if ($this->GPG) {
return $this->GPG->encryptsign($plaintext);
}
return false;
} }
/** /**
@ -235,13 +190,9 @@ class GnuPG
*/ */
public function encryptSignFile(string $filename) /*: string|false*/ public function encryptSignFile(string $filename) /*: string|false*/
{ {
if ($this->GnuPG) { return $this->GnuPG
return $this->GnuPG->encryptsign(\file_get_contents($filename)); ? $this->GnuPG->encryptsign(\file_get_contents($filename))
} : $this->GPG->encryptsignFile($filename);
if ($this->GPG) {
return $this->GPG->encryptsignFile($filename);
}
return false;
} }
/** /**
@ -249,13 +200,9 @@ class GnuPG
*/ */
public function export(string $fingerprint) /*: string|false*/ public function export(string $fingerprint) /*: string|false*/
{ {
if ($this->GnuPG) { return $this->GnuPG
return $this->GnuPG->export($fingerprint); ? $this->GnuPG->export($fingerprint)
} : $this->GPG->export($fingerprint);
if ($this->GPG) {
return $this->GPG->export($fingerprint);
}
return false;
} }
/** /**
@ -263,13 +210,7 @@ class GnuPG
*/ */
public function getEngineInfo() : array public function getEngineInfo() : array
{ {
if ($this->GnuPG) { return $this->handler()->getengineinfo();
return $this->GnuPG->getengineinfo();
}
if ($this->GPG) {
return $this->GPG->getengineinfo();
}
return false;
} }
/** /**
@ -277,13 +218,7 @@ class GnuPG
*/ */
public function getError() /*: string|false*/ public function getError() /*: string|false*/
{ {
if ($this->GnuPG) { return $this->handler()->geterror();
return $this->GnuPG->geterror();
}
if ($this->GPG) {
return $this->GPG->geterror();
}
return false;
} }
/** /**
@ -291,13 +226,7 @@ class GnuPG
*/ */
public function getErrorInfo() : array public function getErrorInfo() : array
{ {
if ($this->GnuPG) { return $this->handler()->geterrorinfo();
return $this->GnuPG->geterrorinfo();
}
if ($this->GPG) {
return $this->GPG->geterrorinfo();
}
return false;
} }
/** /**
@ -305,13 +234,7 @@ class GnuPG
*/ */
public function getProtocol() : int public function getProtocol() : int
{ {
if ($this->GnuPG) { return $this->handler()->getprotocol();
return $this->GnuPG->getprotocol();
}
if ($this->GPG) {
return $this->GPG->getprotocol();
}
return false;
} }
/** /**
@ -333,13 +256,7 @@ class GnuPG
*/ */
public function import(string $keydata) /*: array|false*/ public function import(string $keydata) /*: array|false*/
{ {
if ($this->GnuPG) { return $this->handler()->import($keydata);
return $this->GnuPG->import($keydata);
}
if ($this->GPG) {
return $this->GPG->import($keydata);
}
return false;
} }
/** /**
@ -347,13 +264,9 @@ class GnuPG
*/ */
public function importFile(string $filename) /*: array|false*/ public function importFile(string $filename) /*: array|false*/
{ {
if ($this->GnuPG) { return $this->GnuPG
return $this->GnuPG->import(\file_get_contents($filename)); ? $this->GnuPG->import(\file_get_contents($filename))
} : $this->GPG->importFile($filename);
if ($this->GPG) {
return $this->GPG->importFile($filename);
}
return false;
} }
/** /**
@ -362,63 +275,60 @@ class GnuPG
public function keyInfo(string $pattern) : array public function keyInfo(string $pattern) : array
{ {
$keys = []; $keys = [];
$GPG = $this->GnuPG ?: $this->GPG; // Public
if ($GPG) { foreach ($this->handler()->keyinfo($pattern) as $info) {
// Public if (!$info['disabled'] && !$info['expired'] && !$info['revoked']) {
foreach ($GPG->keyinfo($pattern) as $info) { foreach ($info['uids'] as $uid) {
if (!$info['disabled'] && !$info['expired'] && !$info['revoked']) { $id = $uid['email'];
foreach ($info['uids'] as $uid) { if (isset($keys[$id])) {
$id = $uid['email']; $keys[$id]['can_sign'] = $keys[$id]['can_sign'] || $info['can_sign'];
if (isset($keys[$id])) { $keys[$id]['can_encrypt'] = $keys[$id]['can_encrypt'] || $info['can_encrypt'];
$keys[$id]['can_sign'] = $keys[$id]['can_sign'] || $info['can_sign']; } else {
$keys[$id]['can_encrypt'] = $keys[$id]['can_encrypt'] || $info['can_encrypt']; $keys[$id] = [
} else { 'name' => $uid['name'],
$keys[$id] = [ 'email' => $uid['email'],
'name' => $uid['name'], // Public Key tasks
'email' => $uid['email'], 'can_verify' => $info['can_sign'],
// Public Key tasks 'can_encrypt' => $info['can_encrypt'],
'can_verify' => $info['can_sign'], // Private Key tasks
'can_encrypt' => $info['can_encrypt'], 'can_sign' => false,
// Private Key tasks 'can_decrypt' => false,
'can_sign' => false, // The keys
'can_decrypt' => false, 'publicKeys' => [],
// The keys 'privateKeys' => []
'publicKeys' => [], ];
'privateKeys' => [] }
]; foreach ($info['subkeys'] as $key) {
} $keys[$id]['publicKeys'][$key['fingerprint']] = $key;
foreach ($info['subkeys'] as $key) {
$keys[$id]['publicKeys'][$key['fingerprint']] = $key;
}
} }
} }
} }
// Private, read https://github.com/php-gnupg/php-gnupg/issues/5 }
foreach ($GPG->keyinfo($pattern, 1) as $info) { // Private, read https://github.com/php-gnupg/php-gnupg/issues/5
if (!$info['disabled'] && !$info['expired'] && !$info['revoked']) { foreach ($this->handler()->keyinfo($pattern, 1) as $info) {
foreach ($info['uids'] as $uid) { if (!$info['disabled'] && !$info['expired'] && !$info['revoked']) {
$id = $uid['email']; foreach ($info['uids'] as $uid) {
if (isset($keys[$id])) { $id = $uid['email'];
$keys[$id]['can_sign'] = $keys[$id]['can_sign'] || $info['can_sign']; if (isset($keys[$id])) {
$keys[$id]['can_decrypt'] = $keys[$id]['can_decrypt'] || $info['can_encrypt']; $keys[$id]['can_sign'] = $keys[$id]['can_sign'] || $info['can_sign'];
} else { $keys[$id]['can_decrypt'] = $keys[$id]['can_decrypt'] || $info['can_encrypt'];
$keys[$id] = [ } else {
'name' => $uid['name'], $keys[$id] = [
'email' => $uid['email'], 'name' => $uid['name'],
// Public Key tasks 'email' => $uid['email'],
'can_verify' => false, // Public Key tasks
'can_encrypt' => false, 'can_verify' => false,
// Private Key tasks 'can_encrypt' => false,
'can_sign' => $info['can_sign'], // Private Key tasks
'can_decrypt' => $info['can_encrypt'], 'can_sign' => $info['can_sign'],
// The keys 'can_decrypt' => $info['can_encrypt'],
'publicKeys' => [], // The keys
'privateKeys' => [] 'publicKeys' => [],
]; 'privateKeys' => []
} ];
foreach ($info['subkeys'] as $key) { }
$keys[$id]['privateKeys'][$key['fingerprint']] = $key; foreach ($info['subkeys'] as $key) {
} $keys[$id]['privateKeys'][$key['fingerprint']] = $key;
} }
} }
} }
@ -432,13 +342,7 @@ class GnuPG
*/ */
public function setArmor(bool $armor = true) : bool public function setArmor(bool $armor = true) : bool
{ {
if ($this->GnuPG) { return $this->handler()->setarmor($armor ? 1 : 0);
return $this->GnuPG->setarmor($armor ? 1 : 0);
}
if ($this->GPG) {
return $this->GPG->setarmor($armor ? 1 : 0);
}
return false;
} }
/** /**
@ -448,12 +352,7 @@ class GnuPG
*/ */
public function setErrorMode(int $errormode) : void public function setErrorMode(int $errormode) : void
{ {
if ($this->GnuPG) { $this->handler()->seterrormode($errormode);
$this->GnuPG->seterrormode($errormode);
}
if ($this->GPG) {
$this->GPG->seterrormode($errormode);
}
} }
/** /**
@ -463,13 +362,7 @@ class GnuPG
*/ */
public function setSignMode(int $signmode) : bool public function setSignMode(int $signmode) : bool
{ {
if ($this->GnuPG) { return $this->handler()->setsignmode($signmode);
return $this->GnuPG->setsignmode($signmode);
}
if ($this->GPG) {
return $this->GPG->setsignmode($signmode);
}
return false;
} }
/** /**
@ -477,13 +370,9 @@ class GnuPG
*/ */
public function sign(string $plaintext) /*: string|false*/ public function sign(string $plaintext) /*: string|false*/
{ {
if ($this->GnuPG) { return $this->GnuPG
return $this->GnuPG->sign($plaintext); ? $this->GnuPG->sign($plaintext)
} : $this->GPG->sign($plaintext);
if ($this->GPG) {
return $this->GPG->sign($plaintext);
}
return false;
} }
/** /**
@ -491,13 +380,9 @@ class GnuPG
*/ */
public function signFile(string $filename) /*: string|false*/ public function signFile(string $filename) /*: string|false*/
{ {
if ($this->GnuPG) { return $this->GnuPG
return $this->GnuPG->sign(\file_get_contents($filename)); ? $this->GnuPG->sign(\file_get_contents($filename))
} : $this->GPG->signFile($filename);
if ($this->GPG) {
return $this->GPG->signFile($filename);
}
return false;
} }
/** /**
@ -505,13 +390,9 @@ class GnuPG
*/ */
public function verify(string $signed_text, string $signature, string &$plaintext = null) /*: array|false*/ public function verify(string $signed_text, string $signature, string &$plaintext = null) /*: array|false*/
{ {
if ($this->GnuPG) { return $this->GnuPG
return $this->GnuPG->verify($signed_text, $signature, $plaintext); ? $this->GnuPG->verify($signed_text, $signature, $plaintext)
} : $this->GPG->verify($signed_text, $signature, $plaintext);
if ($this->GPG) {
return $this->GPG->verify($signed_text, $signature, $plaintext);
}
return false;
} }
/** /**
@ -519,26 +400,8 @@ class GnuPG
*/ */
public function verifyFile(string $filename, string $signature, string &$plaintext = null) /*: array|false*/ public function verifyFile(string $filename, string $signature, string &$plaintext = null) /*: array|false*/
{ {
if ($this->GnuPG) { return $this->GnuPG
return $this->GnuPG->verify(\file_get_contents($filename), $signature, $plaintext); ? $this->GnuPG->verify(\file_get_contents($filename), $signature, $plaintext)
} : $this->GPG->verifyFile($filename, $signature, $plaintext);
if ($this->GPG) {
return $this->GPG->verifyFile($filename, $signature, $plaintext);
}
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;
} }
} }

View file

@ -62,6 +62,10 @@ class GPG
$proc_resource, $proc_resource,
$armor = true,
$signmode = 2,
$options = [ $options = [
'homedir' => '', 'homedir' => '',
'keyring' => '', 'keyring' => '',
@ -143,7 +147,7 @@ class GPG
*/ */
public function addDecryptKey(string $fingerprint, string $passphrase) : bool public function addDecryptKey(string $fingerprint, string $passphrase) : bool
{ {
$this->signKeys[$fingerprint] = $passphrase; $this->decryptKeys[$fingerprint] = $passphrase;
return true; return true;
} }
@ -161,7 +165,7 @@ class GPG
*/ */
public function addSignKey(string $fingerprint, ?string $passphrase) : bool public function addSignKey(string $fingerprint, ?string $passphrase) : bool
{ {
$this->decryptKeys[$fingerprint] = $passphrase; $this->signKeys[$fingerprint] = $passphrase;
return false; return false;
} }
@ -192,12 +196,27 @@ class GPG
return true; return true;
} }
protected function _decrypt(/*string|resource*/ $input, /*string|resource*/ $output = null)
{
$this->setInput($input);
$fclose = $this->setOutput($output);
$_ENV['PINENTRY_USER_DATA'] = \json_encode($this->decryptKeys);
$result = $this->exec('--decrypt --skip-verify');
$fclose && \fclose($fclose);
return $output ? true : $result['output'];
}
/** /**
* Decrypts a given text * Decrypts a given text
*/ */
public function decrypt(string $text) /*: string|false */ public function decrypt(string $text) /*: string|false */
{ {
return false; return $this->_decrypt($text);
} }
/** /**
@ -205,7 +224,26 @@ class GPG
*/ */
public function decryptFile(string $filename) /*: string|false */ public function decryptFile(string $filename) /*: string|false */
{ {
return false; $fp = \fopen($filename, 'rb');
try {
if (!$fp) {
throw new \Exception("Could not open file '{$filename}'");
}
return $this->_decrypt($fp, $output);
} finally {
$fp && \fclose($fp);
}
}
/**
* Decrypts a given stream
*/
public function decryptStream($fp, /*string|resource*/ $output = null) /*: string|false*/
{
if (!$fp || !\is_resource($fp)) {
throw new \Exception('Invalid stream resource');
}
return $this->_decrypt($fp, $output);
} }
/** /**
@ -224,26 +262,20 @@ class GPG
return false; return false;
} }
protected function _encrypt(/*resource*/ $input, /*string|resource*/ $output = null, bool $armor = true) protected function _encrypt(/*string|resource*/ $input, /*string|resource*/ $output = null)
{ {
if (!$this->encryptKeys) { if (!$this->encryptKeys) {
throw new \Exception('No encryption keys specified.'); throw new \Exception('No encryption keys specified.');
} }
$fclose = false; $this->setInput($input);
if ($output && !\is_resource($output)) {
$output = \fopen($output, 'rb'); $fclose = $this->setOutput($output);
if (!$output) {
throw new \Exception("Could not open file '{$filename}'");
}
$fclose = true;
}
$this->_output = $output;
$arguments = [ $arguments = [
'--encrypt' '--encrypt'
]; ];
if ($armor) { if ($this->armor) {
$arguments[] = '--armor'; $arguments[] = '--armor';
} }
@ -251,10 +283,9 @@ class GPG
$arguments[] = '--recipient ' . \escapeshellarg($key['fingerprint']); $arguments[] = '--recipient ' . \escapeshellarg($key['fingerprint']);
} }
$this->setInput($input);
$result = $this->exec($arguments); $result = $this->exec($arguments);
$fclose && \fclose($output); $fclose && \fclose($fclose);
return $output ? true : $result['output']; return $output ? true : $result['output'];
} }
@ -283,7 +314,7 @@ class GPG
} }
} }
public function encryptStream($fp, /*string|resource*/ $output = null) /*: string|false*/ public function encryptStream(/*resource*/ $fp, /*string|resource*/ $output = null) /*: string|false*/
{ {
if (!$fp || !\is_resource($fp)) { if (!$fp || !\is_resource($fp)) {
throw new \Exception('Invalid stream resource'); throw new \Exception('Invalid stream resource');
@ -349,10 +380,10 @@ class GPG
*/ */
public function getProtocol() : int public function getProtocol() : int
{ {
return false; return 0;
} }
public function addPassphrase($key, $passphrase) public function addPassphrase($keyId, $passphrase)
{ {
$this->passphrases[$key] = $passphrase; $this->passphrases[$key] = $passphrase;
return $this; return $this;
@ -425,15 +456,11 @@ class GPG
{ {
$arguments = ['--import']; $arguments = ['--import'];
$envKeys = []; if ($this->passphrases) {
if (empty($this->passphrases)) { $_ENV['PINENTRY_USER_DATA'] = \json_encode($this->passphrases);
$arguments[] = '--batch';
} else { } else {
foreach ($this->passphrases as $keyId => $key) { $arguments[] = '--batch';
$envKeys[$keyId] = \is_array($key) ? $key['passphrase'] : $key;
}
} }
$_ENV['PINENTRY_USER_DATA'] = \json_encode($envKeys);
$this->setInput($input); $this->setInput($input);
$result = $this->exec($arguments); $result = $this->exec($arguments);
@ -639,12 +666,12 @@ class GPG
} }
/** /**
* Toggle armored output * Toggle the armored output
* When true the output is ASCII
*/ */
public function setArmor(bool $armor = true) : bool public function setArmor(int $armor = 1) : bool
{ {
return false; $this->armor = !!$armor;
return true;
} }
/** /**
@ -658,43 +685,108 @@ class GPG
/** /**
* Sets the mode for signing * Sets the mode for signing
* GNUPG_SIG_MODE_NORMAL, GNUPG_SIG_MODE_DETACH and GNUPG_SIG_MODE_CLEAR. * GNUPG_SIG_MODE_NORMAL, GNUPG_SIG_MODE_DETACH, GNUPG_SIG_MODE_CLEAR
* By default GNUPG_SIG_MODE_CLEAR * By default GNUPG_SIG_MODE_CLEAR
*/ */
public function setSignMode(int $signmode) : bool public function setSignMode(int $signmode) : bool
{ {
return false; $this->signmode = $signmode;
return true;
}
protected function _sign(/*string|resource*/ $input, /*string|resource*/ $output = null, bool $textmode = true) /*: string|false*/
{
if (!$this->hasSignKeys()) {
throw new \Exception('No signing keys specified.');
}
$this->setInput($input);
$fclose = $this->setOutput($output);
$arguments = [];
switch ($this->signmode)
{
case 0: // GNUPG_SIG_MODE_NORMAL
$arguments[] = '--sign';
break;
case 1: // GNUPG_SIG_MODE_DETACH
$arguments[] = '--detach-sign';
break;
case 2: // GNUPG_SIG_MODE_CLEAR
default:
$arguments[] = '--clearsign';
break;
}
if ($this->armor) {
$arguments[] = '--armor';
}
if ($textmode) {
$arguments[] = '--textmode';
}
foreach ($this->signKeys as $fingerprint => $pass) {
$arguments[] = '--local-user ' . \escapeshellarg($fingerprint);
}
$_ENV['PINENTRY_USER_DATA'] = \json_encode($this->signKeys);
$result = $this->exec($arguments);
$fclose && \fclose($fclose);
return $output ? true : $result['output'];
} }
/** /**
* Signs a given text * Signs a given text
*/ */
public function sign(string $plaintext) /*: string|false*/ public function sign(string $plaintext, /*string|resource*/ $output = null) /*: string|false*/
{ {
return false; return $this->_sign($plaintext, $output);
} }
/** /**
* Signs a given file * Signs a given file
*/ */
public function signFile(string $filename) /*: string|false*/ public function signFile(string $filename, /*string|resource*/ $output = null) /*: string|false*/
{ {
return false; $fp = \fopen($filename, 'rb');
try {
if (!$fp) {
throw new \Exception("Could not open file '{$filename}'");
}
return $this->_sign($fp, $output);
} finally {
$fp && \fclose($fp);
}
}
/**
* Signs a given file
*/
public function signStream($fp, /*string|resource*/ $output = null) /*: array|false*/
{
if (!$fp || !\is_resource($fp)) {
throw new \Exception('Invalid stream resource');
}
return $this->_sign($fp, $output);
} }
protected function _verify($input, string $signature) protected function _verify($input, string $signature)
{ {
$arguments = ['--verify']; $arguments = ['--verify'];
if ('' === $signature) { if ($signature) {
// signed or clearsigned data
$this->setInput($input);
} else {
// detached signature // detached signature
$this->setInput($signature); $this->setInput($signature);
$this->_message =& $input; $this->_message =& $input;
// Signed data goes in FD_MESSAGE, detached signature data goes in FD_INPUT. // Signed data goes in FD_MESSAGE, detached signature data goes in FD_INPUT.
$arguments[] = '--enable-special-filenames'; $arguments[] = '--enable-special-filenames';
$arguments[] = '- "-&' . self::FD_MESSAGE . '"'; $arguments[] = '- "-&' . self::FD_MESSAGE . '"';
} else {
// signed or clearsigned data
$this->setInput($input);
} }
$result = $this->exec($arguments); $result = $this->exec($arguments);
@ -779,20 +871,6 @@ class GPG
return $this->_verify($fp, $signature); 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 private function _debug(string $msg) : void
{ {
if ($this->debug) { if ($this->debug) {
@ -805,9 +883,18 @@ class GPG
$this->_input =& $input; $this->_input =& $input;
} }
private function setOutput($output) : void private function setOutput($output)/* : resource|false*/
{ {
$this->_output = \is_resource($output) ? $output : null; $fclose = false;
if ($output && !\is_resource($output)) {
$output = \fopen($output, 'wb');
if (!$output) {
throw new \Exception("Could not open file '{$filename}'");
}
$fclose = $output;
}
$this->_output = $output;
return $fclose;
} }
public function agent() public function agent()

View file

@ -1,9 +1,4 @@
<?php <?php
/**
* 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; namespace SnappyMail\PGP;
@ -87,7 +82,7 @@ class GPGKeySettings
$keyParams[] = "Key-Usage: {$this->usage}"; $keyParams[] = "Key-Usage: {$this->usage}";
} }
/** Somehow this is broken /** Somehow this is broken and not working in v2.3.4
$subkey = $this->subkeys[0]; $subkey = $this->subkeys[0];
if (!empty($subkey['type'])) { if (!empty($subkey['type'])) {
$keyParams[] = "Subkey-Type: {$subkey['type']}"; $keyParams[] = "Subkey-Type: {$subkey['type']}";