Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F6068207
KeyGenerator.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
28 KB
Referenced Files
None
Subscribers
None
KeyGenerator.php
View Options
<?php
/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
/**
* Crypt_GPG is a package to use GPG from PHP
*
* This file contains an object that handles GnuPG key generation.
*
* PHP version 5
*
* LICENSE:
*
* This library is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of the
* License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* @category Encryption
* @package Crypt_GPG
* @author Michael Gauthier <mike@silverorange.com>
* @copyright 2011-2013 silverorange
* @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
* @version CVS: $Id:$
* @link http://pear.php.net/package/Crypt_GPG
* @link http://www.gnupg.org/
*/
/**
* Base class for GPG methods
*/
require_once
'Crypt/GPGAbstract.php'
;
/**
* Status output handler for key generation
*/
require_once
'Crypt/GPG/KeyGeneratorStatusHandler.php'
;
/**
* Error output handler for key generation
*/
require_once
'Crypt/GPG/KeyGeneratorErrorHandler.php'
;
// {{{ class Crypt_GPG_KeyGenerator
/**
* GnuPG key generator
*
* This class provides an object oriented interface for generating keys with
* the GNU Privacy Guard (GPG).
*
* Secure key generation requires true random numbers, and as such can be slow.
* If the operating system runs out of entropy, key generation will block until
* more entropy is available.
*
* If quick key generation is important, a hardware entropy generator, or an
* entropy gathering daemon may be installed. For example, administrators of
* Debian systems may want to install the 'randomsound' package.
*
* This class uses the experimental automated key generation support available
* in GnuPG. See <b>doc/DETAILS</b> in the
* {@link http://www.gnupg.org/download/ GPG distribution} for detailed
* information on the key generation format.
*
* @category Encryption
* @package Crypt_GPG
* @author Nathan Fredrickson <nathan@silverorange.com>
* @author Michael Gauthier <mike@silverorange.com>
* @copyright 2005-2013 silverorange
* @license http://www.gnu.org/copyleft/lesser.html LGPL License 2.1
* @link http://pear.php.net/package/Crypt_GPG
* @link http://www.gnupg.org/
*/
class
Crypt_GPG_KeyGenerator
extends
Crypt_GPGAbstract
{
// {{{ protected properties
/**
* The expiration date of generated keys
*
* @var integer
*
* @see Crypt_GPG_KeyGenerator::setExpirationDate()
*/
protected
$expirationDate
=
0
;
/**
* The passphrase of generated keys
*
* @var string
*
* @see Crypt_GPG_KeyGenerator::setPassphrase()
*/
protected
$passphrase
=
''
;
/**
* The algorithm for generated primary keys
*
* @var integer
*
* @see Crypt_GPG_KeyGenerator::setKeyParams()
*/
protected
$keyAlgorithm
=
Crypt_GPG_SubKey
::
ALGORITHM_DSA
;
/**
* The size of generated primary keys
*
* @var integer
*
* @see Crypt_GPG_KeyGenerator::setKeyParams()
*/
protected
$keySize
=
1024
;
/**
* The usages of generated primary keys
*
* This is a bitwise combination of the usage constants in
* {@link Crypt_GPG_SubKey}.
*
* @var integer
*
* @see Crypt_GPG_KeyGenerator::setKeyParams()
*/
protected
$keyUsage
=
6
;
// USAGE_SIGN | USAGE_CERTIFY
/**
* The algorithm for generated sub-keys
*
* @var integer
*
* @see Crypt_GPG_KeyGenerator::setSubKeyParams()
*/
protected
$subKeyAlgorithm
=
Crypt_GPG_SubKey
::
ALGORITHM_ELGAMAL_ENC
;
/**
* The size of generated sub-keys
*
* @var integer
*
* @see Crypt_GPG_KeyGenerator::setSubKeyParams()
*/
protected
$subKeySize
=
2048
;
/**
* The usages of generated sub-keys
*
* This is a bitwise combination of the usage constants in
* {@link Crypt_GPG_SubKey}.
*
* @var integer
*
* @see Crypt_GPG_KeyGenerator::setSubKeyParams()
*/
protected
$subKeyUsage
=
Crypt_GPG_SubKey
::
USAGE_ENCRYPT
;
/**
* The GnuPG status handler to use for key generation
*
* @var Crypt_GPG_KeyGeneratorStatusHandler
*
* @see Crypt_GPG_KeyGenerator::setStatusHandler()
*/
protected
$statusHandler
=
null
;
/**
* The GnuPG error handler to use for key generation
*
* @var Crypt_GPG_KeyGeneratorErrorHandler
*
* @see Crypt_GPG_KeyGenerator::setErrorHandler()
*/
protected
$errorHandler
=
null
;
// }}}
// {{{ __construct()
/**
* Creates a new GnuPG key generator
*
* Available options are:
*
* - <kbd>string homedir</kbd> - the directory where the GPG
* keyring files are stored. If not
* specified, Crypt_GPG uses the
* default of <kbd>~/.gnupg</kbd>.
* - <kbd>string publicKeyring</kbd> - the file path of the public
* keyring. Use this if the public
* keyring is not in the homedir, or
* if the keyring is in a directory
* not writable by the process
* invoking GPG (like Apache). Then
* you can specify the path to the
* keyring with this option
* (/foo/bar/pubring.gpg), and specify
* a writable directory (like /tmp)
* using the <i>homedir</i> option.
* - <kbd>string privateKeyring</kbd> - the file path of the private
* keyring. Use this if the private
* keyring is not in the homedir, or
* if the keyring is in a directory
* not writable by the process
* invoking GPG (like Apache). Then
* you can specify the path to the
* keyring with this option
* (/foo/bar/secring.gpg), and specify
* a writable directory (like /tmp)
* using the <i>homedir</i> option.
* - <kbd>string trustDb</kbd> - the file path of the web-of-trust
* database. Use this if the trust
* database is not in the homedir, or
* if the database is in a directory
* not writable by the process
* invoking GPG (like Apache). Then
* you can specify the path to the
* trust database with this option
* (/foo/bar/trustdb.gpg), and specify
* a writable directory (like /tmp)
* using the <i>homedir</i> option.
* - <kbd>string binary</kbd> - the location of the GPG binary. If
* not specified, the driver attempts
* to auto-detect the GPG binary
* location using a list of known
* default locations for the current
* operating system. The option
* <kbd>gpgBinary</kbd> is a
* deprecated alias for this option.
* - <kbd>string agent</kbd> - the location of the GnuPG agent
* binary. The gpg-agent is only
* used for GnuPG 2.x. If not
* specified, the engine attempts
* to auto-detect the gpg-agent
* binary location using a list of
* know default locations for the
* current operating system.
* - <kbd>boolean debug</kbd> - whether or not to use debug mode.
* When debug mode is on, all
* communication to and from the GPG
* subprocess is logged. This can be
*
* @param array $options optional. An array of options used to create the
* GPG object. All options are optional and are
* represented as key-value pairs.
*
* @throws Crypt_GPG_FileException if the <kbd>homedir</kbd> does not exist
* and cannot be created. This can happen if <kbd>homedir</kbd> is
* not specified, Crypt_GPG is run as the web user, and the web
* user has no home directory. This exception is also thrown if any
* of the options <kbd>publicKeyring</kbd>,
* <kbd>privateKeyring</kbd> or <kbd>trustDb</kbd> options are
* specified but the files do not exist or are are not readable.
* This can happen if the user running the Crypt_GPG process (for
* example, the Apache user) does not have permission to read the
* files.
*
* @throws PEAR_Exception if the provided <kbd>binary</kbd> is invalid, or
* if no <kbd>binary</kbd> is provided and no suitable binary could
* be found.
*
* @throws PEAR_Exception if the provided <kbd>agent</kbd> is invalid, or
* if no <kbd>agent</kbd> is provided and no suitable gpg-agent
* cound be found.
*/
public
function
__construct
(
array
$options
=
array
())
{
parent
::
__construct
(
$options
);
$this
->
statusHandler
=
new
Crypt_GPG_KeyGeneratorStatusHandler
();
$this
->
errorHandler
=
new
Crypt_GPG_KeyGeneratorErrorHandler
();
}
// }}}
// {{{ setExpirationDate()
/**
* Sets the expiration date of generated keys
*
* @param string|integer $date either a string that may be parsed by
* PHP's strtotime() function, or an integer
* timestamp representing the number of seconds
* since the UNIX epoch. This date must be at
* least one date in the future. Keys that
* expire in the past may not be generated. Use
* an expiration date of 0 for keys that do not
* expire.
*
* @throws InvalidArgumentException if the date is not a valid format, or
* if the date is not at least one day in
* the future, or if the date is greater
* than 2038-01-19T03:14:07.
*
* @return Crypt_GPG_KeyGenerator the current object, for fluent interface.
*/
public
function
setExpirationDate
(
$date
)
{
if
(
is_int
(
$date
)
||
ctype_digit
(
strval
(
$date
)))
{
$expirationDate
=
intval
(
$date
);
}
else
{
$expirationDate
=
strtotime
(
$date
);
}
if
(
$expirationDate
===
false
)
{
throw
new
InvalidArgumentException
(
sprintf
(
'Invalid expiration date format: "%s". Please use a '
.
'format compatible with PHP
\'
s strtotime().'
,
$date
)
);
}
if
(
$expirationDate
!==
0
&&
$expirationDate
<
time
()
+
86400
)
{
throw
new
InvalidArgumentException
(
'Expiration date must be at least a day in the future.'
);
}
// GnuPG suffers from the 2038 bug
if
(
$expirationDate
>
2147483647
)
{
throw
new
InvalidArgumentException
(
'Expiration date must not be greater than 2038-01-19T03:14:07.'
);
}
$this
->
expirationDate
=
$expirationDate
;
return
$this
;
}
// }}}
// {{{ setPassphrase()
/**
* Sets the passphrase of generated keys
*
* @param string $passphrase the passphrase to use for generated keys. Use
* null or an empty string for no passphrase.
*
* @return Crypt_GPG_KeyGenerator the current object, for fluent interface.
*/
public
function
setPassphrase
(
$passphrase
)
{
$this
->
passphrase
=
strval
(
$passphrase
);
return
$this
;
}
// }}}
// {{{ setKeyParams()
/**
* Sets the parameters for the primary key of generated key-pairs
*
* @param integer $algorithm the algorithm used by the key. This should be
* one of the Crypt_GPG_SubKey::ALGORITHM_*
* constants.
* @param integer $size optional. The size of the key. Different
* algorithms have different size requirements.
* If not specified, the default size for the
* specified algorithm will be used. If an
* invalid key size is used, GnuPG will do its
* best to round it to a valid size.
* @param integer $usage optional. A bitwise combination of key usages.
* If not specified, the primary key will be used
* only to sign and certify. This is the default
* behavior of GnuPG in interactive mode. Use
* the Crypt_GPG_SubKey::USAGE_* constants here.
* The primary key may be used to certify even
* if the certify usage is not specified.
*
* @return Crypt_GPG_KeyGenerator the current object, for fluent interface.
*/
public
function
setKeyParams
(
$algorithm
,
$size
=
0
,
$usage
=
0
)
{
$apgorithm
=
intval
(
$algorithm
);
if
(
$algorithm
===
Crypt_GPG_SubKey
::
ALGORITHM_ELGAMAL_ENC
)
{
throw
new
Crypt_GPG_InvalidKeyParamsException
(
'Primary key algorithm must be capable of signing. The '
.
'Elgamal algorithm can only encrypt.'
,
0
,
$algorithm
,
$size
,
$usage
);
}
if
(
$size
!=
0
)
{
$size
=
intval
(
$size
);
}
if
(
$usage
!=
0
)
{
$usage
=
intval
(
$usage
);
}
$usageEncrypt
=
Crypt_GPG_SubKey
::
USAGE_ENCRYPT
;
if
(
$algorithm
===
Crypt_GPG_SubKey
::
ALGORITHM_DSA
&&
(
$usage
&
$usageEncrypt
)
===
$usageEncrypt
)
{
throw
new
Crypt_GPG_InvalidKeyParamsException
(
'The DSA algorithm is not capable of encrypting. Please '
.
'specify a different algorithm or do not include encryption '
.
'as a usage for the primary key.'
,
0
,
$algorithm
,
$size
,
$usage
);
}
$this
->
keyAlgorithm
=
$algorithm
;
if
(
$size
!=
0
)
{
$this
->
keySize
=
$size
;
}
if
(
$usage
!=
0
)
{
$this
->
keyUsage
=
$usage
;
}
return
$this
;
}
// }}}
// {{{ setSubKeyParams()
/**
* Sets the parameters for the sub-key of generated key-pairs
*
* @param integer $algorithm the algorithm used by the key. This should be
* one of the Crypt_GPG_SubKey::ALGORITHM_*
* constants.
* @param integer $size optional. The size of the key. Different
* algorithms have different size requirements.
* If not specified, the default size for the
* specified algorithm will be used. If an
* invalid key size is used, GnuPG will do its
* best to round it to a valid size.
* @param integer $usage optional. A bitwise combination of key usages.
* If not specified, the sub-key will be used
* only to encrypt. This is the default behavior
* of GnuPG in interactive mode. Use the
* Crypt_GPG_SubKey::USAGE_* constants here.
*
* @return Crypt_GPG_KeyGenerator the current object, for fluent interface.
*/
public
function
setSubKeyParams
(
$algorithm
,
$size
=
''
,
$usage
=
0
)
{
$apgorithm
=
intval
(
$algorithm
);
if
(
$size
!=
0
)
{
$size
=
intval
(
$size
);
}
if
(
$usage
!=
0
)
{
$usage
=
intval
(
$usage
);
}
$usageSign
=
Crypt_GPG_SubKey
::
USAGE_SIGN
;
if
(
$algorithm
===
Crypt_GPG_SubKey
::
ALGORITHM_ELGAMAL_ENC
&&
(
$usage
&
$usageSign
)
===
$usageSign
)
{
throw
new
Crypt_GPG_InvalidKeyParamsException
(
'The Elgamal algorithm is not capable of signing. Please '
.
'specify a different algorithm or do not include signing '
.
'as a usage for the sub-key.'
,
0
,
$algorithm
,
$size
,
$usage
);
}
$usageEncrypt
=
Crypt_GPG_SubKey
::
USAGE_ENCRYPT
;
if
(
$algorithm
===
Crypt_GPG_SubKey
::
ALGORITHM_DSA
&&
(
$usage
&
$usageEncrypt
)
===
$usageEncrypt
)
{
throw
new
Crypt_GPG_InvalidKeyParamsException
(
'The DSA algorithm is not capable of encrypting. Please '
.
'specify a different algorithm or do not include encryption '
.
'as a usage for the sub-key.'
,
0
,
$algorithm
,
$size
,
$usage
);
}
$this
->
subKeyAlgorithm
=
$algorithm
;
if
(
$size
!=
0
)
{
$this
->
subKeySize
=
$size
;
}
if
(
$usage
!=
0
)
{
$this
->
subKeyUsage
=
$usage
;
}
return
$this
;
}
// }}}
// {{{ setStatusHandler()
/**
* Sets the status handler to use for key generation
*
* Normally this method does not need to be used. It provides a means for
* dependency injection.
*
* @param Crypt_GPG_KeyStatusHandler $handler the key status handler to
* use.
*
* @return Crypt_GPG_KeyGenerator the current object, for fluent interface.
*/
public
function
setStatusHandler
(
Crypt_GPG_KeyGeneratorStatusHandler
$handler
)
{
$this
->
statusHandler
=
$handler
;
return
$this
;
}
// }}}
// {{{ setErrorHandler()
/**
* Sets the error handler to use for key generation
*
* Normally this method does not need to be used. It provides a means for
* dependency injection.
*
* @param Crypt_GPG_KeyErrorHandler $handler the key error handler to
* use.
*
* @return Crypt_GPG_KeyGenerator the current object, for fluent interface.
*/
public
function
setErrorHandler
(
Crypt_GPG_KeyGeneratorErrorHandler
$handler
)
{
$this
->
errorHandler
=
$handler
;
return
$this
;
}
// }}}
// {{{ generateKey()
/**
* Generates a new key-pair in the current keyring
*
* Secure key generation requires true random numbers, and as such can be
* solw. If the operating system runs out of entropy, key generation will
* block until more entropy is available.
*
* If quick key generation is important, a hardware entropy generator, or
* an entropy gathering daemon may be installed. For example,
* administrators of Debian systems may want to install the 'randomsound'
* package.
*
* @param string|Crypt_GPG_UserId $name either a {@link Crypt_GPG_UserId}
* object, or a string containing
* the name of the user id.
* @param string $email optional. If <i>$name</i> is
* specified as a string, this is
* the email address of the user id.
* @param string $comment optional. If <i>$name</i> is
* specified as a string, this is
* the comment of the user id.
*
* @return Crypt_GPG_Key the newly generated key.
*
* @throws Crypt_GPG_KeyNotCreatedException if the key parameters are
* incorrect, if an unknown error occurs during key generation, or
* if the newly generated key is not found in the keyring.
*
* @throws Crypt_GPG_Exception if an unknown or unexpected error occurs.
* Use the <kbd>debug</kbd> option and file a bug report if these
* exceptions occur.
*/
public
function
generateKey
(
$name
,
$email
=
''
,
$comment
=
''
)
{
$handle
=
uniqid
(
'key'
,
true
);
$userId
=
$this
->
getUserId
(
$name
,
$email
,
$comment
);
$keyParams
=
array
(
'Key-Type'
=>
$this
->
keyAlgorithm
,
'Key-Length'
=>
$this
->
keySize
,
'Key-Usage'
=>
$this
->
getUsage
(
$this
->
keyUsage
),
'Subkey-Type'
=>
$this
->
subKeyAlgorithm
,
'Subkey-Length'
=>
$this
->
subKeySize
,
'Subkey-Usage'
=>
$this
->
getUsage
(
$this
->
subKeyUsage
),
'Name-Real'
=>
$userId
->
getName
(),
'Handle'
=>
$handle
,
);
if
(
$this
->
expirationDate
!=
0
)
{
// GnuPG only accepts granularity of days
$expirationDate
=
date
(
'Y-m-d'
,
$this
->
expirationDate
);
$keyParams
[
'Expire-Date'
]
=
$expirationDate
;
}
if
(
$this
->
passphrase
!=
''
)
{
$keyParams
[
'Passphrase'
]
=
$this
->
passphrase
;
}
if
(
$userId
->
getEmail
()
!=
''
)
{
$keyParams
[
'Name-Email'
]
=
$userId
->
getEmail
();
}
if
(
$userId
->
getComment
()
!=
''
)
{
$keyParams
[
'Name-Comment'
]
=
$userId
->
getComment
();
}
$keyParamsFormatted
=
array
();
foreach
(
$keyParams
as
$name
=>
$value
)
{
$keyParamsFormatted
[]
=
$name
.
': '
.
$value
;
}
$input
=
implode
(
"
\n
"
,
$keyParamsFormatted
)
.
"
\n
%commit
\n
"
;
$statusHandler
=
clone
$this
->
statusHandler
;
$statusHandler
->
setHandle
(
$handle
);
$errorHandler
=
clone
$this
->
errorHandler
;
$this
->
engine
->
reset
();
$this
->
engine
->
addStatusHandler
(
array
(
$statusHandler
,
'handle'
));
$this
->
engine
->
addErrorHandler
(
array
(
$errorHandler
,
'handle'
));
$this
->
engine
->
setInput
(
$input
);
$this
->
engine
->
setOutput
(
$output
);
$this
->
engine
->
setOperation
(
'--gen-key'
,
array
(
'--batch'
));
$this
->
engine
->
run
();
$code
=
$errorHandler
->
getErrorCode
();
switch
(
$code
)
{
case
self
::
ERROR_BAD_KEY_PARAMS
:
switch
(
$errorHandler
->
getLineNumber
())
{
case
1
:
throw
new
Crypt_GPG_InvalidKeyParamsException
(
'Invalid primary key algorithm specified.'
,
0
,
$this
->
keyAlgorithm
,
$this
->
keySize
,
$this
->
keyUsage
);
case
4
:
throw
new
Crypt_GPG_InvalidKeyParamsException
(
'Invalid sub-key algorithm specified.'
,
0
,
$this
->
subKeyAlgorithm
,
$this
->
subKeySize
,
$this
->
subKeyUsage
);
default
:
throw
new
Crypt_GPG_InvalidKeyParamsException
(
'Invalid key algorithm specified.'
);
}
}
$code
=
$this
->
engine
->
getErrorCode
();
switch
(
$code
)
{
case
self
::
ERROR_NONE
:
break
;
default
:
throw
new
Crypt_GPG_Exception
(
'Unknown error generating key-pair. Please use the
\'
debug
\'
'
.
'option when creating the Crypt_GPG object, and file a bug '
.
'report at '
.
self
::
BUG_URI
,
$code
);
}
$code
=
$statusHandler
->
getErrorCode
();
switch
(
$code
)
{
case
self
::
ERROR_NONE
:
break
;
case
self
::
ERROR_KEY_NOT_CREATED
:
throw
new
Crypt_GPG_KeyNotCreatedException
(
'Unable to create new key-pair. Invalid key parameters. '
.
'Make sure the specified key algorithms and sizes are '
.
'correct.'
,
$code
);
}
$fingerprint
=
$statusHandler
->
getKeyFingerprint
();
$keys
=
$this
->
_getKeys
(
$fingerprint
);
if
(
count
(
$keys
)
===
0
)
{
throw
new
Crypt_GPG_KeyNotCreatedException
(
sprintf
(
'Newly created key "%s" not found in keyring.'
,
$fingerprint
)
);
}
return
$keys
[
0
];
}
// }}}
// {{{ getUsage()
/**
* Builds a GnuPG key usage string suitable for key generation
*
* See <b>doc/DETAILS</b> in the
* {@link http://www.gnupg.org/download/ GPG distribution} for detailed
* information on the key usage format.
*
* @param integer $usage a bitwise combination of the key usages. This is
* a combination of the Crypt_GPG_SubKey::USAGE_*
* constants.
*
* @return string the key usage string.
*/
protected
function
getUsage
(
$usage
)
{
$map
=
array
(
Crypt_GPG_SubKey
::
USAGE_ENCRYPT
=>
'encrypt'
,
Crypt_GPG_SubKey
::
USAGE_SIGN
=>
'sign'
,
Crypt_GPG_SubKey
::
USAGE_CERTIFY
=>
'cert'
,
Crypt_GPG_SubKey
::
USAGE_AUTHENTICATION
=>
'auth'
,
);
// cert is always used for primary keys and does not need to be
// specified
$usage
&=
~
Crypt_GPG_SubKey
::
USAGE_CERTIFY
;
$usageArray
=
array
();
foreach
(
$map
as
$key
=>
$value
)
{
if
((
$usage
&
$key
)
===
$key
)
{
$usageArray
[]
=
$value
;
}
}
return
implode
(
','
,
$usageArray
);
}
// }}}
// {{{ getUserId()
/**
* Gets a user id object from parameters
*
* @param string|Crypt_GPG_UserId $name either a {@link Crypt_GPG_UserId}
* object, or a string containing
* the name of the user id.
* @param string $email optional. If <i>$name</i> is
* specified as a string, this is
* the email address of the user id.
* @param string $comment optional. If <i>$name</i> is
* specified as a string, this is
* the comment of the user id.
*
* @return Crypt_GPG_UserId a user id object for the specified parameters.
*/
protected
function
getUserId
(
$name
,
$email
=
''
,
$comment
=
''
)
{
if
(
$name
instanceof
Crypt_GPG_UserId
)
{
$userId
=
$name
;
}
else
{
$userId
=
new
Crypt_GPG_UserId
();
$userId
->
setName
(
$name
)->
setEmail
(
$email
)->
setComment
(
$comment
);
}
return
$userId
;
}
// }}}
}
// }}}
?>
File Metadata
Details
Attached
Mime Type
text/x-php
Expires
Fri, May 22, 5:31 AM (11 h, 32 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
541466
Default Alt Text
KeyGenerator.php (28 KB)
Attached To
Mode
R3 roundcubemail
Attached
Detach File
Event Timeline
Log In to Comment