mirror of
https://github.com/nextcloud/desktop.git
synced 2025-10-26 11:17:43 +00:00
Instead of immediately popping up the mnemonic dialogue, only show a notification bar on the account setup page. For the cases where the user does not want to use E2E, this is significantly less intrusive than the old approach.
1545 lines
50 KiB
C++
1545 lines
50 KiB
C++
#include <openssl/rsa.h>
|
|
#include <openssl/evp.h>
|
|
#include <openssl/pem.h>
|
|
#include <openssl/err.h>
|
|
#include <openssl/engine.h>
|
|
#include <openssl/rand.h>
|
|
|
|
|
|
#include "clientsideencryption.h"
|
|
#include "account.h"
|
|
#include "capabilities.h"
|
|
#include "networkjobs.h"
|
|
#include "clientsideencryptionjobs.h"
|
|
#include "theme.h"
|
|
#include "creds/abstractcredentials.h"
|
|
|
|
#include <map>
|
|
|
|
#include <cstdio>
|
|
|
|
#include <QDebug>
|
|
#include <QLoggingCategory>
|
|
#include <QFileInfo>
|
|
#include <QDir>
|
|
#include <QJsonObject>
|
|
#include <QXmlStreamReader>
|
|
#include <QXmlStreamNamespaceDeclaration>
|
|
#include <QStack>
|
|
#include <QInputDialog>
|
|
#include <QLineEdit>
|
|
#include <QIODevice>
|
|
#include <QUuid>
|
|
|
|
#include <keychain.h>
|
|
|
|
#include "wordlist.h"
|
|
|
|
QDebug operator<<(QDebug out, const std::string& str)
|
|
{
|
|
out << QString::fromStdString(str);
|
|
return out;
|
|
}
|
|
|
|
using namespace QKeychain;
|
|
|
|
namespace OCC
|
|
{
|
|
|
|
Q_LOGGING_CATEGORY(lcCse, "nextcloud.sync.clientsideencryption", QtInfoMsg)
|
|
Q_LOGGING_CATEGORY(lcCseDecryption, "nextcloud.e2e", QtInfoMsg)
|
|
Q_LOGGING_CATEGORY(lcCseMetadata, "nextcloud.metadata", QtInfoMsg)
|
|
|
|
QString baseUrl(){
|
|
return QStringLiteral("ocs/v2.php/apps/end_to_end_encryption/api/v1/");
|
|
}
|
|
|
|
namespace {
|
|
const char e2e_cert[] = "_e2e-certificate";
|
|
const char e2e_private[] = "_e2e-private";
|
|
const char e2e_mnemonic[] = "_e2e-mnemonic";
|
|
} // ns
|
|
|
|
namespace {
|
|
QByteArray BIO2ByteArray(BIO *b) {
|
|
int pending = BIO_ctrl_pending(b);
|
|
char *tmp = (char *)calloc(pending+1, sizeof(char));
|
|
BIO_read(b, tmp, pending);
|
|
|
|
QByteArray res(tmp, pending);
|
|
free(tmp);
|
|
|
|
return res;
|
|
}
|
|
|
|
QByteArray handleErrors(void)
|
|
{
|
|
auto *bioErrors = BIO_new(BIO_s_mem());
|
|
ERR_print_errors(bioErrors); // This line is not printing anything.
|
|
auto errors = BIO2ByteArray(bioErrors);
|
|
BIO_free_all(bioErrors);
|
|
return errors;
|
|
}
|
|
}
|
|
|
|
namespace EncryptionHelper {
|
|
QByteArray generateRandomFilename()
|
|
{
|
|
return QUuid::createUuid().toRfc4122().toHex();
|
|
}
|
|
|
|
QByteArray generateRandom(int size)
|
|
{
|
|
unsigned char *tmp = (unsigned char *)malloc(sizeof(unsigned char) * size);
|
|
|
|
int ret = RAND_bytes(tmp, size);
|
|
if (ret != 1) {
|
|
qCInfo(lcCse()) << "Random byte generation failed!";
|
|
// Error out?
|
|
}
|
|
|
|
QByteArray result((const char *)tmp, size);
|
|
free(tmp);
|
|
|
|
return result;
|
|
}
|
|
|
|
QByteArray generatePassword(const QString& wordlist, const QByteArray& salt) {
|
|
qCInfo(lcCse()) << "Start encryption key generation!";
|
|
|
|
const int iterationCount = 1024;
|
|
const int keyStrength = 256;
|
|
const int keyLength = keyStrength/8;
|
|
|
|
unsigned char secretKey[keyLength];
|
|
|
|
int ret = PKCS5_PBKDF2_HMAC_SHA1(
|
|
wordlist.toLocal8Bit().constData(), // const char *password,
|
|
wordlist.size(), // int password length,
|
|
(const unsigned char *)salt.constData(), // const unsigned char *salt,
|
|
salt.size(), // int saltlen,
|
|
iterationCount, // int iterations,
|
|
keyLength, // int keylen,
|
|
secretKey // unsigned char *out
|
|
);
|
|
|
|
if (ret != 1) {
|
|
qCInfo(lcCse()) << "Failed to generate encryption key";
|
|
// Error out?
|
|
}
|
|
|
|
qCInfo(lcCse()) << "Encryption key generated!";
|
|
|
|
QByteArray password((const char *)secretKey, keyLength);
|
|
return password;
|
|
}
|
|
|
|
QByteArray encryptPrivateKey(
|
|
const QByteArray& key,
|
|
const QByteArray& privateKey,
|
|
const QByteArray& salt
|
|
) {
|
|
|
|
QByteArray iv = generateRandom(12);
|
|
|
|
EVP_CIPHER_CTX *ctx;
|
|
/* Create and initialise the context */
|
|
if(!(ctx = EVP_CIPHER_CTX_new())) {
|
|
qCInfo(lcCse()) << "Error creating cipher";
|
|
handleErrors();
|
|
}
|
|
|
|
/* Initialise the decryption operation. */
|
|
if(!EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), nullptr, nullptr, nullptr)) {
|
|
qCInfo(lcCse()) << "Error initializing context with aes_256";
|
|
handleErrors();
|
|
}
|
|
|
|
// No padding
|
|
EVP_CIPHER_CTX_set_padding(ctx, 0);
|
|
|
|
/* Set IV length. */
|
|
if(!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, iv.size(), nullptr)) {
|
|
qCInfo(lcCse()) << "Error setting iv length";
|
|
handleErrors();
|
|
}
|
|
|
|
/* Initialise key and IV */
|
|
if(!EVP_EncryptInit_ex(ctx, nullptr, nullptr, (unsigned char *)key.constData(), (unsigned char *)iv.constData())) {
|
|
qCInfo(lcCse()) << "Error initialising key and iv";
|
|
handleErrors();
|
|
}
|
|
|
|
// We write the base64 encoded private key
|
|
QByteArray privateKeyB64 = privateKey.toBase64();
|
|
|
|
// Make sure we have enough room in the cipher text
|
|
unsigned char *ctext = (unsigned char *)malloc(sizeof(unsigned char) * (privateKeyB64.size() + 32));
|
|
|
|
// Do the actual encryption
|
|
int len = 0;
|
|
if(!EVP_EncryptUpdate(ctx, ctext, &len, (unsigned char *)privateKeyB64.constData(), privateKeyB64.size())) {
|
|
qCInfo(lcCse()) << "Error encrypting";
|
|
handleErrors();
|
|
}
|
|
|
|
int clen = len;
|
|
|
|
/* Finalise the encryption. Normally ciphertext bytes may be written at
|
|
* this stage, but this does not occur in GCM mode
|
|
*/
|
|
if(1 != EVP_EncryptFinal_ex(ctx, ctext + len, &len)) {
|
|
qCInfo(lcCse()) << "Error finalizing encryption";
|
|
handleErrors();
|
|
}
|
|
clen += len;
|
|
|
|
/* Get the tag */
|
|
unsigned char *tag = (unsigned char *)calloc(sizeof(unsigned char), 16);
|
|
if(1 != EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, tag)) {
|
|
qCInfo(lcCse()) << "Error getting the tag";
|
|
handleErrors();
|
|
}
|
|
|
|
QByteArray cipherTXT((char *)ctext, clen);
|
|
cipherTXT.append((char *)tag, 16);
|
|
|
|
QByteArray result = cipherTXT.toBase64();
|
|
result += "fA==";
|
|
result += iv.toBase64();
|
|
result += "fA==";
|
|
result += salt.toBase64();
|
|
|
|
return result;
|
|
}
|
|
|
|
QByteArray decryptPrivateKey(const QByteArray& key, const QByteArray& data) {
|
|
qCInfo(lcCse()) << "decryptStringSymmetric key: " << key;
|
|
qCInfo(lcCse()) << "decryptStringSymmetric data: " << data;
|
|
|
|
int sep = data.indexOf("fA==");
|
|
qCInfo(lcCse()) << "sep at" << sep;
|
|
|
|
QByteArray cipherTXT64 = data.left(sep);
|
|
QByteArray ivB64 = data.right(data.size() - sep - 4);
|
|
|
|
qCInfo(lcCse()) << "decryptStringSymmetric cipherTXT: " << cipherTXT64;
|
|
qCInfo(lcCse()) << "decryptStringSymmetric IV: " << ivB64;
|
|
|
|
QByteArray cipherTXT = QByteArray::fromBase64(cipherTXT64);
|
|
QByteArray iv = QByteArray::fromBase64(ivB64);
|
|
|
|
QByteArray tag = cipherTXT.right(16);
|
|
cipherTXT.chop(16);
|
|
|
|
// Init
|
|
EVP_CIPHER_CTX *ctx;
|
|
|
|
/* Create and initialise the context */
|
|
if(!(ctx = EVP_CIPHER_CTX_new())) {
|
|
qCInfo(lcCse()) << "Error creating cipher";
|
|
return QByteArray();
|
|
}
|
|
|
|
/* Initialise the decryption operation. */
|
|
if(!EVP_DecryptInit_ex(ctx, EVP_aes_256_gcm(), nullptr, nullptr, nullptr)) {
|
|
qCInfo(lcCse()) << "Error initialising context with aes 256";
|
|
EVP_CIPHER_CTX_free(ctx);
|
|
return QByteArray();
|
|
}
|
|
|
|
/* Set IV length. Not necessary if this is 12 bytes (96 bits) */
|
|
if(!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, iv.size(), nullptr)) {
|
|
qCInfo(lcCse()) << "Error setting IV size";
|
|
EVP_CIPHER_CTX_free(ctx);
|
|
return QByteArray();
|
|
}
|
|
|
|
/* Initialise key and IV */
|
|
if(!EVP_DecryptInit_ex(ctx, nullptr, nullptr, (unsigned char *)key.constData(), (unsigned char *)iv.constData())) {
|
|
qCInfo(lcCse()) << "Error initialising key and iv";
|
|
EVP_CIPHER_CTX_free(ctx);
|
|
return QByteArray();
|
|
}
|
|
|
|
unsigned char *ptext = (unsigned char *)calloc(cipherTXT.size() + 16, sizeof(unsigned char));
|
|
int plen;
|
|
|
|
/* Provide the message to be decrypted, and obtain the plaintext output.
|
|
* EVP_DecryptUpdate can be called multiple times if necessary
|
|
*/
|
|
if(!EVP_DecryptUpdate(ctx, ptext, &plen, (unsigned char *)cipherTXT.constData(), cipherTXT.size())) {
|
|
qCInfo(lcCse()) << "Could not decrypt";
|
|
EVP_CIPHER_CTX_free(ctx);
|
|
free(ptext);
|
|
return QByteArray();
|
|
}
|
|
|
|
/* Set expected tag value. Works in OpenSSL 1.0.1d and later */
|
|
if(!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, tag.size(), (unsigned char *)tag.constData())) {
|
|
qCInfo(lcCse()) << "Could not set tag";
|
|
EVP_CIPHER_CTX_free(ctx);
|
|
free(ptext);
|
|
return QByteArray();
|
|
}
|
|
|
|
/* Finalise the decryption. A positive return value indicates success,
|
|
* anything else is a failure - the plaintext is not trustworthy.
|
|
*/
|
|
int len = plen;
|
|
if (EVP_DecryptFinal_ex(ctx, ptext + plen, &len) == 0) {
|
|
qCInfo(lcCse()) << "Tag did not match!";
|
|
EVP_CIPHER_CTX_free(ctx);
|
|
free(ptext);
|
|
return QByteArray();
|
|
}
|
|
|
|
QByteArray result((char *)ptext, plen);
|
|
|
|
free(ptext);
|
|
EVP_CIPHER_CTX_free(ctx);
|
|
|
|
return QByteArray::fromBase64(result);
|
|
}
|
|
|
|
QByteArray decryptStringSymmetric(const QByteArray& key, const QByteArray& data) {
|
|
qCInfo(lcCse()) << "decryptStringSymmetric key: " << key;
|
|
qCInfo(lcCse()) << "decryptStringSymmetric data: " << data;
|
|
|
|
int sep = data.indexOf("fA==");
|
|
qCInfo(lcCse()) << "sep at" << sep;
|
|
|
|
QByteArray cipherTXT64 = data.left(sep);
|
|
QByteArray ivB64 = data.right(data.size() - sep - 4);
|
|
|
|
qCInfo(lcCse()) << "decryptStringSymmetric cipherTXT: " << cipherTXT64;
|
|
qCInfo(lcCse()) << "decryptStringSymmetric IV: " << ivB64;
|
|
|
|
QByteArray cipherTXT = QByteArray::fromBase64(cipherTXT64);
|
|
QByteArray iv = QByteArray::fromBase64(ivB64);
|
|
|
|
QByteArray tag = cipherTXT.right(16);
|
|
cipherTXT.chop(16);
|
|
|
|
// Init
|
|
EVP_CIPHER_CTX *ctx;
|
|
|
|
/* Create and initialise the context */
|
|
if(!(ctx = EVP_CIPHER_CTX_new())) {
|
|
qCInfo(lcCse()) << "Error creating cipher";
|
|
return QByteArray();
|
|
}
|
|
|
|
/* Initialise the decryption operation. */
|
|
if(!EVP_DecryptInit_ex(ctx, EVP_aes_128_gcm(), nullptr, nullptr, nullptr)) {
|
|
qCInfo(lcCse()) << "Error initialising context with aes 128";
|
|
EVP_CIPHER_CTX_free(ctx);
|
|
return QByteArray();
|
|
}
|
|
|
|
/* Set IV length. Not necessary if this is 12 bytes (96 bits) */
|
|
if(!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, iv.size(), nullptr)) {
|
|
qCInfo(lcCse()) << "Error setting IV size";
|
|
EVP_CIPHER_CTX_free(ctx);
|
|
return QByteArray();
|
|
}
|
|
|
|
/* Initialise key and IV */
|
|
if(!EVP_DecryptInit_ex(ctx, nullptr, nullptr, (unsigned char *)key.constData(), (unsigned char *)iv.constData())) {
|
|
qCInfo(lcCse()) << "Error initialising key and iv";
|
|
EVP_CIPHER_CTX_free(ctx);
|
|
return QByteArray();
|
|
}
|
|
|
|
unsigned char *ptext = (unsigned char *)calloc(cipherTXT.size() + 16, sizeof(unsigned char));
|
|
int plen;
|
|
|
|
/* Provide the message to be decrypted, and obtain the plaintext output.
|
|
* EVP_DecryptUpdate can be called multiple times if necessary
|
|
*/
|
|
if(!EVP_DecryptUpdate(ctx, ptext, &plen, (unsigned char *)cipherTXT.constData(), cipherTXT.size())) {
|
|
qCInfo(lcCse()) << "Could not decrypt";
|
|
EVP_CIPHER_CTX_free(ctx);
|
|
free(ptext);
|
|
return QByteArray();
|
|
}
|
|
|
|
/* Set expected tag value. Works in OpenSSL 1.0.1d and later */
|
|
if(!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, tag.size(), (unsigned char *)tag.constData())) {
|
|
qCInfo(lcCse()) << "Could not set tag";
|
|
EVP_CIPHER_CTX_free(ctx);
|
|
free(ptext);
|
|
return QByteArray();
|
|
}
|
|
|
|
/* Finalise the decryption. A positive return value indicates success,
|
|
* anything else is a failure - the plaintext is not trustworthy.
|
|
*/
|
|
int len = plen;
|
|
if (EVP_DecryptFinal_ex(ctx, ptext + plen, &len) == 0) {
|
|
qCInfo(lcCse()) << "Tag did not match!";
|
|
EVP_CIPHER_CTX_free(ctx);
|
|
free(ptext);
|
|
return QByteArray();
|
|
}
|
|
|
|
QByteArray result((char *)ptext, plen);
|
|
|
|
free(ptext);
|
|
EVP_CIPHER_CTX_free(ctx);
|
|
|
|
return result;
|
|
}
|
|
|
|
QByteArray privateKeyToPem(const QByteArray key) {
|
|
BIO *privateKeyBio = BIO_new(BIO_s_mem());
|
|
BIO_write(privateKeyBio, key.constData(), key.size());
|
|
EVP_PKEY *pkey = PEM_read_bio_PrivateKey(privateKeyBio, nullptr, nullptr, nullptr);
|
|
|
|
BIO *pemBio = BIO_new(BIO_s_mem());
|
|
PEM_write_bio_PKCS8PrivateKey(pemBio, pkey, nullptr, nullptr, 0, nullptr, nullptr);
|
|
QByteArray pem = BIO2ByteArray(pemBio);
|
|
|
|
BIO_free_all(privateKeyBio);
|
|
BIO_free_all(pemBio);
|
|
EVP_PKEY_free(pkey);
|
|
|
|
return pem;
|
|
}
|
|
|
|
QByteArray encryptStringSymmetric(const QByteArray& key, const QByteArray& data) {
|
|
QByteArray iv = generateRandom(16);
|
|
|
|
EVP_CIPHER_CTX *ctx;
|
|
/* Create and initialise the context */
|
|
if(!(ctx = EVP_CIPHER_CTX_new())) {
|
|
qCInfo(lcCse()) << "Error creating cipher";
|
|
handleErrors();
|
|
return {};
|
|
}
|
|
|
|
/* Initialise the decryption operation. */
|
|
if(!EVP_EncryptInit_ex(ctx, EVP_aes_128_gcm(), nullptr, nullptr, nullptr)) {
|
|
qCInfo(lcCse()) << "Error initializing context with aes_128";
|
|
handleErrors();
|
|
return {};
|
|
}
|
|
|
|
// No padding
|
|
EVP_CIPHER_CTX_set_padding(ctx, 0);
|
|
|
|
/* Set IV length. */
|
|
if(!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, iv.size(), nullptr)) {
|
|
qCInfo(lcCse()) << "Error setting iv length";
|
|
handleErrors();
|
|
return {};
|
|
}
|
|
|
|
/* Initialise key and IV */
|
|
if(!EVP_EncryptInit_ex(ctx, nullptr, nullptr, (unsigned char *)key.constData(), (unsigned char *)iv.constData())) {
|
|
qCInfo(lcCse()) << "Error initialising key and iv";
|
|
handleErrors();
|
|
return {};
|
|
}
|
|
|
|
// We write the data base64 encoded
|
|
QByteArray dataB64 = data.toBase64();
|
|
|
|
// Make sure we have enough room in the cipher text
|
|
unsigned char *ctext = (unsigned char *)malloc(sizeof(unsigned char) * (dataB64.size() + 16));
|
|
|
|
// Do the actual encryption
|
|
int len = 0;
|
|
if(!EVP_EncryptUpdate(ctx, ctext, &len, (unsigned char *)dataB64.constData(), dataB64.size())) {
|
|
qCInfo(lcCse()) << "Error encrypting";
|
|
handleErrors();
|
|
return {};
|
|
}
|
|
|
|
int clen = len;
|
|
|
|
/* Finalise the encryption. Normally ciphertext bytes may be written at
|
|
* this stage, but this does not occur in GCM mode
|
|
*/
|
|
if(1 != EVP_EncryptFinal_ex(ctx, ctext + len, &len)) {
|
|
qCInfo(lcCse()) << "Error finalizing encryption";
|
|
handleErrors();
|
|
return {};
|
|
}
|
|
clen += len;
|
|
|
|
/* Get the tag */
|
|
unsigned char *tag = (unsigned char *)calloc(sizeof(unsigned char), 16);
|
|
if(1 != EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, tag)) {
|
|
qCInfo(lcCse()) << "Error getting the tag";
|
|
handleErrors();
|
|
return {};
|
|
}
|
|
|
|
QByteArray cipherTXT((char *)ctext, clen);
|
|
cipherTXT.append((char *)tag, 16);
|
|
|
|
QByteArray result = cipherTXT.toBase64();
|
|
result += "fA==";
|
|
result += iv.toBase64();
|
|
|
|
return result;
|
|
}
|
|
|
|
QByteArray decryptStringAsymmetric(EVP_PKEY *privateKey, const QByteArray& data) {
|
|
int err = -1;
|
|
|
|
qCInfo(lcCseDecryption()) << "Start to work the decryption.";
|
|
auto ctx = EVP_PKEY_CTX_new(privateKey, ENGINE_get_default_RSA());
|
|
if (!ctx) {
|
|
qCInfo(lcCseDecryption()) << "Could not create the PKEY context.";
|
|
handleErrors();
|
|
return {};
|
|
}
|
|
|
|
err = EVP_PKEY_decrypt_init(ctx);
|
|
if (err <= 0) {
|
|
qCInfo(lcCseDecryption()) << "Could not init the decryption of the metadata";
|
|
handleErrors();
|
|
return {};
|
|
}
|
|
|
|
if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING) <= 0) {
|
|
qCInfo(lcCseDecryption()) << "Error setting the encryption padding.";
|
|
handleErrors();
|
|
return {};
|
|
}
|
|
|
|
if (EVP_PKEY_CTX_set_rsa_oaep_md(ctx, EVP_sha256()) <= 0) {
|
|
qCInfo(lcCseDecryption()) << "Error setting OAEP SHA 256";
|
|
handleErrors();
|
|
return {};
|
|
}
|
|
|
|
if (EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, EVP_sha256()) <= 0) {
|
|
qCInfo(lcCseDecryption()) << "Error setting MGF1 padding";
|
|
handleErrors();
|
|
return {};
|
|
}
|
|
|
|
size_t outlen = 0;
|
|
err = EVP_PKEY_decrypt(ctx, nullptr, &outlen, (unsigned char *)data.constData(), data.size());
|
|
if (err <= 0) {
|
|
qCInfo(lcCseDecryption()) << "Could not determine the buffer length";
|
|
handleErrors();
|
|
return {};
|
|
} else {
|
|
qCInfo(lcCseDecryption()) << "Size of output is: " << outlen;
|
|
qCInfo(lcCseDecryption()) << "Size of data is: " << data.size();
|
|
}
|
|
|
|
unsigned char *out = (unsigned char *) OPENSSL_malloc(outlen);
|
|
if (!out) {
|
|
qCInfo(lcCseDecryption()) << "Could not alloc space for the decrypted metadata";
|
|
handleErrors();
|
|
return {};
|
|
}
|
|
|
|
if (EVP_PKEY_decrypt(ctx, out, &outlen, (unsigned char *)data.constData(), data.size()) <= 0) {
|
|
qCInfo(lcCseDecryption()) << "Could not decrypt the data.";
|
|
ERR_print_errors_fp(stdout); // This line is not printing anything.
|
|
return {};
|
|
} else {
|
|
qCInfo(lcCseDecryption()) << "data decrypted successfully";
|
|
}
|
|
|
|
const auto ret = std::string((char*) out, outlen);
|
|
QByteArray raw((const char*) out, outlen);
|
|
qCInfo(lcCse()) << raw;
|
|
return raw;
|
|
}
|
|
|
|
QByteArray encryptStringAsymmetric(EVP_PKEY *publicKey, const QByteArray& data) {
|
|
int err = -1;
|
|
|
|
auto ctx = EVP_PKEY_CTX_new(publicKey, ENGINE_get_default_RSA());
|
|
if (!ctx) {
|
|
qCInfo(lcCse()) << "Could not initialize the pkey context.";
|
|
exit(1);
|
|
}
|
|
|
|
if (EVP_PKEY_encrypt_init(ctx) != 1) {
|
|
qCInfo(lcCse()) << "Error initilaizing the encryption.";
|
|
exit(1);
|
|
}
|
|
|
|
if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING) <= 0) {
|
|
qCInfo(lcCse()) << "Error setting the encryption padding.";
|
|
exit(1);
|
|
}
|
|
|
|
if (EVP_PKEY_CTX_set_rsa_oaep_md(ctx, EVP_sha256()) <= 0) {
|
|
qCInfo(lcCse()) << "Error setting OAEP SHA 256";
|
|
exit(1);
|
|
}
|
|
|
|
if (EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, EVP_sha256()) <= 0) {
|
|
qCInfo(lcCse()) << "Error setting MGF1 padding";
|
|
exit(1);
|
|
}
|
|
|
|
size_t outLen = 0;
|
|
if (EVP_PKEY_encrypt(ctx, nullptr, &outLen, (unsigned char *)data.constData(), data.size()) != 1) {
|
|
qCInfo(lcCse()) << "Error retrieving the size of the encrypted data";
|
|
exit(1);
|
|
} else {
|
|
qCInfo(lcCse()) << "Encrption Length:" << outLen;
|
|
}
|
|
|
|
unsigned char *out = (uchar*) OPENSSL_malloc(outLen);
|
|
if (!out) {
|
|
qCInfo(lcCse()) << "Error requesting memory for the encrypted contents";
|
|
exit(1);
|
|
}
|
|
|
|
if (EVP_PKEY_encrypt(ctx, out, &outLen, (unsigned char *)data.constData(), data.size()) != 1) {
|
|
qCInfo(lcCse()) << "Could not encrypt key." << err;
|
|
exit(1);
|
|
}
|
|
|
|
// Transform the encrypted data into base64.
|
|
QByteArray raw((const char*) out, outLen);
|
|
qCInfo(lcCse()) << raw.toBase64();
|
|
return raw.toBase64();
|
|
}
|
|
|
|
}
|
|
ClientSideEncryption::ClientSideEncryption()
|
|
{
|
|
}
|
|
|
|
void ClientSideEncryption::setAccount(AccountPtr account)
|
|
{
|
|
_account = account;
|
|
}
|
|
|
|
void ClientSideEncryption::initialize()
|
|
{
|
|
qCInfo(lcCse()) << "Initializing";
|
|
if (!_account->capabilities().clientSideEncryptionAvaliable()) {
|
|
qCInfo(lcCse()) << "No Client side encryption avaliable on server.";
|
|
emit initializationFinished();
|
|
return;
|
|
}
|
|
|
|
fetchFromKeyChain();
|
|
}
|
|
|
|
void ClientSideEncryption::fetchFromKeyChain() {
|
|
const QString kck = AbstractCredentials::keychainKey(
|
|
_account->url().toString(),
|
|
_account->credentials()->user() + e2e_cert,
|
|
_account->id()
|
|
);
|
|
|
|
ReadPasswordJob *job = new ReadPasswordJob(Theme::instance()->appName());
|
|
job->setInsecureFallback(false);
|
|
job->setKey(kck);
|
|
connect(job, &ReadPasswordJob::finished, this, &ClientSideEncryption::publicKeyFetched);
|
|
job->start();
|
|
}
|
|
|
|
void ClientSideEncryption::publicKeyFetched(Job *incoming) {
|
|
ReadPasswordJob *readJob = static_cast<ReadPasswordJob *>(incoming);
|
|
|
|
// Error or no valid public key error out
|
|
if (readJob->error() != NoError || readJob->binaryData().length() == 0) {
|
|
getPublicKeyFromServer();
|
|
return;
|
|
}
|
|
|
|
_certificate = QSslCertificate(readJob->binaryData(), QSsl::Pem);
|
|
|
|
if (_certificate.isNull()) {
|
|
getPublicKeyFromServer();
|
|
return;
|
|
}
|
|
|
|
_publicKey = _certificate.publicKey();
|
|
|
|
qCInfo(lcCse()) << "Public key fetched from keychain";
|
|
|
|
const QString kck = AbstractCredentials::keychainKey(
|
|
_account->url().toString(),
|
|
_account->credentials()->user() + e2e_private,
|
|
_account->id()
|
|
);
|
|
|
|
ReadPasswordJob *job = new ReadPasswordJob(Theme::instance()->appName());
|
|
job->setInsecureFallback(false);
|
|
job->setKey(kck);
|
|
connect(job, &ReadPasswordJob::finished, this, &ClientSideEncryption::privateKeyFetched);
|
|
job->start();
|
|
}
|
|
|
|
void ClientSideEncryption::setFolderEncryptedStatus(const QString& folder, bool status)
|
|
{
|
|
qCDebug(lcCse) << "Setting folder" << folder << "as encrypted" << status;
|
|
_folder2encryptedStatus[folder] = status;
|
|
}
|
|
|
|
void ClientSideEncryption::privateKeyFetched(Job *incoming) {
|
|
ReadPasswordJob *readJob = static_cast<ReadPasswordJob *>(incoming);
|
|
|
|
// Error or no valid public key error out
|
|
if (readJob->error() != NoError || readJob->binaryData().length() == 0) {
|
|
_certificate = QSslCertificate();
|
|
_publicKey = QSslKey();
|
|
getPublicKeyFromServer();
|
|
return;
|
|
}
|
|
|
|
//_privateKey = QSslKey(readJob->binaryData(), QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey);
|
|
_privateKey = readJob->binaryData();
|
|
|
|
if (_privateKey.isNull()) {
|
|
getPrivateKeyFromServer();
|
|
return;
|
|
}
|
|
|
|
qCInfo(lcCse()) << "Private key fetched from keychain";
|
|
|
|
const QString kck = AbstractCredentials::keychainKey(
|
|
_account->url().toString(),
|
|
_account->credentials()->user() + e2e_mnemonic,
|
|
_account->id()
|
|
);
|
|
|
|
ReadPasswordJob *job = new ReadPasswordJob(Theme::instance()->appName());
|
|
job->setInsecureFallback(false);
|
|
job->setKey(kck);
|
|
connect(job, &ReadPasswordJob::finished, this, &ClientSideEncryption::mnemonicKeyFetched);
|
|
job->start();
|
|
}
|
|
|
|
void ClientSideEncryption::mnemonicKeyFetched(QKeychain::Job *incoming) {
|
|
ReadPasswordJob *readJob = static_cast<ReadPasswordJob *>(incoming);
|
|
|
|
// Error or no valid public key error out
|
|
if (readJob->error() != NoError || readJob->textData().length() == 0) {
|
|
_certificate = QSslCertificate();
|
|
_publicKey = QSslKey();
|
|
_privateKey = QByteArray();
|
|
getPublicKeyFromServer();
|
|
return;
|
|
}
|
|
|
|
_mnemonic = readJob->textData();
|
|
|
|
qCInfo(lcCse()) << "Mnemonic key fetched from keychain: " << _mnemonic;
|
|
|
|
emit initializationFinished();
|
|
}
|
|
|
|
void ClientSideEncryption::writePrivateKey() {
|
|
const QString kck = AbstractCredentials::keychainKey(
|
|
_account->url().toString(),
|
|
_account->credentials()->user() + e2e_private,
|
|
_account->id()
|
|
);
|
|
|
|
WritePasswordJob *job = new WritePasswordJob(Theme::instance()->appName());
|
|
job->setInsecureFallback(false);
|
|
job->setKey(kck);
|
|
job->setBinaryData(_privateKey);
|
|
connect(job, &WritePasswordJob::finished, [this](Job *incoming) {
|
|
Q_UNUSED(incoming);
|
|
qCInfo(lcCse()) << "Private key stored in keychain";
|
|
});
|
|
job->start();
|
|
}
|
|
|
|
void ClientSideEncryption::writeCertificate() {
|
|
const QString kck = AbstractCredentials::keychainKey(
|
|
_account->url().toString(),
|
|
_account->credentials()->user() + e2e_cert,
|
|
_account->id()
|
|
);
|
|
|
|
WritePasswordJob *job = new WritePasswordJob(Theme::instance()->appName());
|
|
job->setInsecureFallback(false);
|
|
job->setKey(kck);
|
|
job->setBinaryData(_certificate.toPem());
|
|
connect(job, &WritePasswordJob::finished, [this](Job *incoming) {
|
|
Q_UNUSED(incoming);
|
|
qCInfo(lcCse()) << "Certificate stored in keychain";
|
|
});
|
|
job->start();
|
|
}
|
|
|
|
void ClientSideEncryption::writeMnemonic() {
|
|
const QString kck = AbstractCredentials::keychainKey(
|
|
_account->url().toString(),
|
|
_account->credentials()->user() + e2e_mnemonic,
|
|
_account->id()
|
|
);
|
|
|
|
WritePasswordJob *job = new WritePasswordJob(Theme::instance()->appName());
|
|
job->setInsecureFallback(false);
|
|
job->setKey(kck);
|
|
job->setTextData(_mnemonic);
|
|
connect(job, &WritePasswordJob::finished, [this](Job *incoming) {
|
|
Q_UNUSED(incoming);
|
|
qCInfo(lcCse()) << "Mnemonic stored in keychain";
|
|
});
|
|
job->start();
|
|
}
|
|
|
|
void ClientSideEncryption::forgetSensitiveData()
|
|
{
|
|
_privateKey = QByteArray();
|
|
_certificate = QSslCertificate();
|
|
_publicKey = QSslKey();
|
|
_mnemonic = QString();
|
|
|
|
auto startDeleteJob = [this](QString user) {
|
|
DeletePasswordJob *job = new DeletePasswordJob(Theme::instance()->appName());
|
|
job->setInsecureFallback(false);
|
|
job->setKey(AbstractCredentials::keychainKey(_account->url().toString(), user, _account->id()));
|
|
job->start();
|
|
};
|
|
|
|
auto user = _account->credentials()->user();
|
|
startDeleteJob(user + e2e_private);
|
|
startDeleteJob(user + e2e_cert);
|
|
startDeleteJob(user + e2e_mnemonic);
|
|
}
|
|
|
|
void ClientSideEncryption::slotRequestMnemonic() {
|
|
emit showMnemonic(_mnemonic);
|
|
}
|
|
|
|
bool ClientSideEncryption::hasPrivateKey() const
|
|
{
|
|
return !_privateKey.isNull();
|
|
}
|
|
|
|
bool ClientSideEncryption::hasPublicKey() const
|
|
{
|
|
return !_publicKey.isNull();
|
|
}
|
|
|
|
void ClientSideEncryption::generateKeyPair()
|
|
{
|
|
// AES/GCM/NoPadding,
|
|
// metadataKeys with RSA/ECB/OAEPWithSHA-256AndMGF1Padding
|
|
qCInfo(lcCse()) << "No public key, generating a pair.";
|
|
const int rsaKeyLen = 2048;
|
|
|
|
EVP_PKEY *localKeyPair = nullptr;
|
|
|
|
// Init RSA
|
|
EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr);
|
|
|
|
if(EVP_PKEY_keygen_init(ctx) <= 0) {
|
|
qCInfo(lcCse()) << "Couldn't initialize the key generator";
|
|
return;
|
|
}
|
|
|
|
if(EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, rsaKeyLen) <= 0) {
|
|
qCInfo(lcCse()) << "Couldn't initialize the key generator bits";
|
|
return;
|
|
}
|
|
|
|
if(EVP_PKEY_keygen(ctx, &localKeyPair) <= 0) {
|
|
qCInfo(lcCse()) << "Could not generate the key";
|
|
return;
|
|
}
|
|
EVP_PKEY_CTX_free(ctx);
|
|
qCInfo(lcCse()) << "Key correctly generated";
|
|
qCInfo(lcCse()) << "Storing keys locally";
|
|
|
|
BIO *privKey = BIO_new(BIO_s_mem());
|
|
if (PEM_write_bio_PrivateKey(privKey, localKeyPair, nullptr, nullptr, 0, nullptr, nullptr) <= 0) {
|
|
qCInfo(lcCse()) << "Could not read private key from bio.";
|
|
return;
|
|
}
|
|
QByteArray key = BIO2ByteArray(privKey);
|
|
//_privateKey = QSslKey(key, QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey);
|
|
_privateKey = key;
|
|
|
|
qCInfo(lcCse()) << "Keys generated correctly, sending to server.";
|
|
generateCSR(localKeyPair);
|
|
}
|
|
|
|
void ClientSideEncryption::generateCSR(EVP_PKEY *keyPair)
|
|
{
|
|
// OpenSSL expects const char.
|
|
auto cnArray = _account->davUser().toLocal8Bit();
|
|
qCInfo(lcCse()) << "Getting the following array for the account Id" << cnArray;
|
|
|
|
auto certParams = std::map<const char *, const char*>{
|
|
{"C", "DE"},
|
|
{"ST", "Baden-Wuerttemberg"},
|
|
{"L", "Stuttgart"},
|
|
{"O","Nextcloud"},
|
|
{"CN", cnArray.constData()}
|
|
};
|
|
|
|
int ret = 0;
|
|
int nVersion = 1;
|
|
|
|
X509_REQ *x509_req = nullptr;
|
|
SignPublicKeyApiJob *job = nullptr;
|
|
|
|
// 2. set version of x509 req
|
|
x509_req = X509_REQ_new();
|
|
ret = X509_REQ_set_version(x509_req, nVersion);
|
|
|
|
// 3. set subject of x509 req
|
|
auto x509_name = X509_REQ_get_subject_name(x509_req);
|
|
|
|
using ucharp = const unsigned char *;
|
|
for(const auto& v : certParams) {
|
|
ret = X509_NAME_add_entry_by_txt(x509_name, v.first, MBSTRING_ASC, (ucharp) v.second, -1, -1, 0);
|
|
if (ret != 1) {
|
|
qCInfo(lcCse()) << "Error Generating the Certificate while adding" << v.first << v.second;
|
|
X509_REQ_free(x509_req);
|
|
return;
|
|
}
|
|
}
|
|
|
|
ret = X509_REQ_set_pubkey(x509_req, keyPair);
|
|
if (ret != 1){
|
|
qCInfo(lcCse()) << "Error setting the public key on the csr";
|
|
X509_REQ_free(x509_req);
|
|
return;
|
|
}
|
|
|
|
ret = X509_REQ_sign(x509_req, keyPair, EVP_sha1()); // return x509_req->signature->length
|
|
if (ret <= 0){
|
|
qCInfo(lcCse()) << "Error setting the public key on the csr";
|
|
X509_REQ_free(x509_req);
|
|
return;
|
|
}
|
|
|
|
BIO *out = BIO_new(BIO_s_mem());
|
|
ret = PEM_write_bio_X509_REQ(out, x509_req);
|
|
QByteArray output = BIO2ByteArray(out);
|
|
BIO_free(out);
|
|
EVP_PKEY_free(keyPair);
|
|
|
|
qCInfo(lcCse()) << "Returning the certificate";
|
|
qCInfo(lcCse()) << output;
|
|
|
|
job = new SignPublicKeyApiJob(_account, baseUrl() + "public-key", this);
|
|
job->setCsr(output);
|
|
|
|
connect(job, &SignPublicKeyApiJob::jsonReceived, [this](const QJsonDocument& json, int retCode) {
|
|
if (retCode == 200) {
|
|
QString cert = json.object().value("ocs").toObject().value("data").toObject().value("public-key").toString();
|
|
_certificate = QSslCertificate(cert.toLocal8Bit(), QSsl::Pem);
|
|
_publicKey = _certificate.publicKey();
|
|
qCInfo(lcCse()) << "Certificate saved, Encrypting Private Key.";
|
|
encryptPrivateKey();
|
|
}
|
|
qCInfo(lcCse()) << retCode;
|
|
});
|
|
job->start();
|
|
}
|
|
|
|
void ClientSideEncryption::setTokenForFolder(const QByteArray& folderId, const QByteArray& token)
|
|
{
|
|
_folder2token[folderId] = token;
|
|
}
|
|
|
|
QByteArray ClientSideEncryption::tokenForFolder(const QByteArray& folderId) const
|
|
{
|
|
Q_ASSERT(_folder2token.contains(folderId));
|
|
return _folder2token[folderId];
|
|
}
|
|
|
|
void ClientSideEncryption::encryptPrivateKey()
|
|
{
|
|
QStringList list = WordList::getRandomWords(12);
|
|
_mnemonic = list.join(' ');
|
|
_newMnemonicGenerated = true;
|
|
qCInfo(lcCse()) << "mnemonic Generated:" << _mnemonic;
|
|
|
|
emit mnemonicGenerated(_mnemonic);
|
|
|
|
QString passPhrase = list.join(QString()).toLower();
|
|
qCInfo(lcCse()) << "Passphrase Generated:" << passPhrase;
|
|
|
|
auto salt = EncryptionHelper::generateRandom(40);
|
|
auto secretKey = EncryptionHelper::generatePassword(passPhrase, salt);
|
|
auto cryptedText = EncryptionHelper::encryptPrivateKey(secretKey, EncryptionHelper::privateKeyToPem(_privateKey), salt);
|
|
|
|
// Send private key to the server
|
|
auto job = new StorePrivateKeyApiJob(_account, baseUrl() + "private-key", this);
|
|
job->setPrivateKey(cryptedText);
|
|
connect(job, &StorePrivateKeyApiJob::jsonReceived, [this](const QJsonDocument& doc, int retCode) {
|
|
Q_UNUSED(doc);
|
|
switch(retCode) {
|
|
case 200:
|
|
qCInfo(lcCse()) << "Private key stored encrypted on server.";
|
|
writePrivateKey();
|
|
writeCertificate();
|
|
writeMnemonic();
|
|
emit initializationFinished();
|
|
break;
|
|
default:
|
|
qCInfo(lcCse()) << "Store private key failed, return code:" << retCode;
|
|
}
|
|
});
|
|
job->start();
|
|
}
|
|
|
|
bool ClientSideEncryption::newMnemonicGenerated() const
|
|
{
|
|
return _newMnemonicGenerated;
|
|
}
|
|
|
|
void ClientSideEncryption::decryptPrivateKey(const QByteArray &key) {
|
|
QString msg = tr("Please enter your end to end encryption passphrase:<br>"
|
|
"<br>"
|
|
"User: %2<br>"
|
|
"Account: %3<br>")
|
|
.arg(Utility::escape(_account->credentials()->user()),
|
|
Utility::escape(_account->displayName()));
|
|
|
|
QInputDialog dialog;
|
|
dialog.setWindowTitle(tr("Enter E2E passphrase"));
|
|
dialog.setLabelText(msg);
|
|
dialog.setTextEchoMode(QLineEdit::Normal);
|
|
|
|
QString prev;
|
|
|
|
while(true) {
|
|
if (!prev.isEmpty()) {
|
|
dialog.setTextValue(prev);
|
|
}
|
|
bool ok = dialog.exec();
|
|
if (ok) {
|
|
qCInfo(lcCse()) << "Got mnemonic:" << dialog.textValue();
|
|
prev = dialog.textValue();
|
|
|
|
_mnemonic = prev;
|
|
QString mnemonic = prev.split(" ").join(QString()).toLower();
|
|
qCInfo(lcCse()) << "mnemonic:" << mnemonic;
|
|
|
|
// split off salt
|
|
// Todo better place?
|
|
auto pos = key.lastIndexOf("fA==");
|
|
QByteArray salt = QByteArray::fromBase64(key.mid(pos + 4));
|
|
auto key2 = key.left(pos);
|
|
|
|
auto pass = EncryptionHelper::generatePassword(mnemonic, salt);
|
|
qCInfo(lcCse()) << "Generated key:" << pass;
|
|
|
|
QByteArray privateKey = EncryptionHelper::decryptPrivateKey(pass, key2);
|
|
//_privateKey = QSslKey(privateKey, QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey);
|
|
_privateKey = privateKey;
|
|
|
|
qCInfo(lcCse()) << "Private key: " << _privateKey;
|
|
|
|
if (!_privateKey.isNull()) {
|
|
writePrivateKey();
|
|
writeCertificate();
|
|
writeMnemonic();
|
|
break;
|
|
}
|
|
} else {
|
|
_mnemonic = QString();
|
|
_privateKey = QByteArray();
|
|
qCInfo(lcCse()) << "Cancelled";
|
|
break;
|
|
}
|
|
}
|
|
|
|
emit initializationFinished();
|
|
}
|
|
|
|
void ClientSideEncryption::getPrivateKeyFromServer()
|
|
{
|
|
qCInfo(lcCse()) << "Retrieving private key from server";
|
|
auto job = new JsonApiJob(_account, baseUrl() + "private-key", this);
|
|
connect(job, &JsonApiJob::jsonReceived, [this](const QJsonDocument& doc, int retCode) {
|
|
if (retCode == 200) {
|
|
QString key = doc.object()["ocs"].toObject()["data"].toObject()["private-key"].toString();
|
|
qCInfo(lcCse()) << key;
|
|
qCInfo(lcCse()) << "Found private key, lets decrypt it!";
|
|
decryptPrivateKey(key.toLocal8Bit());
|
|
} else if (retCode == 404) {
|
|
qCInfo(lcCse()) << "No private key on the server: setup is incomplete.";
|
|
} else {
|
|
qCInfo(lcCse()) << "Error while requesting public key: " << retCode;
|
|
}
|
|
});
|
|
job->start();
|
|
}
|
|
|
|
void ClientSideEncryption::getPublicKeyFromServer()
|
|
{
|
|
qCInfo(lcCse()) << "Retrieving public key from server";
|
|
auto job = new JsonApiJob(_account, baseUrl() + "public-key", this);
|
|
connect(job, &JsonApiJob::jsonReceived, [this](const QJsonDocument& doc, int retCode) {
|
|
if (retCode == 200) {
|
|
QString publicKey = doc.object()["ocs"].toObject()["data"].toObject()["public-keys"].toObject()[_account->davUser()].toString();
|
|
_certificate = QSslCertificate(publicKey.toLocal8Bit(), QSsl::Pem);
|
|
_publicKey = _certificate.publicKey();
|
|
qCInfo(lcCse()) << publicKey;
|
|
qCInfo(lcCse()) << "Found Public key, requesting Private Key.";
|
|
getPrivateKeyFromServer();
|
|
} else if (retCode == 404) {
|
|
qCInfo(lcCse()) << "No public key on the server";
|
|
generateKeyPair();
|
|
} else {
|
|
qCInfo(lcCse()) << "Error while requesting public key: " << retCode;
|
|
}
|
|
});
|
|
job->start();
|
|
}
|
|
|
|
void ClientSideEncryption::fetchFolderEncryptedStatus() {
|
|
_refreshingEncryptionStatus = true;
|
|
auto getEncryptedStatus = new GetFolderEncryptStatusJob(_account, QString());
|
|
connect(getEncryptedStatus, &GetFolderEncryptStatusJob::encryptStatusReceived,
|
|
this, &ClientSideEncryption::folderEncryptedStatusFetched);
|
|
connect(getEncryptedStatus, &GetFolderEncryptStatusJob::encryptStatusError,
|
|
this, &ClientSideEncryption::folderEncryptedStatusError);
|
|
getEncryptedStatus->start();
|
|
}
|
|
|
|
void ClientSideEncryption::folderEncryptedStatusFetched(const QMap<QString, bool>& result)
|
|
{
|
|
_refreshingEncryptionStatus = false;
|
|
_folder2encryptedStatus = result;
|
|
qCDebug(lcCse) << "Retrieved correctly the encrypted status of the folders." << result;
|
|
}
|
|
|
|
void ClientSideEncryption::folderEncryptedStatusError(int error)
|
|
{
|
|
_refreshingEncryptionStatus = false;
|
|
qCDebug(lcCse) << "Failed to retrieve the status of the folders." << error;
|
|
}
|
|
|
|
FolderMetadata::FolderMetadata(AccountPtr account, const QByteArray& metadata, int statusCode) : _account(account)
|
|
{
|
|
if (metadata.isEmpty() || statusCode == 404) {
|
|
qCInfo(lcCseMetadata()) << "Setupping Empty Metadata";
|
|
setupEmptyMetadata();
|
|
} else {
|
|
qCInfo(lcCseMetadata()) << "Setting up existing metadata";
|
|
setupExistingMetadata(metadata);
|
|
}
|
|
}
|
|
|
|
void FolderMetadata::setupExistingMetadata(const QByteArray& metadata)
|
|
{
|
|
/* This is the json response from the server, it contains two extra objects that we are *not* interested.
|
|
* ocs and data.
|
|
*/
|
|
QJsonDocument doc = QJsonDocument::fromJson(metadata);
|
|
qCInfo(lcCseMetadata()) << doc.toJson(QJsonDocument::Compact);
|
|
|
|
// The metadata is being retrieved as a string stored in a json.
|
|
// This *seems* to be broken but the RFC doesn't explicits how it wants.
|
|
// I'm currently unsure if this is error on my side or in the server implementation.
|
|
// And because inside of the meta-data there's an object called metadata, without '-'
|
|
// make it really different.
|
|
|
|
QString metaDataStr = doc.object()["ocs"]
|
|
.toObject()["data"]
|
|
.toObject()["meta-data"]
|
|
.toString();
|
|
|
|
QJsonDocument metaDataDoc = QJsonDocument::fromJson(metaDataStr.toLocal8Bit());
|
|
QJsonObject metadataObj = metaDataDoc.object()["metadata"].toObject();
|
|
QJsonObject metadataKeys = metadataObj["metadataKeys"].toObject();
|
|
QByteArray sharing = metadataObj["sharing"].toString().toLocal8Bit();
|
|
QJsonObject files = metaDataDoc.object()["files"].toObject();
|
|
|
|
QJsonDocument debugHelper;
|
|
debugHelper.setObject(metadataKeys);
|
|
qCDebug(lcCse) << "Keys: " << debugHelper.toJson(QJsonDocument::Compact);
|
|
|
|
// Iterate over the document to store the keys. I'm unsure that the keys are in order,
|
|
// perhaps it's better to store a map instead of a vector, perhaps this just doesn't matter.
|
|
for(auto it = metadataKeys.constBegin(), end = metadataKeys.constEnd(); it != end; it++) {
|
|
QByteArray currB64Pass = it.value().toString().toLocal8Bit();
|
|
/*
|
|
* We have to base64 decode the metadatakey here. This was a misunderstanding in the RFC
|
|
* Now we should be compatible with Android and IOS. Maybe we can fix it later.
|
|
*/
|
|
QByteArray b64DecryptedKey = decryptMetadataKey(currB64Pass);
|
|
if (b64DecryptedKey.isEmpty()) {
|
|
qCDebug(lcCse()) << "Could not decrypt metadata for key" << it.key();
|
|
continue;
|
|
}
|
|
|
|
QByteArray decryptedKey = QByteArray::fromBase64(b64DecryptedKey);
|
|
_metadataKeys.insert(it.key().toInt(), decryptedKey);
|
|
}
|
|
|
|
// Cool, We actually have the key, we can decrypt the rest of the metadata.
|
|
qCDebug(lcCse) << "Sharing: " << sharing;
|
|
if (sharing.size()) {
|
|
auto sharingDecrypted = QByteArray::fromBase64(decryptJsonObject(sharing, _metadataKeys.last()));
|
|
qCDebug(lcCse) << "Sharing Decrypted" << sharingDecrypted;
|
|
|
|
//Sharing is also a JSON object, so extract it and populate.
|
|
auto sharingDoc = QJsonDocument::fromJson(sharingDecrypted);
|
|
auto sharingObj = sharingDoc.object();
|
|
for (auto it = sharingObj.constBegin(), end = sharingObj.constEnd(); it != end; it++) {
|
|
_sharing.push_back({it.key(), it.value().toString()});
|
|
}
|
|
} else {
|
|
qCDebug(lcCse) << "Skipping sharing section since it is empty";
|
|
}
|
|
|
|
for (auto it = files.constBegin(), end = files.constEnd(); it != end; it++) {
|
|
EncryptedFile file;
|
|
file.encryptedFilename = it.key();
|
|
|
|
auto fileObj = it.value().toObject();
|
|
file.metadataKey = fileObj["metadataKey"].toInt();
|
|
file.authenticationTag = QByteArray::fromBase64(fileObj["authenticationTag"].toString().toLocal8Bit());
|
|
file.initializationVector = QByteArray::fromBase64(fileObj["initializationVector"].toString().toLocal8Bit());
|
|
|
|
//Decrypt encrypted part
|
|
QByteArray key = _metadataKeys[file.metadataKey];
|
|
auto encryptedFile = fileObj["encrypted"].toString().toLocal8Bit();
|
|
auto decryptedFile = QByteArray::fromBase64(decryptJsonObject(encryptedFile, key));
|
|
auto decryptedFileDoc = QJsonDocument::fromJson(decryptedFile);
|
|
auto decryptedFileObj = decryptedFileDoc.object();
|
|
|
|
file.originalFilename = decryptedFileObj["filename"].toString();
|
|
file.encryptionKey = QByteArray::fromBase64(decryptedFileObj["key"].toString().toLocal8Bit());
|
|
file.mimetype = decryptedFileObj["mimetype"].toString().toLocal8Bit();
|
|
file.fileVersion = decryptedFileObj["version"].toInt();
|
|
|
|
_files.push_back(file);
|
|
}
|
|
}
|
|
|
|
// RSA/ECB/OAEPWithSHA-256AndMGF1Padding using private / public key.
|
|
QByteArray FolderMetadata::encryptMetadataKey(const QByteArray& data) const {
|
|
|
|
BIO *publicKeyBio = BIO_new(BIO_s_mem());
|
|
QByteArray publicKeyPem = _account->e2e()->_publicKey.toPem();
|
|
BIO_write(publicKeyBio, publicKeyPem.constData(), publicKeyPem.size());
|
|
EVP_PKEY *publicKey = PEM_read_bio_PUBKEY(publicKeyBio, nullptr, nullptr, nullptr);
|
|
|
|
// The metadata key is binary so base64 encode it first
|
|
auto ret = EncryptionHelper::encryptStringAsymmetric(publicKey, data.toBase64());
|
|
EVP_PKEY_free(publicKey);
|
|
return ret; // ret is already b64
|
|
}
|
|
|
|
QByteArray FolderMetadata::decryptMetadataKey(const QByteArray& encryptedMetadata) const
|
|
{
|
|
BIO *privateKeyBio = BIO_new(BIO_s_mem());
|
|
QByteArray privateKeyPem = _account->e2e()->_privateKey;
|
|
BIO_write(privateKeyBio, privateKeyPem.constData(), privateKeyPem.size());
|
|
EVP_PKEY *key = PEM_read_bio_PrivateKey(privateKeyBio, nullptr, nullptr, nullptr);
|
|
|
|
// Also base64 decode the result
|
|
QByteArray decryptResult = EncryptionHelper::decryptStringAsymmetric(
|
|
key, QByteArray::fromBase64(encryptedMetadata));
|
|
|
|
if (decryptResult.isEmpty())
|
|
{
|
|
qCDebug(lcCse()) << "ERROR. Could not decrypt the metadata key";
|
|
return {};
|
|
}
|
|
return QByteArray::fromBase64(decryptResult);
|
|
}
|
|
|
|
// AES/GCM/NoPadding (128 bit key size)
|
|
QByteArray FolderMetadata::encryptJsonObject(const QByteArray& obj, const QByteArray pass) const
|
|
{
|
|
return EncryptionHelper::encryptStringSymmetric(pass, obj);
|
|
}
|
|
|
|
QByteArray FolderMetadata::decryptJsonObject(const QByteArray& encryptedMetadata, const QByteArray& pass) const
|
|
{
|
|
return EncryptionHelper::decryptStringSymmetric(pass, encryptedMetadata);
|
|
}
|
|
|
|
void FolderMetadata::setupEmptyMetadata() {
|
|
qCDebug(lcCse) << "Settint up empty metadata";
|
|
QByteArray newMetadataPass = EncryptionHelper::generateRandom(16);
|
|
_metadataKeys.insert(0, newMetadataPass);
|
|
|
|
QString publicKey = _account->e2e()->_publicKey.toPem().toBase64();
|
|
QString displayName = _account->displayName();
|
|
|
|
_sharing.append({displayName, publicKey});
|
|
}
|
|
|
|
QByteArray FolderMetadata::encryptedMetadata() {
|
|
qCDebug(lcCse) << "Generating metadata";
|
|
|
|
QJsonObject metadataKeys;
|
|
for (auto it = _metadataKeys.constBegin(), end = _metadataKeys.constEnd(); it != end; it++) {
|
|
/*
|
|
* We have to already base64 encode the metadatakey here. This was a misunderstanding in the RFC
|
|
* Now we should be compatible with Android and IOS. Maybe we can fix it later.
|
|
*/
|
|
const QByteArray encryptedKey = encryptMetadataKey(it.value().toBase64());
|
|
metadataKeys.insert(QString::number(it.key()), QString(encryptedKey));
|
|
}
|
|
|
|
/* NO SHARING IN V1
|
|
QJsonObject recepients;
|
|
for (auto it = _sharing.constBegin(), end = _sharing.constEnd(); it != end; it++) {
|
|
recepients.insert(it->first, it->second);
|
|
}
|
|
QJsonDocument recepientDoc;
|
|
recepientDoc.setObject(recepients);
|
|
QString sharingEncrypted = encryptJsonObject(recepientDoc.toJson(QJsonDocument::Compact), _metadataKeys.last());
|
|
*/
|
|
|
|
QJsonObject metadata = {
|
|
{"metadataKeys", metadataKeys},
|
|
// {"sharing", sharingEncrypted},
|
|
{"version", 1}
|
|
};
|
|
|
|
QJsonObject files;
|
|
for (auto it = _files.constBegin(), end = _files.constEnd(); it != end; it++) {
|
|
QJsonObject encrypted;
|
|
encrypted.insert("key", QString(it->encryptionKey.toBase64()));
|
|
encrypted.insert("filename", it->originalFilename);
|
|
encrypted.insert("mimetype", QString(it->mimetype));
|
|
encrypted.insert("version", it->fileVersion);
|
|
QJsonDocument encryptedDoc;
|
|
encryptedDoc.setObject(encrypted);
|
|
|
|
QString encryptedEncrypted = encryptJsonObject(encryptedDoc.toJson(QJsonDocument::Compact), _metadataKeys.last());
|
|
if (encryptedEncrypted.isEmpty()) {
|
|
qCDebug(lcCse) << "Metadata generation failed!";
|
|
}
|
|
|
|
QJsonObject file;
|
|
file.insert("encrypted", encryptedEncrypted);
|
|
file.insert("initializationVector", QString(it->initializationVector.toBase64()));
|
|
file.insert("authenticationTag", QString(it->authenticationTag.toBase64()));
|
|
file.insert("metadataKey", _metadataKeys.lastKey());
|
|
|
|
files.insert(it->encryptedFilename, file);
|
|
}
|
|
|
|
QJsonObject metaObject = {
|
|
{"metadata", metadata},
|
|
{"files", files}
|
|
};
|
|
|
|
QJsonDocument internalMetadata;
|
|
internalMetadata.setObject(metaObject);
|
|
return internalMetadata.toJson();
|
|
}
|
|
|
|
void FolderMetadata::addEncryptedFile(const EncryptedFile &f) {
|
|
|
|
for (int i = 0; i < _files.size(); i++) {
|
|
if (_files.at(i).originalFilename == f.originalFilename) {
|
|
_files.removeAt(i);
|
|
break;
|
|
}
|
|
}
|
|
|
|
_files.append(f);
|
|
}
|
|
|
|
void FolderMetadata::removeEncryptedFile(const EncryptedFile &f)
|
|
{
|
|
for (int i = 0; i < _files.size(); i++) {
|
|
if (_files.at(i).originalFilename == f.originalFilename) {
|
|
_files.removeAt(i);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
QVector<EncryptedFile> FolderMetadata::files() const {
|
|
return _files;
|
|
}
|
|
|
|
bool ClientSideEncryption::isFolderEncrypted(const QString& path) const {
|
|
auto it = _folder2encryptedStatus.constFind(path);
|
|
if (it == _folder2encryptedStatus.constEnd())
|
|
return false;
|
|
return (*it);
|
|
}
|
|
|
|
bool EncryptionHelper::fileEncryption(const QByteArray &key, const QByteArray &iv, QFile *input, QFile *output, QByteArray& returnTag)
|
|
{
|
|
if (!input->open(QIODevice::ReadOnly)) {
|
|
qCDebug(lcCse) << "Could not open input file for reading" << input->errorString();
|
|
}
|
|
if (!output->open(QIODevice::WriteOnly)) {
|
|
qCDebug(lcCse) << "Could not oppen output file for writting" << output->errorString();
|
|
}
|
|
|
|
// Init
|
|
EVP_CIPHER_CTX *ctx;
|
|
|
|
/* Create and initialise the context */
|
|
if(!(ctx = EVP_CIPHER_CTX_new())) {
|
|
qCInfo(lcCse()) << "Could not create context";
|
|
return false;
|
|
}
|
|
|
|
/* Initialise the decryption operation. */
|
|
if(!EVP_EncryptInit_ex(ctx, EVP_aes_128_gcm(), nullptr, nullptr, nullptr)) {
|
|
qCInfo(lcCse()) << "Could not init cipher";
|
|
return false;
|
|
}
|
|
|
|
EVP_CIPHER_CTX_set_padding(ctx, 0);
|
|
|
|
/* Set IV length. */
|
|
if(!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, iv.size(), nullptr)) {
|
|
qCInfo(lcCse()) << "Could not set iv length";
|
|
return false;
|
|
}
|
|
|
|
/* Initialise key and IV */
|
|
if(!EVP_EncryptInit_ex(ctx, nullptr, nullptr, (const unsigned char *)key.constData(), (const unsigned char *)iv.constData())) {
|
|
qCInfo(lcCse()) << "Could not set key and iv";
|
|
return false;
|
|
}
|
|
|
|
unsigned char *out = (unsigned char *)malloc(sizeof(unsigned char) * (1024 + 16 -1));
|
|
int len = 0;
|
|
int total_len = 0;
|
|
|
|
qCDebug(lcCse) << "Starting to encrypt the file" << input->fileName() << input->atEnd();
|
|
while(!input->atEnd()) {
|
|
QByteArray data = input->read(1024);
|
|
|
|
if (data.size() == 0) {
|
|
qCInfo(lcCse()) << "Could not read data from file";
|
|
return false;
|
|
}
|
|
|
|
qCDebug(lcCse) << "Encrypting " << data;
|
|
if(!EVP_EncryptUpdate(ctx, out, &len, (unsigned char *)data.constData(), data.size())) {
|
|
qCInfo(lcCse()) << "Could not encrypt";
|
|
return false;
|
|
}
|
|
|
|
output->write((char *)out, len);
|
|
total_len += len;
|
|
}
|
|
|
|
if(1 != EVP_EncryptFinal_ex(ctx, out, &len)) {
|
|
qCInfo(lcCse()) << "Could finalize encryption";
|
|
return false;
|
|
}
|
|
output->write((char *)out, len);
|
|
total_len += len;
|
|
|
|
/* Get the tag */
|
|
unsigned char *tag = (unsigned char *)malloc(sizeof(unsigned char) * 16);
|
|
if(1 != EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, tag)) {
|
|
qCInfo(lcCse()) << "Could not get tag";
|
|
return false;
|
|
}
|
|
|
|
returnTag = QByteArray((const char*) tag, 16);
|
|
output->write((char *)tag, 16);
|
|
|
|
free(out);
|
|
free(tag);
|
|
EVP_CIPHER_CTX_free(ctx);
|
|
|
|
input->close();
|
|
output->close();
|
|
qCDebug(lcCse) << "File Encrypted Successfully";
|
|
return true;
|
|
}
|
|
|
|
bool EncryptionHelper::fileDecryption(const QByteArray &key, const QByteArray& iv,
|
|
QFile *input, QFile *output)
|
|
{
|
|
input->open(QIODevice::ReadOnly);
|
|
output->open(QIODevice::WriteOnly);
|
|
|
|
// Init
|
|
EVP_CIPHER_CTX *ctx;
|
|
|
|
/* Create and initialise the context */
|
|
if(!(ctx = EVP_CIPHER_CTX_new())) {
|
|
qCInfo(lcCse()) << "Could not create context";
|
|
return false;
|
|
}
|
|
|
|
/* Initialise the decryption operation. */
|
|
if(!EVP_DecryptInit_ex(ctx, EVP_aes_128_gcm(), nullptr, nullptr, nullptr)) {
|
|
qCInfo(lcCse()) << "Could not init cipher";
|
|
return false;
|
|
}
|
|
|
|
EVP_CIPHER_CTX_set_padding(ctx, 0);
|
|
|
|
/* Set IV length. */
|
|
if(!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, iv.size(), nullptr)) {
|
|
qCInfo(lcCse()) << "Could not set iv length";
|
|
return false;
|
|
}
|
|
|
|
/* Initialise key and IV */
|
|
if(!EVP_DecryptInit_ex(ctx, nullptr, nullptr, (const unsigned char *) key.constData(), (const unsigned char *) iv.constData())) {
|
|
qCInfo(lcCse()) << "Could not set key and iv";
|
|
return false;
|
|
}
|
|
|
|
qint64 size = input->size() - 16;
|
|
|
|
unsigned char *out = (unsigned char *)malloc(sizeof(unsigned char) * (1024 + 16 -1));
|
|
int len = 0;
|
|
|
|
while(input->pos() < size) {
|
|
|
|
int toRead = size - input->pos();
|
|
if (toRead > 1024) {
|
|
toRead = 1024;
|
|
}
|
|
|
|
QByteArray data = input->read(toRead);
|
|
|
|
if (data.size() == 0) {
|
|
qCInfo(lcCse()) << "Could not read data from file";
|
|
return false;
|
|
}
|
|
|
|
if(!EVP_DecryptUpdate(ctx, out, &len, (unsigned char *)data.constData(), data.size())) {
|
|
qCInfo(lcCse()) << "Could not decrypt";
|
|
return false;
|
|
}
|
|
|
|
output->write((char *)out, len);
|
|
}
|
|
|
|
QByteArray tag = input->read(16);
|
|
|
|
/* Set expected tag value. Works in OpenSSL 1.0.1d and later */
|
|
if(!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, tag.size(), (unsigned char *)tag.constData())) {
|
|
qCInfo(lcCse()) << "Could not set expected tag";
|
|
return false;
|
|
}
|
|
|
|
if(1 != EVP_DecryptFinal_ex(ctx, out, &len)) {
|
|
qCInfo(lcCse()) << "Could finalize decryption";
|
|
return false;
|
|
}
|
|
output->write((char *)out, len);
|
|
|
|
free(out);
|
|
EVP_CIPHER_CTX_free(ctx);
|
|
|
|
input->close();
|
|
output->close();
|
|
return true;
|
|
}
|
|
|
|
}
|