mirror of
https://github.com/the-djmaze/snappymail.git
synced 2025-02-24 14:57:26 +08:00
parent
f26c35c411
commit
151708a920
2 changed files with 266 additions and 106 deletions
|
@ -47,17 +47,18 @@ class GPG
|
|||
$_input,
|
||||
$_output,
|
||||
|
||||
$signKeys = array(),
|
||||
$encryptKeys = array(),
|
||||
$decryptKeys = array();
|
||||
$signKeys = [],
|
||||
$encryptKeys = [],
|
||||
$decryptKeys = [];
|
||||
|
||||
private
|
||||
$binary,
|
||||
$version = '2.0',
|
||||
$cipher_algorithms = ['IDEA', '3DES', 'CAST5', 'BLOWFISH', 'AES', 'AES192', 'AES256', 'TWOFISH', 'CAMELLIA128', 'CAMELLIA192', 'CAMELLIA256'],
|
||||
$hash_algorithms = ['SHA1', 'RIPEMD160', 'SHA256', 'SHA384', 'SHA512', 'SHA224'],
|
||||
$pubkey_algorithms = ['RSA', 'ELG', 'DSA', 'ECDH', 'ECDSA', 'EDDSA'],
|
||||
$compression = ['Uncompressed', 'ZIP', 'ZLIB'],
|
||||
$ciphers = [],
|
||||
$digests = [],
|
||||
$curves = [],
|
||||
$pubkey_types = [],
|
||||
$compressions = [],
|
||||
|
||||
$proc_resource,
|
||||
|
||||
|
@ -97,21 +98,24 @@ class GPG
|
|||
// How to use gpgme-json ?
|
||||
$this->binary = static::findBinary('gpg');
|
||||
|
||||
$info = \preg_replace('/\R +/', ' ', `$this->binary --version`);
|
||||
if (\preg_match('/gpg\\s.+([0-9]+\\.[0-9]+\\.[0-9]+)/', $info, $match)) {
|
||||
$info = \preg_replace('/\R +/', ' ', `$this->binary --with-colons --list-config`);
|
||||
if (\preg_match('/cfg:version:([0-9]+\\.[0-9]+\\.[0-9]+)/', $info, $match)) {
|
||||
$this->version = $match[1];
|
||||
}
|
||||
if (\preg_match('/Cipher: (.+)/', $info, $match)) {
|
||||
$this->cipher_algorithms = \array_map('trim', \explode(',', $match[1]));
|
||||
if (\preg_match('/cfg:cipher:(.+)/', $info, $match) && \preg_match('/cfg:ciphername:(.+)/', $info, $match1)) {
|
||||
$this->ciphers = \array_combine(\explode(';', $match[1]), \explode(';', $match1[1]));
|
||||
}
|
||||
if (\preg_match('/Hash: (.+)/', $info, $match)) {
|
||||
$this->hash_algorithms = \array_map('trim', \explode(',', $match[1]));
|
||||
if (\preg_match('/cfg:digest:(.+)/', $info, $match) && \preg_match('/cfg:digestname:(.+)/', $info, $match1)) {
|
||||
$this->digests = \array_combine(\explode(';', $match[1]), \explode(';', $match1[1]));
|
||||
}
|
||||
if (\preg_match('/Pubkey: (.+)/', $info, $match)) {
|
||||
$this->pubkey_algorithms = \array_map('trim', \explode(',', $match[1]));
|
||||
if (\preg_match('/cfg:pubkey:(.+)/', $info, $match) && \preg_match('/cfg:pubkeyname:(.+)/', $info, $match1)) {
|
||||
$this->pubkey_types = \array_combine(\explode(';', $match[1]), \explode(';', $match1[1]));
|
||||
}
|
||||
if (\preg_match('/Compression: (.+)/', $info, $match)) {
|
||||
$this->compression = \array_map('trim', \explode(',', $match[1]));
|
||||
if (\preg_match('/cfg:compress:(.+)/', $info, $match) && \preg_match('/cfg:compressname:(.+)/', $info, $match1)) {
|
||||
$this->compressions = \array_combine(\explode(';', $match[1]), \explode(';', $match1[1]));
|
||||
}
|
||||
if (\preg_match('/cfg:curve:(.+)/', $info, $match)) {
|
||||
$this->curves = \explode(';', $match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -121,9 +125,9 @@ class GPG
|
|||
|
||||
$gpgconf = static::findBinary('gpgconf');
|
||||
if ($gpgconf) {
|
||||
$env = array('GNUPGHOME' => $this->options['homedir']);
|
||||
$pipes = array();
|
||||
if ($process = \proc_open($gpgconf . ' --kill gpg-agent --homedir ' . \escapeshellarg($this->options['homedir']), array(), $pipes, null, $env)) {
|
||||
$env = ['GNUPGHOME' => $this->options['homedir']];
|
||||
$pipes = [];
|
||||
if ($process = \proc_open($gpgconf . ' --kill gpg-agent --homedir ' . \escapeshellarg($this->options['homedir']), [], $pipes, null, $env)) {
|
||||
\proc_close($process);
|
||||
}
|
||||
}
|
||||
|
@ -220,20 +224,71 @@ class GPG
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts a given text
|
||||
*/
|
||||
public function encrypt(string $plaintext) /*: string|false*/
|
||||
protected function _encrypt(/*resource*/ $input, /*string|resource*/ $output = null, bool $armor = true)
|
||||
{
|
||||
return false;
|
||||
if (!$this->encryptKeys) {
|
||||
throw new \Exception('No encryption keys specified.');
|
||||
}
|
||||
|
||||
$fclose = false;
|
||||
if ($output && !\is_resource($output)) {
|
||||
$output = \fopen($output, 'rb');
|
||||
if (!$output) {
|
||||
throw new \Exception("Could not open file '{$filename}'");
|
||||
}
|
||||
$fclose = true;
|
||||
}
|
||||
$this->_output = $output;
|
||||
|
||||
$arguments = [
|
||||
'--encrypt'
|
||||
];
|
||||
if ($armor) {
|
||||
$arguments[] = '--armor';
|
||||
}
|
||||
|
||||
foreach ($this->encryptKeys as $key) {
|
||||
$arguments[] = '--recipient ' . \escapeshellarg($key['fingerprint']);
|
||||
}
|
||||
|
||||
$this->setInput($input);
|
||||
$result = $this->exec($arguments);
|
||||
|
||||
$fclose && \fclose($output);
|
||||
|
||||
return $output ? true : $result['output'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts a given text
|
||||
*/
|
||||
public function encryptFile(string $filename) /*: string|false*/
|
||||
public function encrypt(string $plaintext, /*string|resource*/ $output = null) /*: string|false*/
|
||||
{
|
||||
return false;
|
||||
return $this->_encrypt($plaintext, $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypts a given text
|
||||
*/
|
||||
public function encryptFile(string $filename, /*string|resource*/ $output = null) /*: string|false*/
|
||||
{
|
||||
$fp = \fopen($filename, 'rb');
|
||||
try {
|
||||
if (!$fp) {
|
||||
throw new \Exception("Could not open file '{$filename}'");
|
||||
}
|
||||
return $this->_encrypt($filename, $output);
|
||||
} finally {
|
||||
$fp && \fclose($fp);
|
||||
}
|
||||
}
|
||||
|
||||
public function encryptStream($fp, /*string|resource*/ $output = null) /*: string|false*/
|
||||
{
|
||||
if (!$fp || !\is_resource($fp)) {
|
||||
throw new \Exception('Invalid stream resource');
|
||||
}
|
||||
return $this->_encrypt($fp, $output);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -306,48 +361,30 @@ class GPG
|
|||
/**
|
||||
* Generates a key
|
||||
* Also saves revocation certificate in {homedir}/openpgp-revocs.d/
|
||||
* https://www.gnupg.org/documentation/manuals/gnupg/OpenPGP-Key-Management.html
|
||||
*/
|
||||
public function generateKey(string $uid, string $passphrase) /*: string|false*/
|
||||
public function generateKey(GPGKeySettings $settings) /*: string|false*/
|
||||
{
|
||||
$arguments = [
|
||||
'--batch',
|
||||
'--yes',
|
||||
'--passphrase', \escapeshellarg($settings->passphrase)
|
||||
];
|
||||
|
||||
/**
|
||||
* https://www.gnupg.org/documentation/manuals/gnupg/Unattended-GPG-key-generation.html
|
||||
* But it can't generate multiple subkeys
|
||||
* Somehow generating first subkey is also broken in v2.3.4
|
||||
$this->_input = $settings->asUnattendedData();
|
||||
$result = $this->exec(['--batch', '--yes', '--full-gen-key']);
|
||||
*/
|
||||
/*
|
||||
$this->_input = "Key-Type: ECDSA
|
||||
Key-Curve: nistp256
|
||||
Key-Usage: sign
|
||||
Subkey-Type: ecdh
|
||||
Subkey-Curve: Curve25519
|
||||
Subkey-Usage: sign
|
||||
Name-Real: John
|
||||
Name-Comment: comment
|
||||
Name-Email: john.doe@example.com
|
||||
Expire-Date: 0
|
||||
Passphrase: {$passphrase}
|
||||
%commit";
|
||||
$result = $this->exec(\array_merge($arguments, [
|
||||
'--quick-gen-key',
|
||||
\escapeshellarg($settings->uid()),
|
||||
$settings->algo(),
|
||||
$settings->usage
|
||||
]));
|
||||
|
||||
// gpg --full-generate-key
|
||||
// gpg --quick-gen-key
|
||||
$result = $this->exec(array(
|
||||
'--batch',
|
||||
'--yes',
|
||||
'--full-gen-key' // '--gen-key'
|
||||
));
|
||||
*/
|
||||
$arguments = array(
|
||||
'--batch',
|
||||
'--yes',
|
||||
'--passphrase', \escapeshellarg($passphrase)
|
||||
);
|
||||
|
||||
$result = $this->exec(\array_merge($arguments, array(
|
||||
'--quick-generate-key',
|
||||
\escapeshellarg(\rawurlencode($uid)),
|
||||
'ed25519',
|
||||
'cert',
|
||||
'0'
|
||||
)));
|
||||
$fingerprint = '';
|
||||
foreach ($result['status'] as $line) {
|
||||
$tokens = \explode(' ', $line);
|
||||
|
@ -361,16 +398,18 @@ Passphrase: {$passphrase}
|
|||
|
||||
$arguments[] = '--quick-add-key';
|
||||
$arguments[] = $fingerprint;
|
||||
$this->exec(\array_merge($arguments, array(
|
||||
'ed25519',
|
||||
'sign',
|
||||
'0'
|
||||
)));
|
||||
$this->exec(\array_merge($arguments, array(
|
||||
'cv25519',
|
||||
'encrypt',
|
||||
'0'
|
||||
)));
|
||||
|
||||
foreach ($settings->subkeys as $i => $key) {
|
||||
$algo = 'default';
|
||||
if (!empty($key['curve'])) {
|
||||
$algo = $key['curve'];
|
||||
}
|
||||
if (!empty($key['type'])) {
|
||||
$algo = $key['type'] . ($key['length'] ?? '');
|
||||
}
|
||||
$this->exec(\array_merge($arguments, [$algo, $key['usage'], '0']));
|
||||
}
|
||||
|
||||
/*
|
||||
[status][0] => KEY_NOT_CREATED
|
||||
[errors][0] => gpg: -:3: specified Key-Usage not allowed for algo 22
|
||||
|
@ -384,9 +423,9 @@ Passphrase: {$passphrase}
|
|||
|
||||
protected function _importKey($input) /*: array|false*/
|
||||
{
|
||||
$arguments = array('--import');
|
||||
$arguments = ['--import'];
|
||||
|
||||
$envKeys = array();
|
||||
$envKeys = [];
|
||||
if (empty($this->passphrases)) {
|
||||
$arguments[] = '--batch';
|
||||
} else {
|
||||
|
@ -396,7 +435,6 @@ Passphrase: {$passphrase}
|
|||
}
|
||||
$_ENV['PINENTRY_USER_DATA'] = \json_encode($envKeys);
|
||||
|
||||
$this->reset();
|
||||
$this->setInput($input);
|
||||
$result = $this->exec($arguments);
|
||||
|
||||
|
@ -459,13 +497,12 @@ Passphrase: {$passphrase}
|
|||
throw new \Exception('Delete private key first: ' . $keyId);
|
||||
}
|
||||
|
||||
$this->reset();
|
||||
$result = $this->exec(array(
|
||||
$result = $this->exec([
|
||||
'--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'
|
||||
|
@ -481,22 +518,21 @@ Passphrase: {$passphrase}
|
|||
{
|
||||
// According to The file 'doc/DETAILS' in the GnuPG distribution, using
|
||||
// double '--with-fingerprint' also prints the fingerprint for subkeys.
|
||||
$arguments = array(
|
||||
$arguments = [
|
||||
'--with-colons',
|
||||
'--with-fingerprint',
|
||||
'--with-fingerprint',
|
||||
'--fixed-list-mode',
|
||||
$private ? '--list-secret-keys' : '--list-public-keys'
|
||||
);
|
||||
];
|
||||
if ($pattern) {
|
||||
$arguments[] = '--utf8-strings';
|
||||
$arguments[] = \escapeshellarg($pattern);
|
||||
}
|
||||
|
||||
$this->reset();
|
||||
$result = $this->exec($arguments);
|
||||
|
||||
$keys = array();
|
||||
$keys = [];
|
||||
$key = null; // current key
|
||||
$subKey = null; // current sub-key
|
||||
|
||||
|
@ -560,7 +596,7 @@ Passphrase: {$passphrase}
|
|||
$name = '';
|
||||
$email = '';
|
||||
$comment = '';
|
||||
$matches = array();
|
||||
$matches = [];
|
||||
|
||||
// get email address from end of string if it exists
|
||||
if (\preg_match('/^(.*?)<([^>]+)>$/', $string, $matches)) {
|
||||
|
@ -569,7 +605,7 @@ Passphrase: {$passphrase}
|
|||
}
|
||||
|
||||
// get comment from end of string if it exists
|
||||
$matches = array();
|
||||
$matches = [];
|
||||
if (\preg_match('/^(.+?) \(([^\)]+)\)$/', $string, $matches)) {
|
||||
$string = $matches[1];
|
||||
$comment = $matches[2];
|
||||
|
@ -648,9 +684,7 @@ Passphrase: {$passphrase}
|
|||
|
||||
protected function _verify($input, string $signature)
|
||||
{
|
||||
$this->reset();
|
||||
|
||||
$arguments = array('--verify');
|
||||
$arguments = ['--verify'];
|
||||
if ('' === $signature) {
|
||||
// signed or clearsigned data
|
||||
$this->setInput($input);
|
||||
|
@ -776,14 +810,6 @@ Passphrase: {$passphrase}
|
|||
$this->_output = \is_resource($output) ? $output : null;
|
||||
}
|
||||
|
||||
private function reset() : void
|
||||
{
|
||||
$this->_message = null;
|
||||
$this->_input = null;
|
||||
$this->_output = null;
|
||||
$this->_commandBuffer = '';
|
||||
}
|
||||
|
||||
public function agent()
|
||||
{
|
||||
// $home = \escapeshellarg($this->options['homedir']);
|
||||
|
@ -797,7 +823,7 @@ Passphrase: {$passphrase}
|
|||
return false;
|
||||
}
|
||||
|
||||
$defaultArguments = array(
|
||||
$defaultArguments = [
|
||||
'--status-fd ' . self::FD_STATUS,
|
||||
'--command-fd ' . self::FD_COMMAND,
|
||||
// '--no-greeting',
|
||||
|
@ -810,7 +836,7 @@ Passphrase: {$passphrase}
|
|||
'--exit-on-status-write-error', // 1.4.2+
|
||||
'--trust-model always', // 1.3.2+ else --always-trust
|
||||
'--pinentry-mode loopback' // 2.1.13+
|
||||
);
|
||||
];
|
||||
|
||||
if (!$this->strict) {
|
||||
$defaultArguments[] = '--ignore-time-conflict';
|
||||
|
@ -838,14 +864,14 @@ Passphrase: {$passphrase}
|
|||
|
||||
$commandLine = $this->binary . ' ' . \implode(' ', \array_merge($defaultArguments, $arguments));
|
||||
|
||||
$descriptorSpec = array(
|
||||
$descriptorSpec = [
|
||||
self::FD_INPUT => array('pipe', 'rb'), // stdin
|
||||
self::FD_OUTPUT => array('pipe', 'wb'), // stdout
|
||||
self::FD_ERROR => array('pipe', 'wb'), // stderr
|
||||
self::FD_STATUS => array('pipe', 'wb'), // status
|
||||
self::FD_COMMAND => array('pipe', 'rb'), // command
|
||||
self::FD_MESSAGE => array('pipe', 'rb') // message
|
||||
);
|
||||
];
|
||||
|
||||
$this->_debug('OPENING GPG SUBPROCESS WITH THE FOLLOWING COMMAND:');
|
||||
$this->_debug($commandLine);
|
||||
|
@ -854,7 +880,7 @@ Passphrase: {$passphrase}
|
|||
$env = $_ENV;
|
||||
$env['LC_ALL'] = 'C';
|
||||
|
||||
$proc_pipes = array();
|
||||
$proc_pipes = [];
|
||||
|
||||
$this->proc_resource = \proc_open(
|
||||
$commandLine,
|
||||
|
@ -862,7 +888,7 @@ Passphrase: {$passphrase}
|
|||
$proc_pipes,
|
||||
null,
|
||||
$env,
|
||||
array('binary_pipes' => true)
|
||||
['binary_pipes' => true]
|
||||
);
|
||||
|
||||
if (!\is_resource($this->proc_resource)) {
|
||||
|
@ -906,9 +932,9 @@ Passphrase: {$passphrase}
|
|||
$inputPosition = 0;
|
||||
|
||||
while (true) {
|
||||
$inputStreams = array();
|
||||
$outputStreams = array();
|
||||
$exceptionStreams = array();
|
||||
$inputStreams = [];
|
||||
$outputStreams = [];
|
||||
$exceptionStreams = [];
|
||||
|
||||
// set up input streams
|
||||
if (!$inputComplete && \is_resource($this->_input)) {
|
||||
|
@ -1147,6 +1173,11 @@ Passphrase: {$passphrase}
|
|||
|
||||
$this->proc_close();
|
||||
|
||||
$this->_message = null;
|
||||
$this->_input = null;
|
||||
$this->_output = null;
|
||||
$this->_commandBuffer = '';
|
||||
|
||||
return [
|
||||
'output' => $outputBuffer,
|
||||
'status' => $status,
|
||||
|
@ -1191,13 +1222,13 @@ Passphrase: {$passphrase}
|
|||
if ($binary && \is_executable($binary)) {
|
||||
return $binary;
|
||||
}
|
||||
$locations = array(
|
||||
$locations = [
|
||||
'/sw/bin/',
|
||||
'/usr/bin/',
|
||||
'/usr/local/bin/',
|
||||
'/opt/local/bin/',
|
||||
'/run/current-system/sw/bin/'
|
||||
);
|
||||
];
|
||||
foreach ($locations as $location) {
|
||||
if (\is_executable($location . $name)) {
|
||||
return $location . $name;
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
<?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;
|
||||
|
||||
class GPGKeySettings
|
||||
{
|
||||
public
|
||||
// Primary key
|
||||
$type = 'EDDSA', // One of pubkey_types
|
||||
$curve = 'ed25519', // One of curves for ECC keys.
|
||||
$length = null, // For non-ECC keys.
|
||||
$usage = 'cert', // cert is default
|
||||
$expires = 0,
|
||||
|
||||
$subkeys = [
|
||||
[
|
||||
'type' => null, // One of pubkey_types
|
||||
'curve' => 'ed25519', // One of curves
|
||||
'length' => null,
|
||||
'usage' => 'sign'
|
||||
],
|
||||
[
|
||||
'type' => null, // One of pubkey_types
|
||||
'curve' => 'cv25519', // One of curves
|
||||
'length' => null,
|
||||
'usage' => 'encrypt'
|
||||
]
|
||||
],
|
||||
|
||||
$name = '',
|
||||
$email = '',
|
||||
$comment = '',
|
||||
$passphrase;
|
||||
|
||||
public function algo() : string
|
||||
{
|
||||
if ($this->curve) {
|
||||
return $this->curve;
|
||||
}
|
||||
return $this->type . ($this->length ?: '');
|
||||
}
|
||||
|
||||
public function uid() : string
|
||||
{
|
||||
$result = '';
|
||||
if (\strlen($this->name)) {
|
||||
$result .= $this->name;
|
||||
}
|
||||
if (\strlen($this->comment)) {
|
||||
$result .= ' (' . $this->comment . ')';
|
||||
}
|
||||
if (\strlen($this->email)) {
|
||||
$result .= ' <' . $this->email. '>';
|
||||
}
|
||||
return \trim($result);
|
||||
}
|
||||
|
||||
public function useDefault() : void
|
||||
{
|
||||
$this->type = 'default';
|
||||
$this->curve = null;
|
||||
$this->length = null;
|
||||
$this->usage = null;
|
||||
$subkeys[0] = ['type' => 'default'];
|
||||
}
|
||||
|
||||
/**
|
||||
* https://www.gnupg.org/documentation/manuals/gnupg/Unattended-GPG-key-generation.html
|
||||
*/
|
||||
public function asUnattendedData() : string
|
||||
{
|
||||
$keyParams = [
|
||||
"Key-Type: {$this->type}"
|
||||
];
|
||||
if ($this->curve) {
|
||||
$keyParams[] = "Key-Curve: {$this->curve}";
|
||||
}
|
||||
if ($this->length) {
|
||||
$keyParams[] = "Key-Length: {$this->length}";
|
||||
}
|
||||
if ($this->usage) {
|
||||
$keyParams[] = "Key-Usage: {$this->usage}";
|
||||
}
|
||||
|
||||
/** Somehow this is broken
|
||||
$subkey = $this->subkeys[0];
|
||||
if (!empty($subkey['type'])) {
|
||||
$keyParams[] = "Subkey-Type: {$subkey['type']}";
|
||||
}
|
||||
if (!empty($subkey['curve'])) {
|
||||
$keyParams[] = "Subkey-Curve: {$subkey['curve']}";
|
||||
}
|
||||
if (!empty($subkey['length'])) {
|
||||
$keyParams[] = "Subkey-Length: {$subkey['length']}";
|
||||
}
|
||||
if (!empty($subkey['usage'])) {
|
||||
$keyParams[] = "Subkey-Usage: {$subkey['usage']}";
|
||||
}
|
||||
*/
|
||||
|
||||
if ($this->expires) {
|
||||
$keyParams[] = "Expire-Date: " . \date('Y-m-d', $this->expires);
|
||||
}
|
||||
|
||||
if (\strlen($this->name)) {
|
||||
$keyParams[] = "Name-Real: {$this->name}";
|
||||
}
|
||||
if (\strlen($this->email)) {
|
||||
$keyParams[] = "Name-Email: {$this->email}";
|
||||
}
|
||||
if (\strlen($this->comment)) {
|
||||
$keyParams[] = "Name-Comment: {$this->comment}";
|
||||
}
|
||||
|
||||
if (\strlen($this->passphrase)) {
|
||||
$keyParams[] = "Passphrase: {$this->passphrase}";
|
||||
} else {
|
||||
$keyParams[] = '%no-protection';
|
||||
}
|
||||
|
||||
$keyParams[] = '%commit';
|
||||
return \implode("\n", $keyParams) . "\n";
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue