Merge pull request #5560 from nextcloud/feature/e2eeFixes

Feature/e2ee fixes
This commit is contained in:
Matthieu Gallien 2023-03-31 17:18:58 +02:00 committed by GitHub
commit 086eb14b83
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 709 additions and 515 deletions

View File

@ -66,7 +66,7 @@ static void fillFileRecordFromGetQuery(SyncJournalFileRecord &rec, SqlQuery &que
rec._serverHasIgnoredFiles = (query.intValue(8) > 0);
rec._checksumHeader = query.baValue(9);
rec._e2eMangledName = query.baValue(10);
rec._isE2eEncrypted = query.intValue(11) > 0;
rec._isE2eEncrypted = static_cast<SyncJournalFileRecord::EncryptionStatus>(query.intValue(11));
rec._lockstate._locked = query.intValue(12) > 0;
rec._lockstate._lockOwnerDisplayName = query.stringValue(13);
rec._lockstate._lockOwnerId = query.stringValue(14);
@ -910,7 +910,7 @@ Result<void, QString> SyncJournalDb::setFileRecord(const SyncJournalFileRecord &
<< "modtime:" << record._modtime << "type:" << record._type << "etag:" << record._etag
<< "fileId:" << record._fileId << "remotePerm:" << record._remotePerm.toString()
<< "fileSize:" << record._fileSize << "checksum:" << record._checksumHeader
<< "e2eMangledName:" << record.e2eMangledName() << "isE2eEncrypted:" << record._isE2eEncrypted
<< "e2eMangledName:" << record.e2eMangledName() << "isE2eEncrypted:" << record.isE2eEncrypted()
<< "lock:" << (record._lockstate._locked ? "true" : "false")
<< "lock owner type:" << record._lockstate._lockOwnerType
<< "lock owner:" << record._lockstate._lockOwnerDisplayName
@ -968,7 +968,7 @@ Result<void, QString> SyncJournalDb::setFileRecord(const SyncJournalFileRecord &
query->bindValue(15, checksum);
query->bindValue(16, contentChecksumTypeId);
query->bindValue(17, record._e2eMangledName);
query->bindValue(18, record._isE2eEncrypted);
query->bindValue(18, static_cast<int>(record._isE2eEncrypted));
query->bindValue(19, record._lockstate._locked ? 1 : 0);
query->bindValue(20, record._lockstate._lockOwnerType);
query->bindValue(21, record._lockstate._lockOwnerDisplayName);
@ -2694,4 +2694,10 @@ bool operator==(const SyncJournalDb::UploadInfo &lhs,
&& lhs._contentChecksum == rhs._contentChecksum;
}
QDebug& operator<<(QDebug &stream, const SyncJournalFileRecord::EncryptionStatus status)
{
stream << static_cast<int>(status);
return stream;
}
} // namespace OCC

View File

@ -53,6 +53,12 @@ public:
return !_path.isEmpty();
}
enum class EncryptionStatus : int {
NotEncrypted = 0,
Encrypted = 1,
EncryptedMigratedV1_2 = 2,
};
/** Returns the numeric part of the full id in _fileId.
*
* On the server this is sometimes known as the internal file id.
@ -67,6 +73,7 @@ public:
[[nodiscard]] bool isVirtualFile() const { return _type == ItemTypeVirtualFile || _type == ItemTypeVirtualFileDownload; }
[[nodiscard]] QString path() const { return QString::fromUtf8(_path); }
[[nodiscard]] QString e2eMangledName() const { return QString::fromUtf8(_e2eMangledName); }
[[nodiscard]] bool isE2eEncrypted() const { return _isE2eEncrypted != SyncJournalFileRecord::EncryptionStatus::NotEncrypted; }
QByteArray _path;
quint64 _inode = 0;
@ -79,13 +86,15 @@ public:
bool _serverHasIgnoredFiles = false;
QByteArray _checksumHeader;
QByteArray _e2eMangledName;
bool _isE2eEncrypted = false;
EncryptionStatus _isE2eEncrypted = EncryptionStatus::NotEncrypted;
SyncJournalFileLockInfo _lockstate;
bool _isShared = false;
qint64 _lastShareStateFetchedTimestamp = 0;
bool _sharedByMe = false;
};
QDebug& operator<<(QDebug &stream, const SyncJournalFileRecord::EncryptionStatus status);
bool OCSYNC_EXPORT
operator==(const SyncJournalFileRecord &lhs,
const SyncJournalFileRecord &rhs);

View File

@ -520,7 +520,7 @@ void AccountSettings::slotSubfolderContextMenuRequested(const QModelIndex& index
if (acc->capabilities().clientSideEncryptionAvailable()) {
// Verify if the folder is empty before attempting to encrypt.
const auto isEncrypted = info->_isEncrypted;
const auto isEncrypted = info->isEncrypted();
const auto isParentEncrypted = _model->isAnyAncestorEncrypted(index);
if (!isEncrypted && !isParentEncrypted) {

View File

@ -250,9 +250,9 @@ void ShareModel::updateData()
_numericFileId = fileRecord.numericFileId();
_isEncryptedItem = fileRecord._isE2eEncrypted;
_isEncryptedItem = fileRecord.isE2eEncrypted();
_isSecureFileDropSupportedFolder =
fileRecord._isE2eEncrypted && fileRecord.e2eMangledName().isEmpty() && _accountState->account()->secureFileDropSupported();
fileRecord.isE2eEncrypted() && fileRecord.e2eMangledName().isEmpty() && _accountState->account()->secureFileDropSupported();
// Will get added when shares are fetched if no link shares are fetched
_placeholderLinkShare.reset(new Share(_accountState->account(),

View File

@ -1347,7 +1347,7 @@ void Folder::removeLocalE2eFiles()
QStringList e2eFoldersToBlacklist;
const auto couldGetFiles = _journal.getFilesBelowPath("", [this, &e2eFoldersToBlacklist, &folderRootDir](const SyncJournalFileRecord &rec) {
// We only want to add the root-most encrypted folder to the blacklist
if (rec.isValid() && rec._isE2eEncrypted && rec.isDirectory()) {
if (rec.isValid() && rec.isE2eEncrypted() && rec.isDirectory()) {
QDir pathDir(_canonicalLocalPath + rec.path());
bool parentPathEncrypted = false;
@ -1359,7 +1359,7 @@ void Folder::removeLocalE2eFiles()
qCWarning(lcFolder) << "Failed to get file record for" << currentCanonicalPath;
}
if (dirRec._isE2eEncrypted) {
if (dirRec.isE2eEncrypted()) {
parentPathEncrypted = true;
break;
}

View File

@ -162,7 +162,7 @@ QVariant FolderStatusModel::data(const QModelIndex &index, int role) const
case Qt::DisplayRole: {
//: Example text: "File.txt (23KB)"
const auto &xParent = static_cast<SubFolderInfo *>(index.internalPointer());
const auto suffix = (subfolderInfo._isNonDecryptable && subfolderInfo._checked && (!xParent || !xParent->_isEncrypted))
const auto suffix = (subfolderInfo._isNonDecryptable && subfolderInfo._checked && (!xParent || !xParent->isEncrypted()))
? QStringLiteral(" - ") + tr("Could not decrypt!")
: QString{};
return subfolderInfo._size < 0 ? QString(subfolderInfo._name + suffix) : QString(tr("%1 (%2)").arg(subfolderInfo._name, Utility::octetsToString(subfolderInfo._size)) + suffix);
@ -179,7 +179,7 @@ QVariant FolderStatusModel::data(const QModelIndex &index, int role) const
if (subfolderInfo._isNonDecryptable && subfolderInfo._checked) {
return QIcon(QLatin1String(":/client/theme/lock-broken.svg"));
}
if (subfolderInfo._isEncrypted) {
if (subfolderInfo.isEncrypted()) {
return QIcon(QLatin1String(":/client/theme/lock-https.svg"));
} else if (subfolderInfo._size > 0 && isAnyAncestorEncrypted(index)) {
return QIcon(QLatin1String(":/client/theme/lock-broken.svg"));
@ -445,7 +445,7 @@ bool FolderStatusModel::isAnyAncestorEncrypted(const QModelIndex &index) const
auto parentIndex = parent(index);
while (parentIndex.isValid()) {
const auto info = infoForIndex(parentIndex);
if (info->_isEncrypted) {
if (info->isEncrypted()) {
return true;
}
parentIndex = parent(parentIndex);
@ -607,7 +607,7 @@ void FolderStatusModel::fetchMore(const QModelIndex &parent)
QString path = info->_folder->remotePathTrailingSlash();
// info->_path always contains non-mangled name, so we need to use mangled when requesting nested folders for encrypted subfolders as required by LsColJob
const QString infoPath = (info->_isEncrypted && !info->_e2eMangledName.isEmpty()) ? info->_e2eMangledName : info->_path;
const QString infoPath = (info->isEncrypted() && !info->_e2eMangledName.isEmpty()) ? info->_e2eMangledName : info->_path;
if (infoPath != QLatin1String("/")) {
path += infoPath;
@ -752,7 +752,7 @@ void FolderStatusModel::slotUpdateDirectories(const QStringList &list)
newInfo._isEncrypted = encryptionMap.value(removeTrailingSlash(path)).toString() == QStringLiteral("1");
newInfo._path = relativePath;
newInfo._isNonDecryptable = newInfo._isEncrypted
newInfo._isNonDecryptable = newInfo.isEncrypted()
&& _accountState->account()->e2e() && !_accountState->account()->e2e()->_publicKey.isNull()
&& _accountState->account()->e2e()->_privateKey.isNull();
@ -762,7 +762,7 @@ void FolderStatusModel::slotUpdateDirectories(const QStringList &list)
}
if (rec.isValid()) {
newInfo._name = removeTrailingSlash(rec._path).split('/').last();
if (rec._isE2eEncrypted && !rec._e2eMangledName.isEmpty()) {
if (rec.isE2eEncrypted() && !rec._e2eMangledName.isEmpty()) {
// we must use local path for Settings Dialog's filesystem tree, otherwise open and create new folder actions won't work
// hence, we are storing _e2eMangledName separately so it can be use later for LsColJob
newInfo._e2eMangledName = relativePath;

View File

@ -85,6 +85,8 @@ public:
// Whether this has a FetchLabel subrow
[[nodiscard]] bool hasLabel() const;
[[nodiscard]] bool isEncrypted() const { return _isEncrypted; }
// Reset all subfolders and fetch status
void resetSubs(FolderStatusModel *model, QModelIndex index);

View File

@ -1220,7 +1220,7 @@ void SocketApi::sendEncryptFolderCommandMenuEntries(const QFileInfo &fileInfo,
bool anyAncestorEncrypted = false;
auto ancestor = fileData.parentFolder();
while (ancestor.journalRecord().isValid()) {
if (ancestor.journalRecord()._isE2eEncrypted) {
if (ancestor.journalRecord().isE2eEncrypted()) {
anyAncestorEncrypted = true;
break;
}
@ -1352,8 +1352,8 @@ void SocketApi::command_GET_MENU_ITEMS(const QString &argument, OCC::SocketListe
FileData fileData = FileData::get(argument);
const auto record = fileData.journalRecord();
const bool isOnTheServer = record.isValid();
const auto isE2eEncryptedPath = fileData.journalRecord()._isE2eEncrypted || !fileData.journalRecord()._e2eMangledName.isEmpty();
const auto isE2eEncryptedRootFolder = fileData.journalRecord()._isE2eEncrypted && fileData.journalRecord()._e2eMangledName.isEmpty();
const auto isE2eEncryptedPath = fileData.journalRecord().isE2eEncrypted() || !fileData.journalRecord()._e2eMangledName.isEmpty();
const auto isE2eEncryptedRootFolder = fileData.journalRecord().isE2eEncrypted() && fileData.journalRecord()._e2eMangledName.isEmpty();
auto flagString = isOnTheServer && !isE2eEncryptedPath ? QLatin1String("::") : QLatin1String(":d:");
const QFileInfo fileInfo(fileData.localPath);

View File

@ -159,7 +159,7 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const
if (!folder->journalDb()->getFileRecord(fileName.mid(1), &rec)) {
qCWarning(lcActivity) << "could not get file from local DB" << fileName.mid(1);
}
if (rec.isValid() && (rec._isE2eEncrypted || !rec._e2eMangledName.isEmpty())) {
if (rec.isValid() && (rec.isE2eEncrypted() || !rec._e2eMangledName.isEmpty())) {
return QString();
}
}

View File

@ -166,6 +166,22 @@ bool Capabilities::clientSideEncryptionAvailable() const
return capabilityAvailable;
}
double Capabilities::clientSideEncryptionVersion() const
{
const auto foundEndToEndEncryptionInCaps = _capabilities.constFind(QStringLiteral("end-to-end-encryption"));
if (foundEndToEndEncryptionInCaps == _capabilities.constEnd()) {
return 1.0;
}
const auto properties = (*foundEndToEndEncryptionInCaps).toMap();
const auto enabled = properties.value(QStringLiteral("enabled"), false).toBool();
if (!enabled) {
return false;
}
return properties.value(QStringLiteral("api-version"), 1.0).toDouble();
}
bool Capabilities::notificationsAvailable() const
{
// We require the OCS style API in 9.x, can't deal with the REST one only found in 8.2

View File

@ -89,6 +89,8 @@ public:
/// returns true if the server supports client side encryption
[[nodiscard]] bool clientSideEncryptionAvailable() const;
[[nodiscard]] double clientSideEncryptionVersion() const;
/// returns true if the capabilities are loaded already.
[[nodiscard]] bool isValid() const;

File diff suppressed because it is too large Load Diff

View File

@ -182,13 +182,22 @@ struct EncryptedFile {
QByteArray authenticationTag;
QString encryptedFilename;
QString originalFilename;
int fileVersion = 0;
int metadataKey = 0;
};
class OWNCLOUDSYNC_EXPORT FolderMetadata {
public:
FolderMetadata(AccountPtr account, const QByteArray& metadata = QByteArray(), int statusCode = -1);
enum class RequiredMetadataVersion {
Version1,
Version1_2,
};
explicit FolderMetadata(AccountPtr account);
explicit FolderMetadata(AccountPtr account,
RequiredMetadataVersion requiredMetadataVersion,
const QByteArray& metadata,
int statusCode = -1);
[[nodiscard]] QByteArray encryptedMetadata() const;
void addEncryptedFile(const EncryptedFile& f);
void removeEncryptedFile(const EncryptedFile& f);
@ -198,6 +207,8 @@ public:
[[nodiscard]] bool isFileDropPresent() const;
[[nodiscard]] bool encryptedMetadataNeedUpdate() const;
[[nodiscard]] bool moveFromFileDropToFiles();
[[nodiscard]] QJsonObject fileDrop() const;
@ -211,15 +222,27 @@ private:
[[nodiscard]] QByteArray encryptData(const QByteArray &data) const;
[[nodiscard]] QByteArray decryptData(const QByteArray &data) const;
[[nodiscard]] QByteArray decryptDataUsingKey(const QByteArray &data,
const QByteArray &key,
const QByteArray &authenticationTag,
const QByteArray &initializationVector) const;
[[nodiscard]] QByteArray encryptJsonObject(const QByteArray& obj, const QByteArray pass) const;
[[nodiscard]] QByteArray decryptJsonObject(const QByteArray& encryptedJsonBlob, const QByteArray& pass) const;
[[nodiscard]] bool checkMetadataKeyChecksum(const QByteArray &metadataKey, const QByteArray &metadataKeyChecksum) const;
[[nodiscard]] QByteArray computeMetadataKeyChecksum(const QByteArray &metadataKey) const;
QByteArray _metadataKey;
QVector<EncryptedFile> _files;
QMap<int, QByteArray> _metadataKeys;
AccountPtr _account;
RequiredMetadataVersion _requiredMetadataVersion = RequiredMetadataVersion::Version1_2;
QVector<QPair<QString, QString>> _sharing;
QJsonObject _fileDrop;
bool _isMetadataSetup = false;
bool _encryptedMetadataNeedUpdate = false;
};
} // namespace OCC

View File

@ -55,7 +55,9 @@ bool GetMetadataApiJob::finished()
return true;
}
QJsonParseError error{};
auto json = QJsonDocument::fromJson(reply()->readAll(), &error);
const auto replyData = reply()->readAll();
auto json = QJsonDocument::fromJson(replyData, &error);
qCInfo(lcCseJob) << "metadata received for file id" << _fileId << json.toJson(QJsonDocument::Compact);
emit jsonReceived(json, reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
return true;
}

View File

@ -218,7 +218,7 @@ void ProcessDirectoryJob::process()
if (handleExcluded(path._target, e, isHidden))
continue;
const auto isEncryptedFolderButE2eIsNotSetup = e.serverEntry.isValid() && e.serverEntry.isE2eEncrypted &&
const auto isEncryptedFolderButE2eIsNotSetup = e.serverEntry.isValid() && e.serverEntry.isE2eEncrypted() &&
_discoveryData->_account->e2e() && !_discoveryData->_account->e2e()->_publicKey.isNull() && _discoveryData->_account->e2e()->_privateKey.isNull();
if (isEncryptedFolderButE2eIsNotSetup) {
@ -431,7 +431,7 @@ void ProcessDirectoryJob::processFile(PathTuple path,
<< " | fileid: " << dbEntry._fileId << "//" << serverEntry.fileId
<< " | inode: " << dbEntry._inode << "/" << localEntry.inode << "/"
<< " | type: " << dbEntry._type << "/" << localEntry.type << "/" << (serverEntry.isDirectory ? ItemTypeDirectory : ItemTypeFile)
<< " | e2ee: " << dbEntry._isE2eEncrypted << "/" << serverEntry.isE2eEncrypted
<< " | e2ee: " << dbEntry.isE2eEncrypted() << "/" << serverEntry.isE2eEncrypted()
<< " | e2eeMangledName: " << dbEntry.e2eMangledName() << "/" << serverEntry.e2eMangledName
<< " | file lock: " << localFileIsLocked << "//" << serverFileIsLocked;
@ -532,7 +532,7 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo(
item->_etag = serverEntry.etag;
item->_directDownloadUrl = serverEntry.directDownloadUrl;
item->_directDownloadCookies = serverEntry.directDownloadCookies;
item->_isEncrypted = serverEntry.isE2eEncrypted;
item->_isEncrypted = serverEntry.isE2eEncrypted() ? SyncFileItem::EncryptionStatus::Encrypted : SyncFileItem::EncryptionStatus::NotEncrypted;
item->_encryptedFileName = [=] {
if (serverEntry.e2eMangledName.isEmpty()) {
return QString();
@ -656,7 +656,7 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo(
// or, maybe, add a flag to the database - vfsE2eeSizeCorrected? if it is not set - subtract it from the placeholder's size and re-create/update a placeholder?
const QueryMode serverQueryMode = [this, &dbEntry, &serverEntry]() {
const bool isVfsModeOn = _discoveryData && _discoveryData->_syncOptions._vfs && _discoveryData->_syncOptions._vfs->mode() != Vfs::Off;
if (isVfsModeOn && dbEntry.isDirectory() && dbEntry._isE2eEncrypted) {
if (isVfsModeOn && dbEntry.isDirectory() && dbEntry.isE2eEncrypted()) {
qint64 localFolderSize = 0;
const auto listFilesCallback = [&localFolderSize](const OCC::SyncJournalFileRecord &record) {
if (record.isFile()) {
@ -1240,7 +1240,7 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo(
return false;
}
if (base._isE2eEncrypted || isInsideEncryptedTree()) {
if (base.isE2eEncrypted() || isInsideEncryptedTree()) {
return false;
}
@ -1289,10 +1289,10 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo(
// If it's not a move it's just a local-NEW
if (!moveCheck()) {
if (base._isE2eEncrypted) {
if (base.isE2eEncrypted()) {
// renaming the encrypted folder is done via remove + re-upload hence we need to mark the newly created folder as encrypted
// base is a record in the SyncJournal database that contains the data about the being-renamed folder with it's old name and encryption information
item->_isEncrypted = true;
item->_isEncrypted = static_cast<SyncFileItem::EncryptionStatus>(base._isE2eEncrypted);
}
postProcessLocalNew();
finalize();
@ -1551,7 +1551,7 @@ void ProcessDirectoryJob::processFileFinalize(
if (recurse) {
auto job = new ProcessDirectoryJob(path, item, recurseQueryLocal, recurseQueryServer,
_lastSyncTimestamp, this);
job->setInsideEncryptedTree(isInsideEncryptedTree() || item->_isEncrypted);
job->setInsideEncryptedTree(isInsideEncryptedTree() || item->isEncrypted());
if (removed) {
job->setParent(_discoveryData);
_discoveryData->enqueueDirectoryToDelete(path._original, job);
@ -1850,6 +1850,7 @@ DiscoverySingleDirectoryJob *ProcessDirectoryJob::startAsyncServerQuery()
connect(serverJob, &DiscoverySingleDirectoryJob::finished, this, [this, serverJob](const auto &results) {
if (_dirItem) {
_dirItem->_isFileDropDetected = serverJob->isFileDropDetected();
_dirItem->_isEncryptedMetadataNeedUpdate = serverJob->encryptedMetadataNeedUpdate();
qCInfo(lcDisco) << "serverJob has finished for folder:" << _dirItem->_file << " and it has _isFileDropDetected:" << true;
}
_discoveryData->_currentlyActiveJobs--;

View File

@ -410,6 +410,11 @@ bool DiscoverySingleDirectoryJob::isFileDropDetected() const
return _isFileDropDetected;
}
bool DiscoverySingleDirectoryJob::encryptedMetadataNeedUpdate() const
{
return _encryptedMetadataNeedUpdate;
}
static void propertyMapToRemoteInfo(const QMap<QString, QString> &map, RemoteInfo &result)
{
for (auto it = map.constBegin(); it != map.constEnd(); ++it) {
@ -458,7 +463,7 @@ static void propertyMapToRemoteInfo(const QMap<QString, QString> &map, RemoteInf
result.sharedByMe = true;
}
} else if (property == "is-encrypted" && value == QStringLiteral("1")) {
result.isE2eEncrypted = true;
result._isE2eEncrypted = true;
} else if (property == "lock") {
result.locked = (value == QStringLiteral("1") ? SyncFileItem::LockStatus::LockedItem : SyncFileItem::LockStatus::UnlockedItem);
}
@ -530,7 +535,7 @@ void DiscoverySingleDirectoryJob::directoryListingIteratedSlot(const QString &fi
_fileId = map.value("id").toUtf8();
}
if (map.contains("is-encrypted") && map.value("is-encrypted") == QStringLiteral("1")) {
_isE2eEncrypted = true;
_isE2eEncrypted = SyncFileItem::EncryptionStatus::Encrypted;
Q_ASSERT(!_fileId.isEmpty());
}
if (map.contains("size")) {
@ -576,7 +581,7 @@ void DiscoverySingleDirectoryJob::lsJobFinishedWithoutErrorSlot()
emit finished(HttpError{ 0, _error });
deleteLater();
return;
} else if (_isE2eEncrypted) {
} else if (isE2eEncrypted()) {
emit etag(_firstEtag, QDateTime::fromString(QString::fromUtf8(_lsColJob->responseTimestamp()), Qt::RFC2822Date));
fetchE2eMetadata();
return;
@ -621,8 +626,13 @@ void DiscoverySingleDirectoryJob::metadataReceived(const QJsonDocument &json, in
qCDebug(lcDiscovery) << "Metadata received, applying it to the result list";
Q_ASSERT(_subPath.startsWith('/'));
const auto metadata = FolderMetadata(_account, json.toJson(QJsonDocument::Compact), statusCode);
const auto metadata = FolderMetadata(_account,
_isE2eEncrypted == SyncFileItem::EncryptionStatus::EncryptedMigratedV1_2 ? FolderMetadata::RequiredMetadataVersion::Version1_2 : FolderMetadata::RequiredMetadataVersion::Version1,
json.toJson(QJsonDocument::Compact),
statusCode);
_isFileDropDetected = metadata.isFileDropPresent();
_encryptedMetadataNeedUpdate = metadata.encryptedMetadataNeedUpdate();
const auto encryptedFiles = metadata.files();
const auto findEncryptedFile = [=](const QString &name) {
@ -640,7 +650,7 @@ void DiscoverySingleDirectoryJob::metadataReceived(const QJsonDocument &json, in
auto result = info;
const auto encryptedFileInfo = findEncryptedFile(result.name);
if (encryptedFileInfo) {
result.isE2eEncrypted = true;
result._isE2eEncrypted = true;
result.e2eMangledName = _subPath.mid(1) + QLatin1Char('/') + result.name;
result.name = encryptedFileInfo->originalFilename;
}

View File

@ -66,12 +66,13 @@ struct RemoteInfo
int64_t size = 0;
int64_t sizeOfFolder = 0;
bool isDirectory = false;
bool isE2eEncrypted = false;
bool _isE2eEncrypted = false;
bool isFileDropDetected = false;
QString e2eMangledName;
bool sharedByMe = false;
[[nodiscard]] bool isValid() const { return !name.isNull(); }
[[nodiscard]] bool isE2eEncrypted() const { return _isE2eEncrypted; }
QString directDownloadUrl;
QString directDownloadCookies;
@ -144,6 +145,7 @@ public:
void start();
void abort();
[[nodiscard]] bool isFileDropDetected() const;
[[nodiscard]] bool encryptedMetadataNeedUpdate() const;
// This is not actually a network job, it is just a job
signals:
@ -160,6 +162,9 @@ private slots:
void metadataError(const QByteArray& fileId, int httpReturnCode);
private:
[[nodiscard]] bool isE2eEncrypted() const { return _isE2eEncrypted != SyncFileItem::EncryptionStatus::NotEncrypted; }
QVector<RemoteInfo> _results;
QString _subPath;
QByteArray _firstEtag;
@ -174,8 +179,9 @@ private:
// If this directory is an external storage (The first item has 'M' in its permission)
bool _isExternalStorage = false;
// If this directory is e2ee
bool _isE2eEncrypted = false;
SyncFileItem::EncryptionStatus _isE2eEncrypted = SyncFileItem::EncryptionStatus::NotEncrypted;
bool _isFileDropDetected = false;
bool _encryptedMetadataNeedUpdate = false;
// If set, the discovery will finish with an error
int64_t _size = 0;
QString _error;

View File

@ -56,7 +56,7 @@ void EncryptFolderJob::slotEncryptionFlagSuccess(const QByteArray &fileId)
qCWarning(lcEncryptFolderJob) << "No valid record found in local DB for fileId" << fileId;
}
rec._isE2eEncrypted = true;
rec._isE2eEncrypted = SyncJournalFileRecord::EncryptionStatus::EncryptedMigratedV1_2;
const auto result = _journal->setFileRecord(rec);
if (!result) {
qCWarning(lcEncryptFolderJob) << "Error when setting the file record to the database" << rec._path << result.error();

View File

@ -326,7 +326,7 @@ bool PropagateItemJob::hasEncryptedAncestor() const
qCWarning(lcPropagator) << "could not get file from local DB" << pathCompontentsJointed;
}
if (rec.isValid() && rec._isE2eEncrypted) {
if (rec.isValid() && rec.isE2eEncrypted()) {
return true;
}
pathComponents.removeLast();
@ -650,6 +650,19 @@ void OwncloudPropagator::startDirectoryPropagation(const SyncFileItemPtr &item,
directoryPropagationJob->appendJob(new UpdateFileDropMetadataJob(this, item->_file));
item->_instruction = CSYNC_INSTRUCTION_NONE;
_anotherSyncNeeded = true;
} else if (item->_isEncryptedMetadataNeedUpdate) {
SyncJournalFileRecord record;
if (_journal->getFileRecord(item->_file, &record) && record._isE2eEncrypted == SyncJournalFileRecord::EncryptionStatus::EncryptedMigratedV1_2) {
qCDebug(lcPropagator) << "could have upgraded metadata";
item->_instruction = CSyncEnums::CSYNC_INSTRUCTION_ERROR;
item->_errorString = tr("Error with the metadata. Getting unexpected metadata format.");
item->_status = SyncFileItem::NormalError;
emit itemCompleted(item);
} else {
directoryPropagationJob->appendJob(new UpdateFileDropMetadataJob(this, item->_file));
item->_instruction = CSYNC_INSTRUCTION_NONE;
_anotherSyncNeeded = true;
}
}
directories.push(qMakePair(item->destination() + "/", directoryPropagationJob.release()));
}
@ -1021,14 +1034,14 @@ bool OwncloudPropagator::isDelayedUploadItem(const SyncFileItemPtr &item) const
if (!accountPtr->capabilities().clientSideEncryptionAvailable() ||
!parentRec.isValid() ||
!parentRec._isE2eEncrypted) {
!parentRec.isE2eEncrypted()) {
return false;
}
return true;
};
return account()->capabilities().bulkUpload() && !_scheduleDelayedTasks && !item->_isEncrypted && _syncOptions._minChunkSize > item->_size && !isInBulkUploadBlackList(item->_file) && !checkFileShouldBeEncrypted(item);
return account()->capabilities().bulkUpload() && !_scheduleDelayedTasks && !item->isEncrypted() && _syncOptions._minChunkSize > item->_size && !isInBulkUploadBlackList(item->_file) && !checkFileShouldBeEncrypted(item);
}
void OwncloudPropagator::setScheduleDelayedTasks(bool active)

View File

@ -199,7 +199,7 @@ public:
// TODO: In fact, we must make sure Lock/Unlock are not colliding and always wait for each other to complete. So, we could refactor this "_parallelism" later
// so every "PropagateItemJob" that will potentially execute Lock job on E2EE folder will get executed sequentially.
// As an alternative, we could optimize Lock/Unlock calls, so we do a batch-write on one folder and only lock and unlock a folder once per batch.
_parallelism = (_item->_isEncrypted || hasEncryptedAncestor()) ? WaitForFinished : FullParallelism;
_parallelism = (_item->isEncrypted() || hasEncryptedAncestor()) ? WaitForFinished : FullParallelism;
}
~PropagateItemJob() override;

View File

@ -468,7 +468,7 @@ void PropagateDownloadFile::start()
const auto account = propagator()->account();
if (!account->capabilities().clientSideEncryptionAvailable() ||
!parentRec.isValid() ||
!parentRec._isE2eEncrypted) {
!parentRec.isE2eEncrypted()) {
startAfterIsEncryptedIsChecked();
} else {
_downloadEncryptedHelper = new PropagateDownloadEncrypted(propagator(), parentPath, _item, this);
@ -718,7 +718,7 @@ void PropagateDownloadFile::startDownload()
if (_item->_directDownloadUrl.isEmpty()) {
// Normal job, download from oC instance
_job = new GETFileJob(propagator()->account(),
propagator()->fullRemotePath(_isEncrypted ? _item->_encryptedFileName : _item->_file),
propagator()->fullRemotePath(isEncrypted() ? _item->_encryptedFileName : _item->_file),
&_tmpFile, headers, expectedEtagForResume, _resumeStart, this);
} else {
// We were provided a direct URL, use that one
@ -940,7 +940,7 @@ void PropagateDownloadFile::slotChecksumFail(const QString &errMsg,
{
if (reason == ValidateChecksumHeader::FailureReason::ChecksumMismatch && propagator()->account()->isChecksumRecalculateRequestSupported()) {
const QByteArray calculatedChecksumHeader(calculatedChecksumType + ':' + calculatedChecksum);
const QString fullRemotePathForFile(propagator()->fullRemotePath(_isEncrypted ? _item->_encryptedFileName : _item->_file));
const QString fullRemotePathForFile(propagator()->fullRemotePath(isEncrypted() ? _item->_encryptedFileName : _item->_file));
auto *job = new SimpleFileJob(propagator()->account(), fullRemotePathForFile);
QObject::connect(job, &SimpleFileJob::finishedSignal, this,
[this, calculatedChecksumHeader, errMsg](const QNetworkReply *reply) { processChecksumRecalculate(reply, calculatedChecksumHeader, errMsg);
@ -1137,7 +1137,7 @@ void PropagateDownloadFile::localFileContentChecksumComputed(const QByteArray &c
void PropagateDownloadFile::finalizeDownload()
{
if (_isEncrypted) {
if (isEncrypted()) {
if (_downloadEncryptedHelper->decryptFile(_tmpFile)) {
downloadFinished();
} else {
@ -1329,7 +1329,7 @@ void PropagateDownloadFile::updateMetadata(bool isConflict)
return;
}
if (_isEncrypted) {
if (isEncrypted()) {
propagator()->_journal->setDownloadInfo(_item->_file, SyncJournalDb::DownloadInfo());
} else {
propagator()->_journal->setDownloadInfo(_item->_encryptedFileName, SyncJournalDb::DownloadInfo());

View File

@ -245,6 +245,7 @@ private slots:
private:
void startAfterIsEncryptedIsChecked();
void deleteExistingFolder();
[[nodiscard]] bool isEncrypted() const { return _isEncrypted; }
qint64 _resumeStart = 0;
qint64 _downloadProgress = 0;

View File

@ -73,7 +73,9 @@ void PropagateDownloadEncrypted::checkFolderEncryptedMetadata(const QJsonDocumen
qCDebug(lcPropagateDownloadEncrypted) << "Metadata Received reading"
<< _item->_instruction << _item->_file << _item->_encryptedFileName;
const QString filename = _info.fileName();
const FolderMetadata metadata(_propagator->account(), json.toJson(QJsonDocument::Compact));
const FolderMetadata metadata(_propagator->account(),
_item->_isEncrypted == SyncFileItem::EncryptionStatus::EncryptedMigratedV1_2 ? FolderMetadata::RequiredMetadataVersion::Version1_2 : FolderMetadata::RequiredMetadataVersion::Version1,
json.toJson(QJsonDocument::Compact));
if (metadata.isMetadataSetup()) {
const QVector<EncryptedFile> files = metadata.files();

View File

@ -33,7 +33,7 @@ void PropagateRemoteDelete::start()
if (propagator()->_abortRequested)
return;
if (!_item->_encryptedFileName.isEmpty() || _item->_isEncrypted) {
if (!_item->_encryptedFileName.isEmpty() || _item->isEncrypted()) {
if (!_item->_encryptedFileName.isEmpty()) {
_deleteEncryptedHelper = new PropagateRemoteDeleteEncrypted(propagator(), _item, this);
} else {

View File

@ -51,7 +51,9 @@ void PropagateRemoteDeleteEncrypted::slotFolderEncryptedMetadataReceived(const Q
return;
}
FolderMetadata metadata(_propagator->account(), json.toJson(QJsonDocument::Compact), statusCode);
FolderMetadata metadata(_propagator->account(),
_item->_isEncrypted == SyncFileItem::EncryptionStatus::EncryptedMigratedV1_2 ? FolderMetadata::RequiredMetadataVersion::Version1_2 : FolderMetadata::RequiredMetadataVersion::Version1,
json.toJson(QJsonDocument::Compact), statusCode);
if (!metadata.isMetadataSetup()) {
taskFailed();

View File

@ -49,7 +49,7 @@ PropagateRemoteDeleteEncryptedRootFolder::PropagateRemoteDeleteEncryptedRootFold
void PropagateRemoteDeleteEncryptedRootFolder::start()
{
Q_ASSERT(_item->_isEncrypted);
Q_ASSERT(_item->isEncrypted());
const bool listFilesResult = _propagator->_journal->listFilesInPath(_item->_file.toUtf8(), [this](const OCC::SyncJournalFileRecord &record) {
_nestedItems[record._e2eMangledName] = record;
@ -81,7 +81,9 @@ void PropagateRemoteDeleteEncryptedRootFolder::slotFolderEncryptedMetadataReceiv
return;
}
FolderMetadata metadata(_propagator->account(), json.toJson(QJsonDocument::Compact), statusCode);
FolderMetadata metadata(_propagator->account(),
_item->_isEncrypted == SyncFileItem::EncryptionStatus::EncryptedMigratedV1_2 ? FolderMetadata::RequiredMetadataVersion::Version1_2 : FolderMetadata::RequiredMetadataVersion::Version1,
json.toJson(QJsonDocument::Compact), statusCode);
if (!metadata.isMetadataSetup()) {
taskFailed();

View File

@ -146,7 +146,7 @@ void PropagateRemoteMkdir::finalizeMkColJob(QNetworkReply::NetworkError err, con
_item->_isShared = _item->_remotePerm.hasPermission(RemotePermissions::IsShared) || _item->_sharedByMe;
_item->_lastShareStateFetchedTimestamp = QDateTime::currentMSecsSinceEpoch();
if (!_uploadEncryptedHelper && !_item->_isEncrypted) {
if (!_uploadEncryptedHelper && !_item->isEncrypted()) {
success();
} else {
// We still need to mark that folder encrypted in case we were uploading it as encrypted one
@ -243,7 +243,7 @@ void PropagateRemoteMkdir::slotEncryptFolderFinished()
{
qCDebug(lcPropagateRemoteMkdir) << "Success making the new folder encrypted";
propagator()->_activeJobList.removeOne(this);
_item->_isEncrypted = true;
_item->_isEncrypted = SyncFileItem::EncryptionStatus::EncryptedMigratedV1_2;
success();
}

View File

@ -219,7 +219,7 @@ void PropagateUploadFileCommon::start()
if (!account->capabilities().clientSideEncryptionAvailable() ||
!parentRec.isValid() ||
!parentRec._isE2eEncrypted) {
!parentRec.isE2eEncrypted()) {
setupUnencryptedFile();
return;
}

View File

@ -121,7 +121,9 @@ void PropagateUploadEncrypted::slotFolderEncryptedMetadataReceived(const QJsonDo
qCDebug(lcPropagateUploadEncrypted) << "Metadata Received, Preparing it for the new file." << json.toVariant();
// Encrypt File!
_metadata.reset(new FolderMetadata(_propagator->account(), json.toJson(QJsonDocument::Compact), statusCode));
_metadata.reset(new FolderMetadata(_propagator->account(),
_item->_isEncrypted == SyncFileItem::EncryptionStatus::EncryptedMigratedV1_2 ? FolderMetadata::RequiredMetadataVersion::Version1_2 : FolderMetadata::RequiredMetadataVersion::Version1,
json.toJson(QJsonDocument::Compact), statusCode));
if (!_metadata->isMetadataSetup()) {
if (_isFolderLocked) {
@ -154,8 +156,6 @@ void PropagateUploadEncrypted::slotFolderEncryptedMetadataReceived(const QJsonDo
if (!found) {
encryptedFile.encryptionKey = EncryptionHelper::generateRandom(16);
encryptedFile.encryptedFilename = EncryptionHelper::generateRandomFilename();
encryptedFile.fileVersion = 1;
encryptedFile.metadataKey = 1;
encryptedFile.originalFilename = fileName;
QMimeDatabase mdb;
@ -171,7 +171,7 @@ void PropagateUploadEncrypted::slotFolderEncryptedMetadataReceived(const QJsonDo
encryptedFile.initializationVector = EncryptionHelper::generateRandom(16);
_item->_encryptedFileName = _remoteParentPath + QLatin1Char('/') + encryptedFile.encryptedFilename;
_item->_isEncrypted = true;
_item->_isEncrypted = SyncFileItem::EncryptionStatus::EncryptedMigratedV1_2;
qCDebug(lcPropagateUploadEncrypted) << "Creating the encrypted file.";

View File

@ -49,7 +49,7 @@ SyncJournalFileRecord SyncFileItem::toSyncJournalFileRecordWithInode(const QStri
rec._serverHasIgnoredFiles = _serverHasIgnoredFiles;
rec._checksumHeader = _checksumHeader;
rec._e2eMangledName = _encryptedFileName.toUtf8();
rec._isE2eEncrypted = _isEncrypted;
rec._isE2eEncrypted = isEncrypted() ? SyncJournalFileRecord::EncryptionStatus::EncryptedMigratedV1_2 : SyncJournalFileRecord::EncryptionStatus::NotEncrypted;
rec._lockstate._locked = _locked == LockStatus::LockedItem;
rec._lockstate._lockOwnerDisplayName = _lockOwnerDisplayName;
rec._lockstate._lockOwnerId = _lockOwnerId;
@ -86,7 +86,7 @@ SyncFileItemPtr SyncFileItem::fromSyncJournalFileRecord(const SyncJournalFileRec
item->_serverHasIgnoredFiles = rec._serverHasIgnoredFiles;
item->_checksumHeader = rec._checksumHeader;
item->_encryptedFileName = rec.e2eMangledName();
item->_isEncrypted = rec._isE2eEncrypted;
item->_isEncrypted = static_cast<SyncFileItem::EncryptionStatus>(rec._isE2eEncrypted);
item->_locked = rec._lockstate._locked ? LockStatus::LockedItem : LockStatus::UnlockedItem;
item->_lockOwnerDisplayName = rec._lockstate._lockOwnerDisplayName;
item->_lockOwnerId = rec._lockstate._lockOwnerId;
@ -123,7 +123,7 @@ SyncFileItemPtr SyncFileItem::fromProperties(const QString &filePath, const QMap
item->_isShared = item->_remotePerm.hasPermission(RemotePermissions::IsShared);
item->_lastShareStateFetchedTimestamp = QDateTime::currentMSecsSinceEpoch();
item->_isEncrypted = properties.value(QStringLiteral("is-encrypted")) == QStringLiteral("1");
item->_isEncrypted = (properties.value(QStringLiteral("is-encrypted")) == QStringLiteral("1") ? SyncFileItem::EncryptionStatus::EncryptedMigratedV1_2 : SyncFileItem::EncryptionStatus::NotEncrypted);
item->_locked =
properties.value(QStringLiteral("lock")) == QStringLiteral("1") ? SyncFileItem::LockStatus::LockedItem : SyncFileItem::LockStatus::UnlockedItem;
item->_lockOwnerDisplayName = properties.value(QStringLiteral("lock-owner-displayname"));

View File

@ -46,6 +46,13 @@ public:
};
Q_ENUM(Direction)
enum class EncryptionStatus : int {
NotEncrypted = 0,
Encrypted = 1,
EncryptedMigratedV1_2 = 2,
};
Q_ENUM(EncryptionStatus)
// Note: the order of these statuses is used for ordering in the SortedActivityListModel
enum Status { // stored in 4 bits
NoStatus,
@ -138,7 +145,6 @@ public:
, _status(NoStatus)
, _isRestoration(false)
, _isSelectiveSync(false)
, _isEncrypted(false)
{
}
@ -228,6 +234,8 @@ public:
&& !(_instruction == CSYNC_INSTRUCTION_CONFLICT && _status == SyncFileItem::Success);
}
[[nodiscard]] bool isEncrypted() const { return _isEncrypted != SyncFileItem::EncryptionStatus::NotEncrypted; }
// Variables useful for everybody
/** The syncfolder-relative filesystem path that the operation is about
@ -273,7 +281,7 @@ public:
Status _status BITFIELD(4);
bool _isRestoration BITFIELD(1); // The original operation was forbidden, and this is a restoration
bool _isSelectiveSync BITFIELD(1); // The file is removed or ignored because it is in the selective sync list
bool _isEncrypted BITFIELD(1); // The file is E2EE or the content of the directory should be E2EE
EncryptionStatus _isEncrypted = EncryptionStatus::NotEncrypted; // The file is E2EE or the content of the directory should be E2EE
quint16 _httpErrorCode = 0;
RemotePermissions _remotePerm;
QString _errorString; // Contains a string only in case of error
@ -319,6 +327,8 @@ public:
bool _sharedByMe = false;
bool _isFileDropDetected = false;
bool _isEncryptedMetadataNeedUpdate = false;
};
inline bool operator<(const SyncFileItemPtr &item1, const SyncFileItemPtr &item2)

View File

@ -122,8 +122,10 @@ void UpdateFileDropMetadataJob::slotFolderEncryptedMetadataReceived(const QJsonD
qCDebug(lcUpdateFileDropMetadataJob) << "Metadata Received, Preparing it for the new file." << json.toVariant();
// Encrypt File!
_metadata.reset(new FolderMetadata(propagator()->account(), json.toJson(QJsonDocument::Compact), statusCode));
if (!_metadata->moveFromFileDropToFiles()) {
_metadata.reset(new FolderMetadata(propagator()->account(),
FolderMetadata::RequiredMetadataVersion::Version1,
json.toJson(QJsonDocument::Compact), statusCode));
if (!_metadata->moveFromFileDropToFiles() && !_metadata->encryptedMetadataNeedUpdate()) {
unlockFolder();
return;
}

View File

@ -123,14 +123,14 @@ private slots:
// the server, let's just manually set the encryption bool in the folder journal
SyncJournalFileRecord rec;
QVERIFY(folder->journalDb()->getFileRecord(QStringLiteral("encrypted"), &rec));
rec._isE2eEncrypted = true;
rec._isE2eEncrypted = SyncJournalFileRecord::EncryptionStatus::EncryptedMigratedV1_2;
rec._path = QStringLiteral("encrypted").toUtf8();
rec._type = CSyncEnums::ItemTypeDirectory;
QVERIFY(folder->journalDb()->setFileRecord(rec));
SyncJournalFileRecord updatedRec;
QVERIFY(folder->journalDb()->getFileRecord(QStringLiteral("encrypted"), &updatedRec));
QVERIFY(updatedRec._isE2eEncrypted);
QVERIFY(updatedRec.isE2eEncrypted());
QVERIFY(updatedRec.isDirectory());
FolderMan::instance()->removeE2eFiles(account);

View File

@ -69,8 +69,8 @@ private slots:
QFile fakeJsonReplyFile(QStringLiteral("fakefiledrope2eefoldermetadata.json"));
if (fakeJsonReplyFile.open(QFile::ReadOnly)) {
const auto jsonDoc = QJsonDocument::fromJson(fakeJsonReplyFile.readAll());
_parsedMetadataWithFileDrop.reset(new FolderMetadata(_fakeFolder.syncEngine().account(), jsonDoc.toJson()));
_parsedMetadataAfterProcessingFileDrop.reset(new FolderMetadata(_fakeFolder.syncEngine().account(), jsonDoc.toJson()));
_parsedMetadataWithFileDrop.reset(new FolderMetadata(_fakeFolder.syncEngine().account(), FolderMetadata::RequiredMetadataVersion::Version1_2, jsonDoc.toJson()));
_parsedMetadataAfterProcessingFileDrop.reset(new FolderMetadata(_fakeFolder.syncEngine().account(), FolderMetadata::RequiredMetadataVersion::Version1_2, jsonDoc.toJson()));
[[maybe_unused]] const auto result = _parsedMetadataAfterProcessingFileDrop->moveFromFileDropToFiles();
reply = new FakePayloadReply(op, req, jsonDoc.toJson(), nullptr);
++_getMetadataCallsCount;