Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Long-term crypto support #1264

Open
wants to merge 27 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
9680baa
Changed CKey handle to shared pointer
Apr 9, 2024
21b7968
Implemented password-based decryption flow
May 9, 2024
8d127f1
Quick'n'dirty first version that actually encrypts and decrypts with …
May 13, 2024
907bf68
Fixes and cleanups for LT crypto
May 14, 2024
de64734
Cleanups
May 14, 2024
0ef8c50
Merge branch 'cdoc2' of github.com:lauris71/DigiDoc4-Client into cdoc2
May 16, 2024
c4f866d
removed encryptLT
May 16, 2024
3137c43
Merge commit 'ccb29a5f93c22ca36b06dea98e2ae2128bc7f445' into cdoc2
May 16, 2024
059a62d
Fixed include filename capitalization
May 16, 2024
f77decc
Fixed CKey struct/class confusion and made PasswordDialog Qt5 compatible
May 16, 2024
e747ed5
Some more fixes for ubuntu20
May 16, 2024
f3379d4
Fixed ambiguous overload
May 16, 2024
49c0fed
Use PBKDF from openssl instead of Qt
May 16, 2024
dde8a82
Removed EVP_KDF_HKDF_MODE_EXTRACT_AND_EXPAND, replaced with 2-step ex…
May 16, 2024
cda361d
Fixed indentation and CodeQL warnings
May 17, 2024
1347dfb
Reverted some indentation changes for clarity
May 17, 2024
c9ed433
Apply suggestions from code review
lauris71 Jun 3, 2024
5a0d798
Reverted more cdoc1/cdoc2 whitespace
Jun 7, 2024
cc1203e
More whitespace reverts
Jun 7, 2024
d79dc50
Apply suggestions from code review
lauris71 Jul 14, 2024
b1230a0
Update client/CDoc1.cpp
lauris71 Jul 14, 2024
f9c585a
Update client/Crypto.cpp
lauris71 Jul 15, 2024
3e9db88
Fixed some typos in review ;)
Jul 15, 2024
f7e918d
Merge branch 'master' into cdoc2
lauris71 Aug 10, 2024
deef68f
Merge branch 'master' into cdoc2
metsma Nov 11, 2024
f0e9c77
Fix whitespace issues and apply static code formating changes
metsma Nov 11, 2024
0840e09
Merge pull request #1 from metsma/cdoc2
lauris71 Nov 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 103 additions & 55 deletions client/CDoc1.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,16 @@ const QHash<QString, QCryptographicHash::Algorithm> CDoc1::SHA_MTH{
};
const QHash<QString, quint32> CDoc1::KWAES_SIZE{{KWAES128_MTH, 16}, {KWAES192_MTH, 24}, {KWAES256_MTH, 32}};

const QByteArray XML_TAG = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";

bool CDoc1::isCDoc1File(const QString& path)
{
QFile f(path);
if(!f.open(QFile::ReadOnly)) return false;
if (f.read(XML_TAG.length()) != XML_TAG) return false;
return true;
lauris71 marked this conversation as resolved.
Show resolved Hide resolved
}

CDoc1::CDoc1(const QString &path)
: QFile(path)
{
Expand Down Expand Up @@ -107,9 +117,10 @@ CDoc1::CDoc1(const QString &path)
if(xml.name() != QLatin1String("EncryptedKey"))
return;

CKey key;
key.id = xml.attributes().value(QLatin1String("Id")).toString();
key.recipient = xml.attributes().value(QLatin1String("Recipient")).toString();
std::shared_ptr<CKeyCDoc1> key = std::make_shared<CKeyCDoc1>();
// Id is never used
//key->id = xml.attributes().value(QLatin1String("Id")).toString();
key->label = xml.attributes().value(QLatin1String("Recipient")).toString();
while(!xml.atEnd())
{
xml.readNext();
Expand All @@ -118,50 +129,57 @@ CDoc1::CDoc1(const QString &path)
if(!xml.isStartElement())
continue;
// EncryptedData/KeyInfo/KeyName
if(xml.name() == QLatin1String("KeyName"))
key.name = xml.readElementText();
// Name is never used
//if(xml.name() == QLatin1String("KeyName"))
// key->name = xml.readElementText();
// EncryptedData/KeyInfo/EncryptedKey/EncryptionMethod
else if(xml.name() == QLatin1String("EncryptionMethod"))
key.method = xml.attributes().value(QLatin1String("Algorithm")).toString();
key->method = xml.attributes().value(QLatin1String("Algorithm")).toString();
// EncryptedData/KeyInfo/EncryptedKey/KeyInfo/AgreementMethod
else if(xml.name() == QLatin1String("AgreementMethod"))
key.agreement = xml.attributes().value(QLatin1String("Algorithm")).toString();
else if(xml.name() == QLatin1String("AgreementMethod")) {
// fixme: handle error
if (xml.attributes().value(QLatin1String("Algorithm")).toString() != AGREEMENT_MTH)
return;
//key->agreement = xml.attributes().value(QLatin1String("Algorithm")).toString();
// EncryptedData/KeyInfo/EncryptedKey/KeyInfo/AgreementMethod/KeyDerivationMethod
else if(xml.name() == QLatin1String("KeyDerivationMethod"))
key.derive = xml.attributes().value(QLatin1String("Algorithm")).toString();
} else if(xml.name() == QLatin1String("KeyDerivationMethod")) {
// fixme: handle error
if (xml.attributes().value(QLatin1String("Algorithm")).toString() != CONCATKDF_MTH)
return;
// key->derive = xml.attributes().value(QLatin1String("Algorithm")).toString();
// EncryptedData/KeyInfo/EncryptedKey/KeyInfo/AgreementMethod/KeyDerivationMethod/ConcatKDFParams
else if(xml.name() == QLatin1String("ConcatKDFParams"))
} else if(xml.name() == QLatin1String("ConcatKDFParams"))
{
key.AlgorithmID = QByteArray::fromHex(xml.attributes().value(QLatin1String("AlgorithmID")).toUtf8());
if(key.AlgorithmID.front() == char(0x00)) key.AlgorithmID.remove(0, 1);
key.PartyUInfo = QByteArray::fromHex(xml.attributes().value(QLatin1String("PartyUInfo")).toUtf8());
if(key.PartyUInfo.front() == char(0x00)) key.PartyUInfo.remove(0, 1);
key.PartyVInfo = QByteArray::fromHex(xml.attributes().value(QLatin1String("PartyVInfo")).toUtf8());
if(key.PartyVInfo.front() == char(0x00)) key.PartyVInfo.remove(0, 1);
key->AlgorithmID = QByteArray::fromHex(xml.attributes().value(QLatin1String("AlgorithmID")).toUtf8());
if(key->AlgorithmID.front() == char(0x00)) key->AlgorithmID.remove(0, 1);
key->PartyUInfo = QByteArray::fromHex(xml.attributes().value(QLatin1String("PartyUInfo")).toUtf8());
if(key->PartyUInfo.front() == char(0x00)) key->PartyUInfo.remove(0, 1);
key->PartyVInfo = QByteArray::fromHex(xml.attributes().value(QLatin1String("PartyVInfo")).toUtf8());
if(key->PartyVInfo.front() == char(0x00)) key->PartyVInfo.remove(0, 1);
}
// EncryptedData/KeyInfo/EncryptedKey/KeyInfo/AgreementMethod/KeyDerivationMethod/ConcatKDFParams/DigestMethod
else if(xml.name() == QLatin1String("DigestMethod"))
key.concatDigest = xml.attributes().value(QLatin1String("Algorithm")).toString();
key->concatDigest = xml.attributes().value(QLatin1String("Algorithm")).toString();
// EncryptedData/KeyInfo/EncryptedKey/KeyInfo/AgreementMethod/OriginatorKeyInfo/KeyValue/ECKeyValue/PublicKey
else if(xml.name() == QLatin1String("PublicKey"))
{
xml.readNext();
key.publicKey = fromBase64(xml.text());
key->publicKey = fromBase64(xml.text());
}
// EncryptedData/KeyInfo/EncryptedKey/KeyInfo/X509Data/X509Certificate
else if(xml.name() == QLatin1String("X509Certificate"))
{
xml.readNext();
key.setCert(QSslCertificate(fromBase64(xml.text()), QSsl::Der));
key->setCert(QSslCertificate(fromBase64(xml.text()), QSsl::Der));
}
// EncryptedData/KeyInfo/EncryptedKey/KeyInfo/CipherData/CipherValue
else if(xml.name() == QLatin1String("CipherValue"))
{
xml.readNext();
key.cipher = fromBase64(xml.text());
key->encrypted_fmk = fromBase64(xml.text());
}
}
keys.append(std::move(key));
keys.append(key);
});
if(!keys.isEmpty())
setLastError({});
Expand All @@ -178,6 +196,15 @@ CDoc1::CDoc1(const QString &path)
}
}

std::unique_ptr<CDoc1>
CDoc1::load(const QString& path)
{
CDoc1 *cdoc = new CDoc1(path);
if (!cdoc->keys.isEmpty()) return std::unique_ptr<CDoc1>(cdoc);
delete cdoc;
return nullptr;
lauris71 marked this conversation as resolved.
Show resolved Hide resolved
}

bool CDoc1::decryptPayload(const QByteArray &key)
{
if(!isOpen())
Expand Down Expand Up @@ -267,22 +294,32 @@ bool CDoc1::decryptPayload(const QByteArray &key)
return !files.empty();
}

CKey CDoc1::canDecrypt(const QSslCertificate &cert) const
CKey::DecryptionStatus
CDoc1::canDecrypt(const QSslCertificate &cert) const
{
std::shared_ptr<CKey> key = getDecryptionKey(cert);
if (key) return CKey::DecryptionStatus::CAN_DECRYPT;
return CKey::DecryptionStatus::CANNOT_DECRYPT;
}

std::shared_ptr<CKey> CDoc1::getDecryptionKey(const QSslCertificate &cert) const
{
for(const CKey &k: qAsConst(keys))
for(std::shared_ptr<CKey> key: qAsConst(keys))
{
if (key->type != CKey::Type::CDOC1) continue;
std::shared_ptr<CKeyCDoc1> k = std::static_pointer_cast<CKeyCDoc1>(key);
if(!ENC_MTH.contains(method) ||
k.cert != cert ||
k.cipher.isEmpty())
k->cert != cert ||
k->encrypted_fmk.isEmpty())
continue;
if(cert.publicKey().algorithm() == QSsl::Rsa &&
k.method == RSA_MTH)
k->method == RSA_MTH)
return k;
if(cert.publicKey().algorithm() == QSsl::Ec &&
!k.publicKey.isEmpty() &&
KWAES_SIZE.contains(k.method) &&
k.derive == CONCATKDF_MTH &&
k.agreement == AGREEMENT_MTH)
!k->publicKey.isEmpty() &&
KWAES_SIZE.contains(k->method) /* &&
k->derive == CONCATKDF_MTH &&
k->agreement == AGREEMENT_MTH*/ )
return k;
}
return {};
Expand Down Expand Up @@ -429,33 +466,38 @@ bool CDoc1::save(const QString &path)
});
w.writeNamespace(DS, QStringLiteral("ds"));
writeElement(w, DS, QStringLiteral("KeyInfo"), [&]{
for(const CKey &k: qAsConst(keys))
for(std::shared_ptr<CKey> key: qAsConst(keys))
{
// Only certificate-based keys can be used in CDoc1
if (key->type != CKey::Type::CERTIFICATE) return;
std::shared_ptr<CKeyCert> ckey = std::static_pointer_cast<CKeyCert>(key);
writeElement(w, DENC, QStringLiteral("EncryptedKey"), [&]{
if(!k.id.isEmpty())
w.writeAttribute(QStringLiteral("Id"), k.id);
if(!k.recipient.isEmpty())
w.writeAttribute(QStringLiteral("Recipient"), k.recipient);
// Id is never used
//if(!k->id.isEmpty())
// w.writeAttribute(QStringLiteral("Id"), k->id);
if(!ckey->label.isEmpty())
w.writeAttribute(QStringLiteral("Recipient"), ckey->label);
QByteArray cipher;
if(k.isRSA)
if(ckey->pk_type == CKey::PKType::RSA)
{
cipher = Crypto::encrypt(X509_get0_pubkey((const X509*)k.cert.handle()), RSA_PKCS1_PADDING, transportKey);
cipher = Crypto::encrypt(X509_get0_pubkey((const X509*)ckey->cert.handle()), RSA_PKCS1_PADDING, transportKey);
if(cipher.isEmpty())
return;
writeElement(w, DENC, QStringLiteral("EncryptionMethod"), {
{QStringLiteral("Algorithm"), RSA_MTH},
});
writeElement(w, DS, QStringLiteral("KeyInfo"), [&]{
if(!k.name.isEmpty())
w.writeTextElement(DS, QStringLiteral("KeyName"), k.name);
// Name is never used
//if(!k->name.isEmpty())
// w.writeTextElement(DS, QStringLiteral("KeyName"), k->name);
writeElement(w, DS, QStringLiteral("X509Data"), [&]{
writeBase64Element(w, DS, QStringLiteral("X509Certificate"), k.cert.toDer());
writeBase64Element(w, DS, QStringLiteral("X509Certificate"), ckey->cert.toDer());
});
});
}
else
{
EVP_PKEY *peerPKey = X509_get0_pubkey((const X509*)k.cert.handle());
EVP_PKEY *peerPKey = X509_get0_pubkey((const X509*)ckey->cert.handle());
auto priv = Crypto::genECKey(peerPKey);
QByteArray sharedSecret = Crypto::derive(priv.get(), peerPKey);
if(sharedSecret.isEmpty())
Expand All @@ -472,14 +514,14 @@ bool CDoc1::save(const QString &path)
default: concatDigest = SHA512_MTH; break;
}
QByteArray encryptionKey = Crypto::concatKDF(SHA_MTH[concatDigest], KWAES_SIZE[encryptionMethod],
sharedSecret, props.value(QStringLiteral("DocumentFormat")).toUtf8() + SsDer + k.cert.toDer());
sharedSecret, props.value(QStringLiteral("DocumentFormat")).toUtf8() + SsDer + ckey->cert.toDer());
#ifndef NDEBUG
qDebug() << "ENC Ss" << SsDer.toHex();
qDebug() << "ENC Ksr" << sharedSecret.toHex();
qDebug() << "ENC ConcatKDF" << encryptionKey.toHex();
#endif

cipher = Crypto::aes_wrap(encryptionKey, transportKey, true);
cipher = Crypto::aes_wrap(encryptionKey, transportKey);
if(cipher.isEmpty())
return;

Expand All @@ -497,7 +539,7 @@ bool CDoc1::save(const QString &path)
writeElement(w, XENC11, QStringLiteral("ConcatKDFParams"), {
{QStringLiteral("AlgorithmID"), QStringLiteral("00") + props.value(QStringLiteral("DocumentFormat")).toUtf8().toHex()},
{QStringLiteral("PartyUInfo"), QStringLiteral("00") + SsDer.toHex()},
{QStringLiteral("PartyVInfo"), QStringLiteral("00") + k.cert.toDer().toHex()},
{QStringLiteral("PartyVInfo"), QStringLiteral("00") + ckey->cert.toDer().toHex()},
}, [&]{
writeElement(w, DS, QStringLiteral("DigestMethod"), {
{QStringLiteral("Algorithm"), concatDigest},
Expand All @@ -517,7 +559,7 @@ bool CDoc1::save(const QString &path)
});
writeElement(w, DENC, QStringLiteral("RecipientKeyInfo"), [&]{
writeElement(w, DS, QStringLiteral("X509Data"), [&]{
writeBase64Element(w, DS, QStringLiteral("X509Certificate"), k.cert.toDer());
writeBase64Element(w, DS, QStringLiteral("X509Certificate"), ckey->cert.toDer());
});
});
});
Expand All @@ -528,6 +570,7 @@ bool CDoc1::save(const QString &path)
});
});
}});
// This is actual content, for some weird reason named cipherData/cipherValue
writeElement(w,DENC, QStringLiteral("CipherData"), [&]{
writeBase64Element(w, DENC, QStringLiteral("CipherValue"),
Crypto::cipher(ENC_MTH[method], transportKey, data.buffer(), true)
Expand All @@ -546,27 +589,32 @@ bool CDoc1::save(const QString &path)
return true;
}

QByteArray CDoc1::transportKey(const CKey &key)
QByteArray CDoc1::getFMK(const CKey &key, const QByteArray& secret)
{
if (key.type != CKey::Type::CDOC1) {
setLastError(QStringLiteral("Not a CDoc1 key"));
return {};
}
const CKeyCDoc1& ckey = static_cast<const CKeyCDoc1&>(key);
setLastError({});
QByteArray decryptedKey = qApp->signer()->decrypt([&key](QCryptoBackend *backend) {
if(key.isRSA)
return backend->decrypt(key.cipher, false);
return backend->deriveConcatKDF(key.publicKey, SHA_MTH[key.concatDigest],
int(KWAES_SIZE[key.method]), key.AlgorithmID, key.PartyUInfo, key.PartyVInfo);
QByteArray decryptedKey = qApp->signer()->decrypt([&ckey](QCryptoBackend *backend) {
if(ckey.pk_type == CKey::PKType::RSA)
return backend->decrypt(ckey.encrypted_fmk, false);
return backend->deriveConcatKDF(ckey.publicKey, SHA_MTH[ckey.concatDigest],
int(KWAES_SIZE[ckey.method]), ckey.AlgorithmID, ckey.PartyUInfo, ckey.PartyVInfo);
});
if(decryptedKey.isEmpty())
{
setLastError(QStringLiteral("Failed to decrypt/derive key"));
return {};
}
if(key.isRSA)
if(ckey.pk_type == CKey::PKType::RSA)
return decryptedKey;
#ifndef NDEBUG
qDebug() << "DEC Ss" << key.publicKey.toHex();
qDebug() << "DEC Ss" << ckey.publicKey.toHex();
qDebug() << "DEC ConcatKDF" << decryptedKey.toHex();
#endif
return Crypto::aes_wrap(decryptedKey, key.cipher, false);
return Crypto::aes_unwrap(decryptedKey, ckey.encrypted_fmk);
}

int CDoc1::version()
Expand Down
12 changes: 9 additions & 3 deletions client/CDoc1.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,20 @@ class CDoc1 final: public CDoc, private QFile
{
public:
CDoc1() = default;
CDoc1(const QString &path);
CKey canDecrypt(const QSslCertificate &cert) const final;

static bool isCDoc1File(const QString& path);

CKey::DecryptionStatus canDecrypt(const QSslCertificate &cert) const final;
std::shared_ptr<CKey> getDecryptionKey(const QSslCertificate &cert) const final;
bool decryptPayload(const QByteArray &key) final;
bool save(const QString &path) final;
QByteArray transportKey(const CKey &key) final;
QByteArray getFMK(const CKey &key, const QByteArray& secret) final;
int version() final;

static std::unique_ptr<CDoc1> load(const QString& path);
private:
CDoc1(const QString &path);

void writeDDoc(QIODevice *ddoc);

static QByteArray fromBase64(QStringView data);
Expand Down
Loading
Loading