prevent downgrading e2ee metadata format after initial migration

Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
This commit is contained in:
Matthieu Gallien 2023-03-29 12:42:11 +02:00
parent b121a127c1
commit 8659df2266
No known key found for this signature in database
GPG key ID: 7D0F74F05C22F553
19 changed files with 115 additions and 32 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);
@ -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,7 +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; }
[[nodiscard]] bool isE2eEncrypted() const { return _isE2eEncrypted != SyncJournalFileRecord::EncryptionStatus::NotEncrypted; }
QByteArray _path;
quint64 _inode = 0;
@ -80,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

@ -1498,7 +1498,19 @@ void ClientSideEncryption::fetchAndValidatePublicKeyFromServer(const AccountPtr
job->start();
}
FolderMetadata::FolderMetadata(AccountPtr account, const QByteArray& metadata, int statusCode) : _account(account)
FolderMetadata::FolderMetadata(AccountPtr account)
: _account(account)
{
qCInfo(lcCseMetadata()) << "Setupping Empty Metadata";
setupEmptyMetadata();
}
FolderMetadata::FolderMetadata(AccountPtr account,
RequiredMetadataVersion requiredMetadataVersion,
const QByteArray& metadata,
int statusCode)
: _account(account)
, _requiredMetadataVersion(requiredMetadataVersion)
{
if (metadata.isEmpty() || statusCode == 404) {
qCInfo(lcCseMetadata()) << "Setupping Empty Metadata";
@ -1541,7 +1553,7 @@ void FolderMetadata::setupExistingMetadata(const QByteArray& metadata)
}
auto migratedMetadata = false;
if (_metadataKey.isEmpty()) {
if (_metadataKey.isEmpty() && _requiredMetadataVersion != RequiredMetadataVersion::Version1_2) {
qCDebug(lcCse()) << "Migrating from v1.1 to v1.2";
migratedMetadata = true;
@ -1618,6 +1630,8 @@ void FolderMetadata::setupExistingMetadata(const QByteArray& metadata)
file.mimetype = QByteArrayLiteral("httpd/unix-directory");
}
qCDebug(lcCseMetadata) << "encrypted file" << decryptedFileObj["filename"].toString() << decryptedFileObj["key"].toString() << it.key();
_files.push_back(file);
}
@ -1631,6 +1645,10 @@ void FolderMetadata::setupExistingMetadata(const QByteArray& metadata)
// decryption finished, create new metadata key to be used for encryption
_metadataKey = EncryptionHelper::generateRandom(metadataKeySize);
_isMetadataSetup = true;
if (migratedMetadata) {
_encryptedMetadataNeedUpdate = true;
}
}
// RSA/ECB/OAEPWithSHA-256AndMGF1Padding using private / public key.
@ -1792,6 +1810,11 @@ bool FolderMetadata::isFileDropPresent() const
return _fileDrop.size() > 0;
}
bool FolderMetadata::encryptedMetadataNeedUpdate() const
{
return _encryptedMetadataNeedUpdate;
}
bool FolderMetadata::moveFromFileDropToFiles()
{
if (_fileDrop.isEmpty()) {

View file

@ -186,7 +186,18 @@ struct EncryptedFile {
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);
@ -196,6 +207,8 @@ public:
[[nodiscard]] bool isFileDropPresent() const;
[[nodiscard]] bool encryptedMetadataNeedUpdate() const;
[[nodiscard]] bool moveFromFileDropToFiles();
[[nodiscard]] QJsonObject fileDrop() const;
@ -221,9 +234,11 @@ private:
QVector<EncryptedFile> _files;
AccountPtr _account;
RequiredMetadataVersion _requiredMetadataVersion = RequiredMetadataVersion::Version1_2;
QVector<QPair<QString, QString>> _sharing;
QJsonObject _fileDrop;
bool _isMetadataSetup = false;
bool _encryptedMetadataNeedUpdate = false;
};
} // namespace OCC

View file

@ -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::EncryptedMigratedV1_2 : SyncFileItem::EncryptionStatus::NotEncrypted;
item->_encryptedFileName = [=] {
if (serverEntry.e2eMangledName.isEmpty()) {
return QString();
@ -1292,7 +1292,7 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo(
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 = SyncFileItem::EncryptionStatus::EncryptedMigratedV1_2;
}
postProcessLocalNew();
finalize();
@ -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) {
@ -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")) {
@ -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) {

View file

@ -145,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:
@ -162,7 +163,7 @@ private slots:
private:
[[nodiscard]] bool isE2eEncrypted() const { return _isE2eEncrypted; }
[[nodiscard]] bool isE2eEncrypted() const { return _isE2eEncrypted != SyncFileItem::EncryptionStatus::NotEncrypted; }
QVector<RemoteInfo> _results;
QString _subPath;
@ -178,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

@ -646,7 +646,7 @@ void OwncloudPropagator::startDirectoryPropagation(const SyncFileItemPtr &item,
const auto currentDirJob = directories.top().second;
currentDirJob->appendJob(directoryPropagationJob.get());
}
if (item->_isFileDropDetected) {
if (item->_isFileDropDetected || item->_isEncryptedMetadataNeedUpdate) {
directoryPropagationJob->appendJob(new UpdateFileDropMetadataJob(this, item->_file));
item->_instruction = CSYNC_INSTRUCTION_NONE;
_anotherSyncNeeded = true;

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

@ -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

@ -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

@ -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

@ -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) {
@ -169,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,7 +234,7 @@ public:
&& !(_instruction == CSYNC_INSTRUCTION_CONFLICT && _status == SyncFileItem::Success);
}
[[nodiscard]] bool isEncrypted() const { return _isEncrypted; }
[[nodiscard]] bool isEncrypted() const { return _isEncrypted != SyncFileItem::EncryptionStatus::NotEncrypted; }
// Variables useful for everybody
@ -275,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
@ -321,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,7 +123,7 @@ 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));

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;