Page Menu
Home
GnuPG
Search
Configure Global Search
Log In
Files
F18826347
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Size
25 KB
Subscribers
None
View Options
diff --git a/src/keycache.cpp b/src/keycache.cpp
index 58e7c67..2331c89 100644
--- a/src/keycache.cpp
+++ b/src/keycache.cpp
@@ -1,942 +1,943 @@
/* @file keycache.cpp
* @brief Internal keycache
*
* Copyright (C) 2018 Intevation GmbH
*
* This file is part of GpgOL.
*
* GpgOL 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.
*
* GpgOL 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 program; if not, see <http://www.gnu.org/licenses/>.
*/
#include "keycache.h"
#include "common.h"
#include "cpphelp.h"
#include "mail.h"
#include <gpg-error.h>
#include <gpgme++/context.h>
#include <gpgme++/key.h>
#include <windows.h>
#include <set>
#include <unordered_map>
#include <sstream>
GPGRT_LOCK_DEFINE (keycache_lock);
GPGRT_LOCK_DEFINE (fpr_map_lock);
GPGRT_LOCK_DEFINE (update_lock);
static KeyCache* singleton = nullptr;
/** At some point we need to set a limit. There
seems to be no limit on how many recipients a mail
can have in outlook.
We would run out of resources or block.
50 Threads already seems a bit excessive but
it should really cover most legit use cases.
*/
#define MAX_LOCATOR_THREADS 50
static int s_thread_cnt;
namespace
{
class LocateArgs
{
public:
LocateArgs (const std::string& mbox, Mail *mail = nullptr):
m_mbox (mbox),
m_mail (mail)
{
s_thread_cnt++;
Mail::lockDelete ();
if (Mail::isValidPtr (m_mail))
{
m_mail->incrementLocateCount ();
}
Mail::unlockDelete ();
};
~LocateArgs()
{
s_thread_cnt--;
Mail::lockDelete ();
if (Mail::isValidPtr (m_mail))
{
m_mail->decrementLocateCount ();
}
Mail::unlockDelete ();
}
std::string m_mbox;
Mail *m_mail;
};
} // namespace
typedef std::pair<std::string, GpgME::Protocol> update_arg_t;
static DWORD WINAPI
do_update (LPVOID arg)
{
auto args = std::unique_ptr<update_arg_t> ((update_arg_t*) arg);
log_mime_parser ("%s:%s updating: \"%s\" with protocol %s",
SRCNAME, __func__, args->first.c_str (),
to_cstr (args->second));
auto ctx = std::unique_ptr<GpgME::Context> (GpgME::Context::createForProtocol
(args->second));
if (!ctx)
{
TRACEPOINT;
KeyCache::instance ()->onUpdateJobDone (args->first.c_str(),
GpgME::Key ());
return 0;
}
ctx->setKeyListMode (GpgME::KeyListMode::Local |
GpgME::KeyListMode::Signatures |
GpgME::KeyListMode::Validate |
GpgME::KeyListMode::WithTofu);
GpgME::Error err;
const auto newKey = ctx->key (args->first.c_str (), err, false);
TRACEPOINT;
if (newKey.isNull())
{
log_debug ("%s:%s Failed to find key for %s",
SRCNAME, __func__, args->first.c_str ());
}
if (err)
{
log_debug ("%s:%s Failed to find key for %s err: ",
SRCNAME, __func__, err.asString ());
}
KeyCache::instance ()->onUpdateJobDone (args->first.c_str(),
newKey);
log_debug ("%s:%s Update job done",
SRCNAME, __func__);
return 0;
}
class KeyCache::Private
{
public:
Private()
{
}
void setPgpKey(const std::string &mbox, const GpgME::Key &key)
{
gpgrt_lock_lock (&keycache_lock);
auto it = m_pgp_key_map.find (mbox);
if (it == m_pgp_key_map.end ())
{
m_pgp_key_map.insert (std::pair<std::string, GpgME::Key> (mbox, key));
}
else
{
it->second = key;
}
insertOrUpdateInFprMap (key);
gpgrt_lock_unlock (&keycache_lock);
}
void setSmimeKey(const std::string &mbox, const GpgME::Key &key)
{
gpgrt_lock_lock (&keycache_lock);
auto it = m_smime_key_map.find (mbox);
if (it == m_smime_key_map.end ())
{
m_smime_key_map.insert (std::pair<std::string, GpgME::Key> (mbox, key));
}
else
{
it->second = key;
}
insertOrUpdateInFprMap (key);
gpgrt_lock_unlock (&keycache_lock);
}
void setPgpKeySecret(const std::string &mbox, const GpgME::Key &key)
{
gpgrt_lock_lock (&keycache_lock);
auto it = m_pgp_skey_map.find (mbox);
if (it == m_pgp_skey_map.end ())
{
m_pgp_skey_map.insert (std::pair<std::string, GpgME::Key> (mbox, key));
}
else
{
it->second = key;
}
insertOrUpdateInFprMap (key);
gpgrt_lock_unlock (&keycache_lock);
}
void setSmimeKeySecret(const std::string &mbox, const GpgME::Key &key)
{
gpgrt_lock_lock (&keycache_lock);
auto it = m_smime_skey_map.find (mbox);
if (it == m_smime_skey_map.end ())
{
m_smime_skey_map.insert (std::pair<std::string, GpgME::Key> (mbox, key));
}
else
{
it->second = key;
}
insertOrUpdateInFprMap (key);
gpgrt_lock_unlock (&keycache_lock);
}
GpgME::Key getKey (const char *addr, GpgME::Protocol proto)
{
if (!addr)
{
return GpgME::Key();
}
auto mbox = GpgME::UserID::addrSpecFromString (addr);
if (proto == GpgME::OpenPGP)
{
gpgrt_lock_lock (&keycache_lock);
const auto it = m_pgp_key_map.find (mbox);
if (it == m_pgp_key_map.end ())
{
gpgrt_lock_unlock (&keycache_lock);
return GpgME::Key();
}
const auto ret = it->second;
gpgrt_lock_unlock (&keycache_lock);
return ret;
}
gpgrt_lock_lock (&keycache_lock);
const auto it = m_smime_key_map.find (mbox);
if (it == m_smime_key_map.end ())
{
gpgrt_lock_unlock (&keycache_lock);
return GpgME::Key();
}
const auto ret = it->second;
gpgrt_lock_unlock (&keycache_lock);
return ret;
}
GpgME::Key getSKey (const char *addr, GpgME::Protocol proto)
{
if (!addr)
{
return GpgME::Key();
}
auto mbox = GpgME::UserID::addrSpecFromString (addr);
if (proto == GpgME::OpenPGP)
{
gpgrt_lock_lock (&keycache_lock);
const auto it = m_pgp_skey_map.find (mbox);
if (it == m_pgp_skey_map.end ())
{
gpgrt_lock_unlock (&keycache_lock);
return GpgME::Key();
}
const auto ret = it->second;
gpgrt_lock_unlock (&keycache_lock);
return ret;
}
gpgrt_lock_lock (&keycache_lock);
const auto it = m_smime_skey_map.find (mbox);
if (it == m_smime_skey_map.end ())
{
gpgrt_lock_unlock (&keycache_lock);
return GpgME::Key();
}
const auto ret = it->second;
gpgrt_lock_unlock (&keycache_lock);
return ret;
}
GpgME::Key getSigningKey (const char *addr, GpgME::Protocol proto)
{
const auto key = getSKey (addr, proto);
if (key.isNull())
{
log_mime_parser ("%s:%s: secret key for %s is null",
SRCNAME, __func__, addr);
return key;
}
if (!key.canReallySign())
{
log_mime_parser ("%s:%s: Discarding key for %s because it can't sign",
SRCNAME, __func__, addr);
return GpgME::Key();
}
if (!key.hasSecret())
{
log_mime_parser ("%s:%s: Discarding key for %s because it has no secret",
SRCNAME, __func__, addr);
return GpgME::Key();
}
if (in_de_vs_mode () && !key.isDeVs())
{
log_mime_parser ("%s:%s: signing key for %s is not deVS",
SRCNAME, __func__, addr);
return GpgME::Key();
}
return key;
}
std::vector<GpgME::Key> getEncryptionKeys (const std::vector<std::string>
&recipients,
GpgME::Protocol proto)
{
std::vector<GpgME::Key> ret;
if (recipients.empty ())
{
TRACEPOINT;
return ret;
}
for (const auto &recip: recipients)
{
const auto key = getKey (recip.c_str (), proto);
if (key.isNull())
{
log_mime_parser ("%s:%s: No key for %s. no internal encryption",
SRCNAME, __func__, recip.c_str ());
return std::vector<GpgME::Key>();
}
if (!key.canEncrypt() || key.isRevoked() ||
key.isExpired() || key.isDisabled() || key.isInvalid())
{
log_mime_parser ("%s:%s: Invalid key for %s. no internal encryption",
SRCNAME, __func__, recip.c_str ());
return std::vector<GpgME::Key>();
}
if (in_de_vs_mode () && !key.isDeVs ())
{
log_mime_parser ("%s:%s: key for %s is not deVS",
SRCNAME, __func__, recip.c_str ());
return std::vector<GpgME::Key>();
}
bool validEnough = false;
/* Here we do the check if the key is valid for this recipient */
const auto addrSpec = GpgME::UserID::addrSpecFromString (recip.c_str ());
for (const auto &uid: key.userIDs ())
{
if (addrSpec != uid.addrSpec())
{
// Ignore unmatching addr specs
continue;
}
if (uid.validity() >= GpgME::UserID::Marginal ||
uid.origin() == GpgME::Key::OriginWKD)
{
validEnough = true;
break;
}
}
if (!validEnough)
{
log_mime_parser ("%s:%s: UID for %s does not have at least marginal trust",
SRCNAME, __func__, recip.c_str ());
return std::vector<GpgME::Key>();
}
// Accepting key
ret.push_back (key);
}
return ret;
}
void insertOrUpdateInFprMap (const GpgME::Key &key)
{
if (key.isNull() || !key.primaryFingerprint())
{
TRACEPOINT;
return;
}
gpgrt_lock_lock (&fpr_map_lock);
/* First ensure that we have the subkeys mapped to the primary
fpr */
const char *primaryFpr = key.primaryFingerprint ();
for (const auto &sub: key.subkeys())
{
const char *subFpr = sub.fingerprint();
auto it = m_sub_fpr_map.find (subFpr);
if (it == m_sub_fpr_map.end ())
{
m_sub_fpr_map.insert (std::make_pair(
std::string (subFpr),
std::string (primaryFpr)));
}
}
auto it = m_fpr_map.find (primaryFpr);
log_mime_parser ("%s:%s \"%s\" updated.",
SRCNAME, __func__, primaryFpr);
if (it == m_fpr_map.end ())
{
m_fpr_map.insert (std::make_pair (primaryFpr, key));
gpgrt_lock_unlock (&fpr_map_lock);
return;
}
if (it->second.hasSecret () && !key.hasSecret())
{
log_debug ("%s:%s Lost secret info on update. Merging.",
SRCNAME, __func__);
auto merged = key;
merged.mergeWith (it->second);
it->second = merged;
}
else
{
it->second = key;
}
gpgrt_lock_unlock (&fpr_map_lock);
return;
}
GpgME::Key getFromMap (const char *fpr) const
{
if (!fpr)
{
TRACEPOINT;
return GpgME::Key();
}
gpgrt_lock_lock (&fpr_map_lock);
std::string primaryFpr;
const auto it = m_sub_fpr_map.find (fpr);
if (it != m_sub_fpr_map.end ())
{
log_debug ("%s:%s using \"%s\" for \"%s\"",
SRCNAME, __func__, it->second.c_str(), fpr);
primaryFpr = it->second;
}
else
{
primaryFpr = fpr;
}
const auto keyIt = m_fpr_map.find (primaryFpr);
if (keyIt != m_fpr_map.end ())
{
+ const auto ret = keyIt->second;
gpgrt_lock_unlock (&fpr_map_lock);
- return keyIt->second;
+ return ret;
}
gpgrt_lock_unlock (&fpr_map_lock);
return GpgME::Key();
}
GpgME::Key getByFpr (const char *fpr, bool block) const
{
if (!fpr)
{
TRACEPOINT;
return GpgME::Key ();
}
TRACEPOINT;
const auto ret = getFromMap (fpr);
if (ret.isNull())
{
// If the key was not found we need to check if there is
// an update running.
if (block)
{
const std::string sFpr (fpr);
int i = 0;
gpgrt_lock_lock (&update_lock);
while (m_update_jobs.find(sFpr) != m_update_jobs.end ())
{
i++;
if (i % 100 == 0)
{
log_debug ("%s:%s Waiting on update for \"%s\"",
SRCNAME, __func__, fpr);
}
gpgrt_lock_unlock (&update_lock);
Sleep (10);
gpgrt_lock_lock (&update_lock);
if (i == 3000)
{
/* Just to be on the save side */
log_error ("%s:%s Waiting on update for \"%s\" "
"failed! Bug!",
SRCNAME, __func__, fpr);
break;
}
}
gpgrt_lock_unlock (&update_lock);
TRACEPOINT;
const auto ret2 = getFromMap (fpr);
if (ret2.isNull ())
{
log_debug ("%s:%s Cache miss after blocking check %s.",
SRCNAME, __func__, fpr);
}
else
{
log_debug ("%s:%s Cache hit after wait for %s.",
SRCNAME, __func__, fpr);
return ret2;
}
}
log_debug ("%s:%s Cache miss for %s.",
SRCNAME, __func__, fpr);
return GpgME::Key();
}
log_debug ("%s:%s Cache hit for %s.",
SRCNAME, __func__, fpr);
return ret;
}
void update (const char *fpr, GpgME::Protocol proto)
{
if (!fpr)
{
return;
}
const std::string sFpr (fpr);
gpgrt_lock_lock (&update_lock);
if (m_update_jobs.find(sFpr) != m_update_jobs.end ())
{
log_debug ("%s:%s Update for \"%s\" already in progress.",
SRCNAME, __func__, fpr);
gpgrt_lock_unlock (&update_lock);
}
m_update_jobs.insert (sFpr);
gpgrt_lock_unlock (&update_lock);
update_arg_t * args = new update_arg_t;
args->first = sFpr;
args->second = proto;
CloseHandle (CreateThread (NULL, 0, do_update,
(LPVOID) args, 0,
NULL));
}
void onUpdateJobDone (const char *fpr, const GpgME::Key &key)
{
if (!fpr)
{
return;
}
TRACEPOINT;
insertOrUpdateInFprMap (key);
gpgrt_lock_lock (&update_lock);
const auto it = m_update_jobs.find(fpr);
if (it == m_update_jobs.end())
{
log_error ("%s:%s Update for \"%s\" already finished.",
SRCNAME, __func__, fpr);
gpgrt_lock_unlock (&update_lock);
return;
}
m_update_jobs.erase (it);
gpgrt_lock_unlock (&update_lock);
TRACEPOINT;
return;
}
std::unordered_map<std::string, GpgME::Key> m_pgp_key_map;
std::unordered_map<std::string, GpgME::Key> m_smime_key_map;
std::unordered_map<std::string, GpgME::Key> m_pgp_skey_map;
std::unordered_map<std::string, GpgME::Key> m_smime_skey_map;
std::unordered_map<std::string, GpgME::Key> m_fpr_map;
std::unordered_map<std::string, std::string> m_sub_fpr_map;
std::set<std::string> m_update_jobs;
};
KeyCache::KeyCache():
d(new Private)
{
}
KeyCache *
KeyCache::instance ()
{
if (!singleton)
{
singleton = new KeyCache();
}
return singleton;
}
GpgME::Key
KeyCache::getSigningKey (const char *addr, GpgME::Protocol proto) const
{
return d->getSigningKey (addr, proto);
}
std::vector<GpgME::Key>
KeyCache::getEncryptionKeys (const std::vector<std::string> &recipients, GpgME::Protocol proto) const
{
return d->getEncryptionKeys (recipients, proto);
}
static DWORD WINAPI
do_locate (LPVOID arg)
{
if (!arg)
{
return 0;
}
auto args = std::unique_ptr<LocateArgs> ((LocateArgs *) arg);
const auto addr = args->m_mbox;
log_mime_parser ("%s:%s searching key for addr: \"%s\"",
SRCNAME, __func__, addr.c_str());
const auto k = GpgME::Key::locate (addr.c_str());
if (!k.isNull ())
{
log_mime_parser ("%s:%s found key for addr: \"%s\":%s",
SRCNAME, __func__, addr.c_str(),
k.primaryFingerprint());
KeyCache::instance ()->setPgpKey (addr, k);
}
if (opt.enable_smime)
{
auto ctx = std::unique_ptr<GpgME::Context> (
GpgME::Context::createForProtocol (GpgME::CMS));
if (!ctx)
{
TRACEPOINT;
return 0;
}
// We need to validate here to fetch CRL's
ctx->setKeyListMode (GpgME::KeyListMode::Local |
GpgME::KeyListMode::Validate |
GpgME::KeyListMode::Signatures);
GpgME::Error e = ctx->startKeyListing (addr.c_str());
if (e)
{
TRACEPOINT;
return 0;
}
std::vector<GpgME::Key> keys;
GpgME::Error err;
do {
keys.push_back(ctx->nextKey(err));
} while (!err);
keys.pop_back();
GpgME::Key candidate;
for (const auto &key: keys)
{
if (key.isRevoked() || key.isExpired() ||
key.isDisabled() || key.isInvalid())
{
log_mime_parser ("%s:%s: Skipping invalid S/MIME key",
SRCNAME, __func__);
continue;
}
if (candidate.isNull() || !candidate.numUserIDs())
{
if (key.numUserIDs() &&
candidate.userID(0).validity() <= key.userID(0).validity())
{
candidate = key;
}
}
}
if (!candidate.isNull())
{
log_mime_parser ("%s:%s found SMIME key for addr: \"%s\":%s",
SRCNAME, __func__, addr.c_str(),
candidate.primaryFingerprint());
KeyCache::instance()->setSmimeKey (addr, candidate);
}
}
log_debug ("%s:%s locator thread done",
SRCNAME, __func__);
return 0;
}
static void
locate_secret (const char *addr, GpgME::Protocol proto)
{
auto ctx = std::unique_ptr<GpgME::Context> (
GpgME::Context::createForProtocol (proto));
if (!ctx)
{
TRACEPOINT;
return;
}
if (!addr)
{
TRACEPOINT;
return;
}
const auto mbox = GpgME::UserID::addrSpecFromString (addr);
if (mbox.empty())
{
log_debug ("%s:%s: Empty mbox for addr %s",
SRCNAME, __func__, addr);
return;
}
// We need to validate here to fetch CRL's
ctx->setKeyListMode (GpgME::KeyListMode::Local |
GpgME::KeyListMode::Validate);
GpgME::Error e = ctx->startKeyListing (mbox.c_str(), true);
if (e)
{
TRACEPOINT;
return;
}
std::vector<GpgME::Key> keys;
GpgME::Error err;
do
{
const auto key = ctx->nextKey(err);
if (key.isNull())
{
continue;
}
if (key.isRevoked() || key.isExpired() ||
key.isDisabled() || key.isInvalid())
{
if ((opt.enable_debug & DBG_MIME_PARSER))
{
std::stringstream ss;
ss << key;
log_mime_parser ("%s:%s: Skipping invalid secret key %s",
SRCNAME, __func__, ss.str().c_str());
}
continue;
}
if (proto == GpgME::OpenPGP)
{
log_mime_parser ("%s:%s found pgp skey for addr: \"%s\":%s",
SRCNAME, __func__, mbox.c_str(),
key.primaryFingerprint());
KeyCache::instance()->setPgpKeySecret (mbox, key);
return;
}
if (proto == GpgME::CMS)
{
log_mime_parser ("%s:%s found cms skey for addr: \"%s\":%s",
SRCNAME, __func__, mbox.c_str (),
key.primaryFingerprint());
KeyCache::instance()->setSmimeKeySecret (mbox, key);
return;
}
} while (!err);
return;
}
static DWORD WINAPI
do_locate_secret (LPVOID arg)
{
auto args = std::unique_ptr<LocateArgs> ((LocateArgs *) arg);
log_mime_parser ("%s:%s searching secret key for addr: \"%s\"",
SRCNAME, __func__, args->m_mbox.c_str ());
locate_secret (args->m_mbox.c_str(), GpgME::OpenPGP);
if (opt.enable_smime)
{
locate_secret (args->m_mbox.c_str(), GpgME::CMS);
}
log_debug ("%s:%s locator sthread thread done",
SRCNAME, __func__);
return 0;
}
void
KeyCache::startLocate (const std::vector<std::string> &addrs, Mail *mail) const
{
for (const auto &addr: addrs)
{
startLocate (addr.c_str(), mail);
}
}
void
KeyCache::startLocate (const char *addr, Mail *mail) const
{
if (!addr)
{
TRACEPOINT;
return;
}
std::string recp = GpgME::UserID::addrSpecFromString (addr);
if (recp.empty ())
{
return;
}
gpgrt_lock_lock (&keycache_lock);
if (d->m_pgp_key_map.find (recp) == d->m_pgp_key_map.end ())
{
// It's enough to look at the PGP Key map. We marked
// searched keys there.
d->m_pgp_key_map.insert (std::pair<std::string, GpgME::Key> (recp, GpgME::Key()));
log_debug ("%s:%s Creating a locator thread",
SRCNAME, __func__);
const auto args = new LocateArgs(recp, mail);
HANDLE thread = CreateThread (NULL, 0, do_locate,
args, 0,
NULL);
CloseHandle (thread);
}
gpgrt_lock_unlock (&keycache_lock);
}
void
KeyCache::startLocateSecret (const char *addr, Mail *mail) const
{
if (!addr)
{
TRACEPOINT;
return;
}
std::string recp = GpgME::UserID::addrSpecFromString (addr);
if (recp.empty ())
{
return;
}
gpgrt_lock_lock (&keycache_lock);
if (d->m_pgp_skey_map.find (recp) == d->m_pgp_skey_map.end ())
{
// It's enough to look at the PGP Key map. We marked
// searched keys there.
d->m_pgp_skey_map.insert (std::pair<std::string, GpgME::Key> (recp, GpgME::Key()));
log_debug ("%s:%s Creating a locator thread",
SRCNAME, __func__);
const auto args = new LocateArgs(recp, mail);
HANDLE thread = CreateThread (NULL, 0, do_locate_secret,
(LPVOID) args, 0,
NULL);
CloseHandle (thread);
}
gpgrt_lock_unlock (&keycache_lock);
}
void
KeyCache::setSmimeKey(const std::string &mbox, const GpgME::Key &key)
{
d->setSmimeKey(mbox, key);
}
void
KeyCache::setPgpKey(const std::string &mbox, const GpgME::Key &key)
{
d->setPgpKey(mbox, key);
}
void
KeyCache::setSmimeKeySecret(const std::string &mbox, const GpgME::Key &key)
{
d->setSmimeKeySecret(mbox, key);
}
void
KeyCache::setPgpKeySecret(const std::string &mbox, const GpgME::Key &key)
{
d->setPgpKeySecret(mbox, key);
}
bool
KeyCache::isMailResolvable(Mail *mail)
{
/* Get the data from the mail. */
const auto sender = mail->getSender ();
auto recps = mail->getCachedRecipients ();
if (sender.empty() || recps.empty())
{
log_debug ("%s:%s: Mail has no sender or no recipients.",
SRCNAME, __func__);
return false;
}
std::vector<GpgME::Key> encKeys = getEncryptionKeys (recps,
GpgME::OpenPGP);
if (!encKeys.empty())
{
return true;
}
if (!opt.enable_smime)
{
return false;
}
/* Check S/MIME instead here we need to include the sender
as we can't just generate a key. */
recps.push_back (sender);
GpgME::Key sigKey= getSigningKey (sender.c_str(), GpgME::CMS);
encKeys = getEncryptionKeys (recps, GpgME::CMS);
return !encKeys.empty() && !sigKey.isNull();
}
void
KeyCache::update (const char *fpr, GpgME::Protocol proto)
{
d->update (fpr, proto);
}
GpgME::Key
KeyCache::getByFpr (const char *fpr, bool block) const
{
return d->getByFpr (fpr, block);
}
void
KeyCache::onUpdateJobDone (const char *fpr, const GpgME::Key &key)
{
return d->onUpdateJobDone (fpr, key);
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Dec 23, 5:08 PM (10 h, 18 m)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
91/92/881370ceacc9c3de828132e562cf
Attached To
rO GpgOL
Event Timeline
Log In to Comment