diff --git a/CMakeLists.txt b/CMakeLists.txt index 0b6af4abfa..ca8b0774e0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -166,15 +166,16 @@ if("${CMAKE_SIZEOF_VOID_P}" EQUAL "4") set(IS_32BIT TRUE) endif() +set(CLANG_COMPILER_ID_REGEX "^(Apple)?[Cc]lang$") if("${CMAKE_C_COMPILER}" MATCHES "clang$" OR "${CMAKE_EXTRA_GENERATOR_C_SYSTEM_DEFINED_MACROS}" MATCHES "__clang__" - OR "${CMAKE_C_COMPILER_ID}" STREQUAL "Clang") + OR "${CMAKE_C_COMPILER_ID}" MATCHES ${CLANG_COMPILER_ID_REGEX}) set(CMAKE_COMPILER_IS_CLANG 1) endif() if("${CMAKE_CXX_COMPILER}" MATCHES "clang(\\+\\+)?$" OR "${CMAKE_EXTRA_GENERATOR_CXX_SYSTEM_DEFINED_MACROS}" MATCHES "__clang__" - OR "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") + OR "${CMAKE_CXX_COMPILER_ID}" MATCHES ${CLANG_COMPILER_ID_REGEX}) set(CMAKE_COMPILER_IS_CLANGXX 1) endif() @@ -253,6 +254,15 @@ if(CMAKE_COMPILER_IS_GNUCXX) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wcast-align") endif() +if(WITH_COVERAGE AND CMAKE_COMPILER_IS_CLANGXX) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fprofile-instr-generate -fcoverage-mapping") + # then: + # $ llvm-profdata merge -sparse default.profraw -o default.profdata + # $ llvm-cov show ./tests/${the_test_binary} \ + # -format=html -instr-profile=default.profdata -output-dir=./coverages \ + # `find src -iname '*.h' -or -iname '*.cpp'` +endif() + if(CMAKE_COMPILER_IS_GNUCC) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wcast-align") endif() @@ -276,7 +286,7 @@ if((CMAKE_COMPILER_IS_GNUCXX AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 4.9. add_gcc_compiler_cxxflags("-fsized-deallocation") endif() -if(APPLE) +if(APPLE AND CMAKE_COMPILER_IS_CLANGXX) add_gcc_compiler_cxxflags("-stdlib=libc++") endif() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0407008f42..7a1a547a80 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -79,6 +79,11 @@ set(keepassx_SOURCES format/Kdbx4Reader.cpp format/Kdbx4Writer.cpp format/KdbxXmlWriter.cpp + format/OpData01.cpp + format/OpVaultReader.cpp + format/OpVaultReaderAttachments.cpp + format/OpVaultReaderBandEntry.cpp + format/OpVaultReaderSections.cpp gui/AboutDialog.cpp gui/Application.cpp gui/CategoryListWidget.cpp @@ -103,6 +108,7 @@ set(keepassx_SOURCES gui/MainWindow.cpp gui/MessageBox.cpp gui/MessageWidget.cpp + gui/OpVaultOpenWidget.cpp gui/PasswordEdit.cpp gui/PasswordGeneratorWidget.cpp gui/ApplicationSettingsWidget.cpp diff --git a/src/autotype/AutoType.cpp b/src/autotype/AutoType.cpp index 0f772d8d3e..7366ebbec4 100644 --- a/src/autotype/AutoType.cpp +++ b/src/autotype/AutoType.cpp @@ -619,9 +619,8 @@ bool AutoType::windowMatches(const QString& windowTitle, const QString& windowPa if (windowPattern.startsWith("//") && windowPattern.endsWith("//") && windowPattern.size() >= 4) { QRegExp regExp(windowPattern.mid(2, windowPattern.size() - 4), Qt::CaseInsensitive, QRegExp::RegExp2); return (regExp.indexIn(windowTitle) != -1); - } else { - return WildcardMatcher(windowTitle).match(windowPattern); } + return WildcardMatcher(windowTitle).match(windowPattern); } /** diff --git a/src/autotype/AutoTypeFilterLineEdit.cpp b/src/autotype/AutoTypeFilterLineEdit.cpp index 09b902e7aa..d94292d26f 100644 --- a/src/autotype/AutoTypeFilterLineEdit.cpp +++ b/src/autotype/AutoTypeFilterLineEdit.cpp @@ -18,7 +18,7 @@ #include "AutoTypeFilterLineEdit.h" #include -void AutoTypeFilterLineEdit::keyPressEvent(QKeyEvent *event) +void AutoTypeFilterLineEdit::keyPressEvent(QKeyEvent* event) { if (event->key() == Qt::Key_Up) { emit keyUpPressed(); @@ -29,7 +29,7 @@ void AutoTypeFilterLineEdit::keyPressEvent(QKeyEvent *event) } } -void AutoTypeFilterLineEdit::keyReleaseEvent(QKeyEvent *event) +void AutoTypeFilterLineEdit::keyReleaseEvent(QKeyEvent* event) { if (event->key() == Qt::Key_Escape) { emit escapeReleased(); diff --git a/src/autotype/AutoTypeFilterLineEdit.h b/src/autotype/AutoTypeFilterLineEdit.h index 7e7169816d..6f8ca570d3 100644 --- a/src/autotype/AutoTypeFilterLineEdit.h +++ b/src/autotype/AutoTypeFilterLineEdit.h @@ -25,10 +25,14 @@ class AutoTypeFilterLineEdit : public QLineEdit Q_OBJECT public: - AutoTypeFilterLineEdit(QWidget* widget) : QLineEdit(widget) {} + AutoTypeFilterLineEdit(QWidget* widget) + : QLineEdit(widget) + { + } + protected: - virtual void keyPressEvent(QKeyEvent *event); - virtual void keyReleaseEvent(QKeyEvent *event); + virtual void keyPressEvent(QKeyEvent* event); + virtual void keyReleaseEvent(QKeyEvent* event); signals: void keyUpPressed(); void keyDownPressed(); diff --git a/src/autotype/AutoTypeSelectDialog.cpp b/src/autotype/AutoTypeSelectDialog.cpp index 6fa3af7e14..69f1817658 100644 --- a/src/autotype/AutoTypeSelectDialog.cpp +++ b/src/autotype/AutoTypeSelectDialog.cpp @@ -27,9 +27,9 @@ #include #include #include -#include #include #include +#include #include "autotype/AutoTypeSelectView.h" #include "core/AutoTypeMatch.h" @@ -77,7 +77,7 @@ AutoTypeSelectDialog::AutoTypeSelectDialog(QWidget* parent) connect(m_view, SIGNAL(rejected()), SLOT(reject())); // clang-format on - QSortFilterProxyModel *proxy = qobject_cast(m_view->model()); + QSortFilterProxyModel* proxy = qobject_cast(m_view->model()); if (proxy) { proxy->setFilterKeyColumn(-1); proxy->setFilterCaseSensitivity(Qt::CaseInsensitive); @@ -146,10 +146,9 @@ void AutoTypeSelectDialog::matchRemoved() } } - void AutoTypeSelectDialog::filterList(QString filterString) { - QSortFilterProxyModel *proxy = qobject_cast(m_view->model()); + QSortFilterProxyModel* proxy = qobject_cast(m_view->model()); if (proxy) { proxy->setFilterWildcard(filterString); if (!m_view->currentIndex().isValid()) { diff --git a/src/autotype/mac/CMakeLists.txt b/src/autotype/mac/CMakeLists.txt index f1c5387f34..7427450a10 100644 --- a/src/autotype/mac/CMakeLists.txt +++ b/src/autotype/mac/CMakeLists.txt @@ -1,10 +1,6 @@ set(autotype_mac_SOURCES AutoTypeMac.cpp) -set(autotype_mac_mm_SOURCES - ${CMAKE_SOURCE_DIR}/src/gui/macutils/AppKitImpl.mm - ${CMAKE_SOURCE_DIR}/src/gui/macutils/MacUtils.cpp) - -add_library(keepassx-autotype-cocoa MODULE ${autotype_mac_SOURCES} ${autotype_mac_mm_SOURCES}) +add_library(keepassx-autotype-cocoa MODULE ${autotype_mac_SOURCES}) set_target_properties(keepassx-autotype-cocoa PROPERTIES LINK_FLAGS "-framework Foundation -framework AppKit -framework Carbon") target_link_libraries(keepassx-autotype-cocoa ${PROGNAME} Qt5::Core Qt5::Widgets) diff --git a/src/browser/BrowserAction.cpp b/src/browser/BrowserAction.cpp index 1a4cbf5ece..ea3d50f884 100644 --- a/src/browser/BrowserAction.cpp +++ b/src/browser/BrowserAction.cpp @@ -340,7 +340,7 @@ QJsonObject BrowserAction::handleSetLogin(const QJsonObject& json, const QString QJsonObject message = buildMessage(newNonce); message["count"] = QJsonValue::Null; message["entries"] = QJsonValue::Null; - message["error"] = ""; + message["error"] = QString(""); message["hash"] = hash; return buildResponse(action, message, newNonce); diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp index 112a7cda92..d9d1b7507a 100644 --- a/src/browser/BrowserService.cpp +++ b/src/browser/BrowserService.cpp @@ -417,7 +417,7 @@ QJsonArray BrowserService::findMatchingEntries(const QString& id, // Fill the list for (Entry* entry : pwEntries) { - result << prepareEntry(entry); + result.append(prepareEntry(entry)); } return result; @@ -825,7 +825,7 @@ QJsonObject BrowserService::prepareEntry(const Entry* entry) if (key.startsWith(QLatin1String("KPH: "))) { QJsonObject sField; sField[key] = entry->resolveMultiplePlaceholders(attr->value(key)); - stringFields << sField; + stringFields.append(sField); } } res["stringFields"] = stringFields; diff --git a/src/browser/BrowserSettings.cpp b/src/browser/BrowserSettings.cpp index dd74dc1cb6..e1fc3a2da9 100644 --- a/src/browser/BrowserSettings.cpp +++ b/src/browser/BrowserSettings.cpp @@ -240,13 +240,13 @@ void BrowserSettings::setVivaldiSupport(bool enabled) bool BrowserSettings::braveSupport() { - return m_hostInstaller.checkIfInstalled(HostInstaller::SupportedBrowsers::BRAVE); + return m_hostInstaller.checkIfInstalled(HostInstaller::SupportedBrowsers::BRAVE); } void BrowserSettings::setBraveSupport(bool enabled) { - m_hostInstaller.installBrowser( - HostInstaller::SupportedBrowsers::BRAVE, enabled, supportBrowserProxy(), customProxyLocation()); + m_hostInstaller.installBrowser( + HostInstaller::SupportedBrowsers::BRAVE, enabled, supportBrowserProxy(), customProxyLocation()); } bool BrowserSettings::torBrowserSupport() diff --git a/src/browser/HostInstaller.cpp b/src/browser/HostInstaller.cpp index 20c5545661..65dd639322 100644 --- a/src/browser/HostInstaller.cpp +++ b/src/browser/HostInstaller.cpp @@ -171,7 +171,7 @@ QString HostInstaller::getTargetPath(SupportedBrowsers browser) const case SupportedBrowsers::TOR_BROWSER: return TARGET_DIR_TOR_BROWSER; case SupportedBrowsers::BRAVE: - return TARGET_DIR_BRAVE; + return TARGET_DIR_BRAVE; default: return QString(); } @@ -194,11 +194,11 @@ QString HostInstaller::getBrowserName(SupportedBrowsers browser) const case SupportedBrowsers::FIREFOX: return "firefox"; case SupportedBrowsers::VIVALDI: - return "vivaldi"; + return "vivaldi"; case SupportedBrowsers::TOR_BROWSER: return "tor-browser"; case SupportedBrowsers::BRAVE: - return "brave"; + return "brave"; default: return QString(); } @@ -299,9 +299,9 @@ QJsonObject HostInstaller::constructFile(SupportedBrowsers browser, const bool& QJsonObject script; script["name"] = HOST_NAME; - script["description"] = "KeePassXC integration with native messaging support"; + script["description"] = QString("KeePassXC integration with native messaging support"); script["path"] = path; - script["type"] = "stdio"; + script["type"] = QString("stdio"); QJsonArray arr; if (browser == SupportedBrowsers::FIREFOX || browser == SupportedBrowsers::TOR_BROWSER) { diff --git a/src/browser/NativeMessagingBase.cpp b/src/browser/NativeMessagingBase.cpp index a6b8d97c0e..9be399644d 100644 --- a/src/browser/NativeMessagingBase.cpp +++ b/src/browser/NativeMessagingBase.cpp @@ -102,7 +102,7 @@ void NativeMessagingBase::readNativeMessages() { #ifdef Q_OS_WIN quint32 length = 0; - while (m_running.load() && !std::cin.eof()) { + while (m_running.load() != 0 && !std::cin.eof()) { length = 0; std::cin.read(reinterpret_cast(&length), 4); readStdIn(length); diff --git a/src/browser/NativeMessagingBase.h b/src/browser/NativeMessagingBase.h index 7a099a4ace..b68208c681 100644 --- a/src/browser/NativeMessagingBase.h +++ b/src/browser/NativeMessagingBase.h @@ -19,7 +19,7 @@ #ifndef NATIVEMESSAGINGBASE_H #define NATIVEMESSAGINGBASE_H -#include +#include #include #include #include @@ -60,7 +60,7 @@ protected slots: QString getLocalServerPath() const; protected: - QAtomicInteger m_running; + QAtomicInt m_running; QSharedPointer m_notifier; QFuture m_future; }; diff --git a/src/browser/NativeMessagingHost.cpp b/src/browser/NativeMessagingHost.cpp index 5d2383b2b4..a6c3212156 100644 --- a/src/browser/NativeMessagingHost.cpp +++ b/src/browser/NativeMessagingHost.cpp @@ -35,9 +35,9 @@ NativeMessagingHost::NativeMessagingHost(DatabaseTabWidget* parent, const bool e { m_localServer.reset(new QLocalServer(this)); m_localServer->setSocketOptions(QLocalServer::UserAccessOption); - m_running.store(false); + m_running.store(0); - if (browserSettings()->isEnabled() && !m_running) { + if (browserSettings()->isEnabled() && m_running.load() == 0) { run(); } @@ -59,7 +59,7 @@ int NativeMessagingHost::init() void NativeMessagingHost::run() { QMutexLocker locker(&m_mutex); - if (!m_running.load() && init() == -1) { + if (m_running.load() == 0 && init() == -1) { return; } @@ -69,7 +69,7 @@ void NativeMessagingHost::run() browserSettings()->useCustomProxy() ? browserSettings()->customProxyLocation() : ""); } - m_running.store(true); + m_running.store(1); #ifdef Q_OS_WIN m_future = QtConcurrent::run(this, static_cast(&NativeMessagingHost::readNativeMessages)); @@ -100,7 +100,7 @@ void NativeMessagingHost::stop() databaseLocked(); QMutexLocker locker(&m_mutex); m_socketList.clear(); - m_running.testAndSetOrdered(true, false); + m_running.testAndSetOrdered(1, 0); m_future.waitForFinished(); m_localServer->close(); } @@ -210,13 +210,13 @@ void NativeMessagingHost::disconnectSocket() void NativeMessagingHost::databaseLocked() { QJsonObject response; - response["action"] = "database-locked"; + response["action"] = QString("database-locked"); sendReplyToAllClients(response); } void NativeMessagingHost::databaseUnlocked() { QJsonObject response; - response["action"] = "database-unlocked"; + response["action"] = QString("database-unlocked"); sendReplyToAllClients(response); } diff --git a/src/cli/Command.cpp b/src/cli/Command.cpp index e64dd4aaa0..48e1f41162 100644 --- a/src/cli/Command.cpp +++ b/src/cli/Command.cpp @@ -47,8 +47,7 @@ const QCommandLineOption Command::KeyFileOption = QCommandLineOption(QStringList QObject::tr("path")); const QCommandLineOption Command::NoPasswordOption = - QCommandLineOption(QStringList() << "no-password", - QObject::tr("Deactivate password key for the database.")); + QCommandLineOption(QStringList() << "no-password", QObject::tr("Deactivate password key for the database.")); QMap commands; diff --git a/src/cli/Extract.cpp b/src/cli/Extract.cpp index 054a391bd9..90713e1ecd 100644 --- a/src/cli/Extract.cpp +++ b/src/cli/Extract.cpp @@ -56,7 +56,7 @@ int Extract::execute(const QStringList& arguments) errorTextStream << parser.helpText().replace("[options]", "extract [options]"); return EXIT_FAILURE; } - + auto compositeKey = QSharedPointer::create(); auto db = Utils::unlockDatabase(args.at(0), diff --git a/src/cli/keepassxc-cli.cpp b/src/cli/keepassxc-cli.cpp index 2c6ce46602..1f76812b07 100644 --- a/src/cli/keepassxc-cli.cpp +++ b/src/cli/keepassxc-cli.cpp @@ -61,8 +61,7 @@ int main(int argc, char** argv) parser.addPositionalArgument("command", QObject::tr("Name of the command to execute.")); - QCommandLineOption debugInfoOption(QStringList() << "debug-info", - QObject::tr("Displays debugging information.")); + QCommandLineOption debugInfoOption(QStringList() << "debug-info", QObject::tr("Displays debugging information.")); parser.addOption(debugInfoOption); parser.addHelpOption(); parser.addVersionOption(); diff --git a/src/core/Alloc.cpp b/src/core/Alloc.cpp index a33b561962..89d044ec39 100644 --- a/src/core/Alloc.cpp +++ b/src/core/Alloc.cpp @@ -78,7 +78,7 @@ void operator delete[](void* ptr) noexcept * Custom insecure delete operator that does not zero out memory before * freeing a buffer. Can be used for better performance. */ -void operator delete(void* ptr, bool) noexcept +void operator delete(void* ptr, bool)noexcept { std::free(ptr); } diff --git a/src/core/Bootstrap.cpp b/src/core/Bootstrap.cpp index 2d1a3e0878..204942f4de 100644 --- a/src/core/Bootstrap.cpp +++ b/src/core/Bootstrap.cpp @@ -257,7 +257,7 @@ namespace Bootstrap nullptr, // do not change owner or group pACL, // DACL specified nullptr // do not change SACL - ); + ); Cleanup: diff --git a/src/core/Merger.cpp b/src/core/Merger.cpp index dcbed250f8..c70cd4b043 100644 --- a/src/core/Merger.cpp +++ b/src/core/Merger.cpp @@ -625,9 +625,9 @@ Merger::ChangeList Merger::mergeMetadata(const MergeContext& context) // Merge Custom Data if source is newer const auto targetCustomDataModificationTime = sourceMetadata->customData()->getLastModified(); const auto sourceCustomDataModificationTime = targetMetadata->customData()->getLastModified(); - if (!targetMetadata->customData()->contains(CustomData::LastModified) || - (targetCustomDataModificationTime.isValid() && sourceCustomDataModificationTime.isValid() && - targetCustomDataModificationTime > sourceCustomDataModificationTime)) { + if (!targetMetadata->customData()->contains(CustomData::LastModified) + || (targetCustomDataModificationTime.isValid() && sourceCustomDataModificationTime.isValid() + && targetCustomDataModificationTime > sourceCustomDataModificationTime)) { const auto sourceCustomDataKeys = sourceMetadata->customData()->keys(); const auto targetCustomDataKeys = targetMetadata->customData()->keys(); diff --git a/src/core/Tools.cpp b/src/core/Tools.cpp index 46cde95bce..beacbaaad7 100644 --- a/src/core/Tools.cpp +++ b/src/core/Tools.cpp @@ -23,6 +23,7 @@ #include "core/Config.h" #include "core/Translator.h" +#include "git-info.h" #include #include #include @@ -33,7 +34,6 @@ #include #include #include -#include "git-info.h" #ifdef Q_OS_WIN #include // for Sleep() @@ -75,7 +75,6 @@ namespace Tools #endif debugInfo.append("\n"); - #if QT_VERSION >= QT_VERSION_CHECK(5, 4, 0) debugInfo.append(QObject::tr("Operating system: %1\nCPU architecture: %2\nKernel: %3 %4") .arg(QSysInfo::prettyProductName(), diff --git a/src/core/Translator.h b/src/core/Translator.h index cfc49d7105..23a18422cd 100644 --- a/src/core/Translator.h +++ b/src/core/Translator.h @@ -18,9 +18,9 @@ #ifndef KEEPASSX_TRANSLATOR_H #define KEEPASSX_TRANSLATOR_H +#include #include #include -#include class Translator { diff --git a/src/fdosecrets/objects/Collection.cpp b/src/fdosecrets/objects/Collection.cpp index 119ec4ad26..6926a6c20e 100644 --- a/src/fdosecrets/objects/Collection.cpp +++ b/src/fdosecrets/objects/Collection.cpp @@ -157,8 +157,8 @@ namespace FdoSecrets if (ret.isError()) { return ret; } - return static_cast( - m_backend->database()->rootGroup()->timeInfo().creationTime().toMSecsSinceEpoch() / 1000); + return static_cast(m_backend->database()->rootGroup()->timeInfo().creationTime().toMSecsSinceEpoch() + / 1000); } DBusReturn Collection::modified() const diff --git a/src/fdosecrets/objects/Item.cpp b/src/fdosecrets/objects/Item.cpp index fa37564a26..18624bbdb2 100644 --- a/src/fdosecrets/objects/Item.cpp +++ b/src/fdosecrets/objects/Item.cpp @@ -354,12 +354,12 @@ namespace FdoSecrets return pathComponents.join('/'); } - QString Item::encodeAttributeKey(const QString &key) + QString Item::encodeAttributeKey(const QString& key) { return QUrl::toPercentEncoding(key, "", "_:").replace('%', '_'); } - QString Item::decodeAttributeKey(const QString &key) + QString Item::decodeAttributeKey(const QString& key) { return QString::fromUtf8(QByteArray::fromPercentEncoding(key.toLatin1(), '_')); } diff --git a/src/fdosecrets/objects/Service.cpp b/src/fdosecrets/objects/Service.cpp index c15d4052c9..cf9dfdbf75 100644 --- a/src/fdosecrets/objects/Service.cpp +++ b/src/fdosecrets/objects/Service.cpp @@ -66,8 +66,10 @@ namespace FdoSecrets // Connect to service unregistered signal m_serviceWatcher.reset(new QDBusServiceWatcher()); - connect( - m_serviceWatcher.data(), &QDBusServiceWatcher::serviceUnregistered, this, &Service::dbusServiceUnregistered); + connect(m_serviceWatcher.data(), + &QDBusServiceWatcher::serviceUnregistered, + this, + &Service::dbusServiceUnregistered); m_serviceWatcher->setConnection(QDBusConnection::sessionBus()); diff --git a/src/format/OpData01.cpp b/src/format/OpData01.cpp new file mode 100644 index 0000000000..d373e58142 --- /dev/null +++ b/src/format/OpData01.cpp @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2019 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "OpData01.h" + +#include "crypto/CryptoHash.h" +#include "crypto/SymmetricCipher.h" + +#include +#include + +OpData01::OpData01(QObject* parent) + : QObject(parent) +{ +} + +OpData01::~OpData01() +{ +} + +bool OpData01::decodeBase64(QString const& b64String, const QByteArray& key, const QByteArray& hmacKey) +{ + const QByteArray b64Bytes = QByteArray::fromBase64(b64String.toUtf8()); + return decode(b64Bytes, key, hmacKey); +} + +bool OpData01::decode(const QByteArray& data, const QByteArray& key, const QByteArray& hmacKey) +{ + /*! + * The first 8 bytes of the data are the string “opdata01”. + */ + const QByteArray header("opdata01"); + if (!data.startsWith(header)) { + m_errorStr = tr("Invalid OpData01, does not contain header"); + return false; + } + + QDataStream in(data); + in.setByteOrder(QDataStream::LittleEndian); + in.skipRawData(header.size()); + + /*! + * The next 8 bytes contain the length in bytes of the plaintext as a little endian unsigned 64 bit integer. + */ + qlonglong len; + in >> len; + + /*! + * The next 16 bytes are the randomly chosen initialization vector. + */ + QByteArray iv(16, '\0'); + int read = in.readRawData(iv.data(), 16); + if (read != 16) { + m_errorStr = tr("Unable to read all IV bytes, wanted 16 but got %1").arg(iv.size()); + return false; + } + + SymmetricCipher cipher(SymmetricCipher::Aes256, SymmetricCipher::Cbc, SymmetricCipher::Decrypt); + if (!cipher.init(key, iv)) { + m_errorStr = tr("Unable to init cipher for opdata01: %1").arg(cipher.errorString()); + return false; + } + + /*! + * The plaintext is padded using the following scheme. + * + * If the size of the plaintext is an even multiple of the block size then 1 block of random data is prepended + * to the plaintext. Otherwise, between 1 and 15 (inclusive) bytes of random data are prepended to the plaintext + * to achieve an even multiple of blocks. + */ + const int blockSize = cipher.blockSize(); + int randomBytes = blockSize - (len % blockSize); + if (randomBytes == 0) { + // add random block + randomBytes = blockSize; + } + qlonglong clear_len = len + randomBytes; + QByteArray qbaCT(clear_len, '\0'); + in.readRawData(qbaCT.data(), clear_len); + + /*! + * The HMAC-SHA256 is computed over the entirety of the opdata including header, length, IV and ciphertext + * using a 256-bit MAC key. The 256-bit MAC is not truncated. It is appended to the ciphertext. + */ + const int hmacLen = 256 / 8; + QByteArray hmacSig(hmacLen, '\0'); // 256 / 8, '\0'); + in.readRawData(hmacSig.data(), hmacLen); + if (hmacSig.size() != hmacLen) { + m_errorStr = tr("Unable to read all HMAC signature bytes"); + return false; + } + + const QByteArray hmacData = data.mid(0, data.size() - hmacSig.size()); + const QByteArray actualHmac = CryptoHash::hmac(hmacData, hmacKey, CryptoHash::Algorithm::Sha256); + if (actualHmac != hmacSig) { + m_errorStr = tr("Malformed OpData01 due to a failed HMAC"); + return false; + } + + if (!cipher.processInPlace(qbaCT)) { + m_errorStr = tr("Unable to process clearText in place"); + return false; + } + + // Remove random bytes + const QByteArray& clearText = qbaCT.mid(randomBytes); + if (clearText.size() != len) { + m_errorStr = tr("Expected %1 bytes of clear-text, found %2").arg(len, clearText.size()); + return false; + } + m_clearText = clearText; + return true; +} + +QByteArray OpData01::getClearText() +{ + return m_clearText; +} + +QString OpData01::errorString() +{ + return m_errorStr; +} diff --git a/src/format/OpData01.h b/src/format/OpData01.h new file mode 100644 index 0000000000..13eadec257 --- /dev/null +++ b/src/format/OpData01.h @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2019 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_OPDATA01_H +#define KEEPASSXC_OPDATA01_H + +#include + +/*! + * Packages and transports the AgileBits data structure called \c OpData01 + * used to encypt and provide HMAC for encrypted data. + * \sa https://support.1password.com/opvault-design/#opdata01 + */ +class OpData01 : public QObject +{ +public: + explicit OpData01(QObject* parent = nullptr); + ~OpData01() override; + + /*! + * The convenience equivalent of decode01(OpData01,const QByteArray,const QByteArray,const QByteArray) that simply + * decodes the provided base64 string into its underlying \c QByteArray. + */ + bool decodeBase64(QString const& b64String, const QByteArray& key, const QByteArray& hmacKey); + + /*! + * Populates the given \code OpData01 structure by decoding the provided blob of data, + * using the given key and then verifies using the given HMAC key. + * \returns true if things went well and \code m_clearText is usable, false and \code m_errorStr will contain + * details. + */ + bool decode(const QByteArray& data, const QByteArray& key, const QByteArray& hmacKey); + + QByteArray getClearText(); + + QString errorString(); + +private: + QByteArray m_clearText; + QString m_errorStr; +}; + +#endif // KEEPASSXC_OPDATA01_H diff --git a/src/format/OpVaultReader.cpp b/src/format/OpVaultReader.cpp new file mode 100644 index 0000000000..49d62b624e --- /dev/null +++ b/src/format/OpVaultReader.cpp @@ -0,0 +1,478 @@ +/* + * Copyright (C) 2019 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "OpVaultReader.h" +#include "OpData01.h" + +#include "core/Group.h" +#include "core/Tools.h" +#include "crypto/CryptoHash.h" +#include "crypto/SymmetricCipher.h" +#include "keys/PasswordKey.h" + +#include +#include +#include +#include +#include + +OpVaultReader::OpVaultReader(QObject* parent) + : QObject(parent) + , m_error(false) +{ +} + +OpVaultReader::~OpVaultReader() +{ +} + +Database* OpVaultReader::readDatabase(QDir& opdataDir, const QString& password) +{ + if (!opdataDir.exists()) { + m_error = true; + m_errorStr = tr("Directory .opvault must exist"); + return nullptr; + } + if (!opdataDir.isReadable()) { + m_error = true; + m_errorStr = tr("Directory .opvault must be readable"); + return nullptr; + } + + // https://support.1password.com/opvault-design/#directory-layout + QDir defaultDir = QDir(opdataDir); + if (!defaultDir.cd("default")) { + m_error = true; + m_errorStr = tr("Directory .opvault/default must exist"); + return nullptr; + } + if (!defaultDir.isReadable()) { + m_error = true; + m_errorStr = tr("Directory .opvault/default must be readable"); + return nullptr; + } + + auto key = QSharedPointer::create(); + key->addKey(QSharedPointer::create(password)); + + QScopedPointer db(new Database()); + db->setKdf(KeePass2::uuidToKdf(KeePass2::KDF_ARGON2)); + db->setCipher(KeePass2::CIPHER_AES256); + db->setKey(key, true, false); + db->metadata()->setName(opdataDir.dirName()); + + auto rootGroup = db->rootGroup(); + rootGroup->setTimeInfo({}); + rootGroup->setUpdateTimeinfo(false); + rootGroup->setName("OPVault Root Group"); + rootGroup->setUuid(QUuid::createUuid()); + + populateCategoryGroups(rootGroup); + + QFile profileJsFile(defaultDir.absoluteFilePath("profile.js")); + QJsonObject profileJson = readAndAssertJsonFile(profileJsFile, "var profile=", ";"); + if (profileJson.isEmpty()) { + return nullptr; + } + if (!processProfileJson(profileJson, password, rootGroup)) { + zeroKeys(); + return nullptr; + } + if (profileJson.contains("uuid") and profileJson["uuid"].isString()) { + rootGroup->setUuid(Tools::hexToUuid(profileJson["uuid"].toString())); + } + + QFile foldersJsFile(defaultDir.filePath("folders.js")); + if (foldersJsFile.exists()) { + QJsonObject foldersJs = readAndAssertJsonFile(foldersJsFile, "loadFolders(", ");"); + if (!processFolderJson(foldersJs, rootGroup)) { + zeroKeys(); + return nullptr; + } + } + + const QString bandChars("0123456789ABCDEF"); + QString bandPattern("band_%1.js"); + for (QChar ch : bandChars) { + QFile bandFile(defaultDir.filePath(bandPattern.arg(ch))); + if (!bandFile.exists()) { + qWarning() << "Skipping missing file \"" << bandFile.fileName() << "\""; + continue; + } + // https://support.1password.com/opvault-design/#band-files + QJsonObject bandJs = readAndAssertJsonFile(bandFile, "ld(", ");"); + const QStringList keys = bandJs.keys(); + for (const QString& entryKey : keys) { + const QJsonObject bandEnt = bandJs[entryKey].toObject(); + const QString uuid = bandEnt["uuid"].toString(); + if (entryKey != uuid) { + qWarning() << QString("Mismatched Entry UUID, its JSON key <<%1>> and its UUID <<%2>>") + .arg(entryKey) + .arg(uuid); + } + QStringList requiredKeys({"d", "k", "hmac"}); + bool ok = true; + for (const QString& requiredKey : asConst(requiredKeys)) { + if (!bandEnt.contains(requiredKey)) { + qCritical() << "Skipping malformed Entry UUID " << uuid << " without key " << requiredKey; + ok = false; + break; + } + } + if (!ok) { + continue; + } + // https://support.1password.com/opvault-design/#items + Entry* entry = processBandEntry(bandEnt, defaultDir, rootGroup); + if (!entry) { + qWarning() << "Unable to process Band Entry " << uuid; + } + } + } + + zeroKeys(); + return db.take(); +} + +bool OpVaultReader::hasError() +{ + return m_error; +} + +QString OpVaultReader::errorString() +{ + return m_errorStr; +} + +bool OpVaultReader::processProfileJson(QJsonObject& profileJson, const QString& password, Group* rootGroup) +{ + unsigned long iterations = profileJson["iterations"].toInt(); + // QString lastUpdatedBy = profileJson["lastUpdatedBy"].toString(); + QString masterKeyB64 = profileJson["masterKey"].toString(); + QString overviewKeyB64 = profileJson["overviewKey"].toString(); + // QString profileName = profileJs["profileName"].toString(); + QByteArray salt; + { + QString saltB64 = profileJson["salt"].toString(); + salt = QByteArray::fromBase64(saltB64.toUtf8()); + } + auto rootGroupTime = rootGroup->timeInfo(); + auto createdAt = static_cast(profileJson["createdAt"].toInt()); + rootGroupTime.setCreationTime(QDateTime::fromTime_t(createdAt, Qt::UTC)); + auto updatedAt = static_cast(profileJson["updatedAt"].toInt()); + rootGroupTime.setLastModificationTime(QDateTime::fromTime_t(updatedAt, Qt::UTC)); + rootGroup->setUuid(Tools::hexToUuid(profileJson["uuid"].toString())); + + const auto derivedKeys = deriveKeysFromPassPhrase(salt, password, iterations); + if (derivedKeys->error) { + m_error = true; + m_errorStr = derivedKeys->errorStr; + delete derivedKeys; + return false; + } + + QByteArray encKey = derivedKeys->encrypt; + QByteArray hmacKey = derivedKeys->hmac; + delete derivedKeys; + + auto masterKeys = decodeB64CompositeKeys(masterKeyB64, encKey, hmacKey); + if (masterKeys->error) { + m_error = true; + m_errorStr = masterKeys->errorStr; + delete masterKeys; + return false; + } + m_masterKey = masterKeys->encrypt; + m_masterHmacKey = masterKeys->hmac; + delete masterKeys; + auto overviewKeys = decodeB64CompositeKeys(overviewKeyB64, encKey, hmacKey); + if (overviewKeys->error) { + m_error = true; + m_errorStr = overviewKeys->errorStr; + delete overviewKeys; + return false; + } + m_overviewKey = overviewKeys->encrypt; + m_overviewHmacKey = overviewKeys->hmac; + delete overviewKeys; + + return true; +} + +bool OpVaultReader::processFolderJson(QJsonObject& foldersJson, Group* rootGroup) +{ + const QStringList keys = foldersJson.keys(); + + bool result = true; + for (const QString& key : keys) { + const QJsonValueRef& folderValue = foldersJson[key]; + if (!folderValue.isObject()) { + qWarning() << "Found non-Object folder with key \"" << key << "\""; + continue; + } + const QJsonObject folder = folderValue.toObject(); + QJsonObject overviewJs; + const QString overviewStr = folder.value("overview").toString(); + OpData01 foldOverview01; + if (!foldOverview01.decodeBase64(overviewStr, m_overviewKey, m_overviewHmacKey)) { + qCritical() << "Unable to decipher folder UUID \"" << key << "\": " << foldOverview01.errorString(); + result = false; + continue; + } + auto foldOverview = foldOverview01.getClearText(); + QJsonDocument fOverJSON = QJsonDocument::fromJson(foldOverview); + overviewJs = fOverJSON.object(); + + const QString& folderTitle = overviewJs["title"].toString(); + auto myGroup = new Group(); + myGroup->setParent(rootGroup); + myGroup->setName(folderTitle); + if (folder.contains("uuid")) { + myGroup->setUuid(Tools::hexToUuid(folder["uuid"].toString())); + } + + if (overviewJs.contains("smart") && overviewJs["smart"].toBool()) { + if (!overviewJs.contains("predicate_b64")) { + const QString& errMsg = + QString(R"(Expected a predicate in smart folder[uuid="%1"; title="%2"]))").arg(key, folderTitle); + qWarning() << errMsg; + myGroup->setNotes(errMsg); + } else { + QByteArray pB64 = QByteArray::fromBase64(overviewJs["predicate_b64"].toString().toUtf8()); + myGroup->setNotes(pB64.toHex()); + } + } + + TimeInfo ti; + bool timeInfoOk = false; + if (folder.contains("created")) { + auto createdTime = static_cast(folder["created"].toInt()); + ti.setCreationTime(QDateTime::fromTime_t(createdTime, Qt::UTC)); + timeInfoOk = true; + } + if (folder.contains("updated")) { + auto updateTime = static_cast(folder["updated"].toInt()); + ti.setLastModificationTime(QDateTime::fromTime_t(updateTime, Qt::UTC)); + timeInfoOk = true; + } + // "tx" is modified by sync, not by user; maybe a custom attribute? + if (timeInfoOk) { + myGroup->setTimeInfo(ti); + } + } + return result; +} + +/* + * Asserts that the given file is an existing file, able to be read, contains JSON, and that + * the payload is a JSON object. Currently it just returns an empty QJsonObject as a means of + * indicating the error, although it will qCritical() if unable to actually open the file for reading. + * + * @param file the path containing the JSON file + * @param stripLeading any leading characters that might be present in file which should be removed + * @param stripTrailing the trailing characters that might be present in file which should be removed + * @return + */ +QJsonObject OpVaultReader::readAndAssertJsonFile(QFile& file, const QString& stripLeading, const QString& stripTrailing) +{ + QByteArray filePayload; + const QFileInfo& fileInfo = QFileInfo(file); + auto absFilePath = fileInfo.absoluteFilePath(); + if (!fileInfo.exists()) { + qCritical() << QString("File \"%1\" must exist").arg(absFilePath); + return QJsonObject(); + } + if (!fileInfo.isReadable()) { + qCritical() << QString("File \"%1\" must be readable").arg(absFilePath); + return QJsonObject(); + } + + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + qCritical() << QString("Unable to open \"%1\" readonly+text").arg(absFilePath); + } + filePayload = file.readAll(); + file.close(); + if (!stripLeading.isEmpty()) { + QByteArray prefix = stripLeading.toUtf8(); + if (filePayload.startsWith(prefix)) { + filePayload = filePayload.remove(0, prefix.size()); + } + } + if (!stripTrailing.isEmpty()) { + QByteArray suffix = stripTrailing.toUtf8(); + if (filePayload.endsWith(suffix)) { + const int delBytes = suffix.size(); + filePayload = filePayload.remove(filePayload.length() - delBytes, delBytes); + } + } + + QJsonParseError* error = Q_NULLPTR; + QJsonDocument jDoc = QJsonDocument::fromJson(filePayload, error); + if (!jDoc.isObject()) { + qCritical() << "Expected " << filePayload << "to be a JSON Object"; + return QJsonObject(); + } + return jDoc.object(); +} + +/* Convenience method for calling decodeCompositeKeys when you have a base64 encrypted composite key. */ +OpVaultReader::DerivedKeyHMAC* +OpVaultReader::decodeB64CompositeKeys(const QString& b64, const QByteArray& encKey, const QByteArray& hmacKey) +{ + auto result = new DerivedKeyHMAC(); + + OpData01 keyKey01; + if (!keyKey01.decodeBase64(b64, encKey, hmacKey)) { + result->error = true; + result->errorStr = tr("Unable to decode masterKey: %1").arg(keyKey01.errorString()); + return result; + } + const QByteArray keyKey = keyKey01.getClearText(); + + return decodeCompositeKeys(keyKey); +} + +/* + * Given a string of bytes, decompose it into its constituent parts, an encryption key and a HMAC key. + * The plaintext of the masterKey is 256 bytes of data selected randomly when the keychain was first created. + * + * The 256 byte (2048 bit) plaintext content of the masterKey is then hashed with SHA-512. + * The first 32 bytes (256-bits) of the resulting hash are the master encryption key, + * and the second 32 bytes are the master hmac key. + */ +OpVaultReader::DerivedKeyHMAC* OpVaultReader::decodeCompositeKeys(const QByteArray& keyKey) +{ + const int encKeySize = 256 / 8; + const int hmacKeySize = 256 / 8; + const int digestSize = encKeySize + hmacKeySize; + + auto result = new DerivedKeyHMAC; + result->error = false; + + result->encrypt = QByteArray(encKeySize, '\0'); + result->hmac = QByteArray(hmacKeySize, '\0'); + + const char* buffer_vp = keyKey.data(); + auto buf_len = size_t(keyKey.size()); + + const int algo = GCRY_MD_SHA512; + unsigned char digest[digestSize]; + gcry_md_hash_buffer(algo, digest, buffer_vp, buf_len); + + unsigned char* cp = digest; + for (int i = 0, len = encKeySize; i < len; ++i) { + result->encrypt[i] = *(cp++); + } + for (int i = 0, len = hmacKeySize; i < len; ++i) { + result->hmac[i] = *(cp++); + } + + return result; +} + +/* + * Translates the provided salt and passphrase into a derived set of keys, one for encryption + * and one for use as a HMAC key. See https://support.1password.com/opvault-design/#key-derivation + * @param iterations the number of rounds to apply the derivation formula + * @return a non-null structure containing either the error or the two password-derived keys + */ +OpVaultReader::DerivedKeyHMAC* +OpVaultReader::deriveKeysFromPassPhrase(QByteArray& salt, const QString& password, unsigned long iterations) +{ + const int derivedEncKeySize = 256 / 8; + const int derivedMACSize = 256 / 8; + const int keysize = derivedEncKeySize + derivedMACSize; + + auto result = new DerivedKeyHMAC; + result->error = false; + + QByteArray keybuffer(keysize, '\0'); + auto err = gcry_kdf_derive(password.toUtf8().constData(), + password.size(), + GCRY_KDF_PBKDF2, + GCRY_MD_SHA512, + salt.constData(), + salt.size(), + iterations, + keysize, + keybuffer.data()); + if (err != 0) { + result->error = true; + result->errorStr = tr("Unable to derive master key: %1").arg(gcry_strerror(err)); + return result; + } + if (keysize != keybuffer.size()) { + qWarning() << "Calling PBKDF2(keysize=" << keysize << "yielded" << keybuffer.size() << "bytes"; + } + + QByteArray::const_iterator it = keybuffer.cbegin(); + + result->encrypt = QByteArray(derivedEncKeySize, '\0'); + for (int i = 0, len = derivedEncKeySize; i < len && it != keybuffer.cend(); ++i, ++it) { + result->encrypt[i] = *it; + } + + result->hmac = QByteArray(derivedMACSize, '\0'); + for (int i = 0; i < derivedMACSize && it != keybuffer.cend(); ++i, ++it) { + result->hmac[i] = *it; + } + return result; +} + +/*! + * \sa https://support.1password.com/opvault-design/#category + */ +void OpVaultReader::populateCategoryGroups(Group* rootGroup) +{ + QMap categoryMap; + categoryMap.insert("001", "Login"); + categoryMap.insert("002", "Credit Card"); + categoryMap.insert("003", "Secure Note"); + categoryMap.insert("004", "Identity"); + categoryMap.insert("005", "Password"); + categoryMap.insert("099", "Tombstone"); + categoryMap.insert("100", "Software License"); + categoryMap.insert("101", "Bank Account"); + categoryMap.insert("102", "Database"); + categoryMap.insert("103", "Driver License"); + categoryMap.insert("104", "Outdoor License"); + categoryMap.insert("105", "Membership"); + categoryMap.insert("106", "Passport"); + categoryMap.insert("107", "Rewards"); + categoryMap.insert("108", "SSN"); + categoryMap.insert("109", "Router"); + categoryMap.insert("110", "Server"); + categoryMap.insert("111", "Email"); + for (const QString& catNum : categoryMap.keys()) { + const QString& category = categoryMap[catNum]; + auto g = new Group(); + g->setName(category); + g->setProperty("code", catNum); + g->setUpdateTimeinfo(false); + // maybe make these stable, so folks can depend on them? + g->setUuid(QUuid::createUuid()); + g->setParent(rootGroup); + } +} + +void OpVaultReader::zeroKeys() +{ + m_masterKey.fill('\0'); + m_masterHmacKey.fill('\0'); + m_overviewKey.fill('\0'); + m_overviewHmacKey.fill('\0'); +} diff --git a/src/format/OpVaultReader.h b/src/format/OpVaultReader.h new file mode 100644 index 0000000000..5854158546 --- /dev/null +++ b/src/format/OpVaultReader.h @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2019 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef OPVAULT_READER_H_ +#define OPVAULT_READER_H_ + +#include + +#include "core/Database.h" +#include "core/Metadata.h" + +/*! + * Imports a directory in the 1Password \c opvault format into a \c Database. + * \sa https://support.1password.com/opvault-overview/ + * \sa https://support.1password.com/opvault-design/ + * \sa https://cache.agilebits.com/security-kb/freddy-2013-12-04.tar.gz is the sample data used to test this class, + * and its password is \c freddy + */ +class OpVaultReader : public QObject +{ + Q_OBJECT + +public: + explicit OpVaultReader(QObject* parent = nullptr); + ~OpVaultReader() override; + + Database* readDatabase(QDir& opdataDir, const QString& password); + + bool hasError(); + QString errorString(); + +private: + struct DerivedKeyHMAC + { + QByteArray encrypt; + QByteArray hmac; + bool error; + QString errorStr; + }; + + QJsonObject readAndAssertJsonFile(QFile& file, const QString& stripLeading, const QString& stripTrailing); + + DerivedKeyHMAC* deriveKeysFromPassPhrase(QByteArray& salt, const QString& password, unsigned long iterations); + DerivedKeyHMAC* decodeB64CompositeKeys(const QString& b64, const QByteArray& encKey, const QByteArray& hmacKey); + DerivedKeyHMAC* decodeCompositeKeys(const QByteArray& keyKey); + + /*! + * \sa https://support.1password.com/opvault-design/#profile-js + * @param profileJson the contents of \c profile.js + * @return \c true if the profile data was decrypted successfully, \c false otherwise + */ + bool processProfileJson(QJsonObject& profileJson, const QString& password, Group* rootGroup); + + /*! + * \sa https://support.1password.com/opvault-design/#folders-js + * @param foldersJson the map from a folder UUID to its data (name and any smart query) + * @return \c true if the folder data was decrypted successfully, \c false otherwise + */ + bool processFolderJson(QJsonObject& foldersJson, Group* rootGroup); + + /*! + * Decrypts the provided band object into its interior structure, + * as well as the encryption key and HMAC key declared therein, + * which are used to decrypt the attachments, also. + * @returns \c nullptr if unable to do the decryption, otherwise the interior object and its keys + */ + bool decryptBandEntry(const QJsonObject& bandEntry, QJsonObject& data, QByteArray& key, QByteArray& hmacKey); + Entry* processBandEntry(const QJsonObject& bandEntry, const QDir& attachmentDir, Group* rootGroup); + + bool readAttachment(const QString& filePath, + const QByteArray& itemKey, + const QByteArray& itemHmacKey, + QJsonObject& metadata, + QByteArray& payload); + void fillAttachment(Entry* entry, + const QFileInfo& attachmentFileInfo, + const QByteArray& entryKey, + const QByteArray& entryHmacKey); + void fillAttachments(Entry* entry, + const QDir& attachmentDir, + const QByteArray& entryKey, + const QByteArray& entryHmacKey); + + bool fillAttributes(Entry* entry, const QJsonObject& bandEntry); + + void fillFromSection(Entry* entry, const QJsonObject& section); + void fillFromSectionField(Entry* entry, const QString& sectionName, QJsonObject& field); + QString resolveAttributeName(const QString& section, const QString& name, const QString& text); + + void populateCategoryGroups(Group* rootGroup); + /*! Used to blank the memory after the keys have been used. */ + void zeroKeys(); + + bool m_error; + QString m_errorStr; + QByteArray m_masterKey; + QByteArray m_masterHmacKey; + /*! Used to decrypt overview text, such as folder names. */ + QByteArray m_overviewKey; + QByteArray m_overviewHmacKey; + + friend class TestOpVaultReader; +}; + +#endif /* OPVAULT_READER_H_ */ diff --git a/src/format/OpVaultReaderAttachments.cpp b/src/format/OpVaultReaderAttachments.cpp new file mode 100644 index 0000000000..49367e3063 --- /dev/null +++ b/src/format/OpVaultReaderAttachments.cpp @@ -0,0 +1,250 @@ +/* + * Copyright (C) 2019 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "OpData01.h" +#include "OpVaultReader.h" + +#include "core/Group.h" +#include "core/Tools.h" + +#include +#include +#include +#include + +/*! + * This will \c qCritical() if unable to open the file for reading. + * @param file the \c .attachment file to decode + * @return \c nullptr if unable to take action, else a pair of metadata and the actual attachment bits + * \sa https://support.1password.com/opvault-design/#attachments + */ +bool OpVaultReader::readAttachment(const QString& filePath, + const QByteArray& itemKey, + const QByteArray& itemHmacKey, + QJsonObject& metadata, + QByteArray& payload) +{ + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly)) { + qCritical() << QString("Unable to open \"%s\" for reading").arg(file.fileName()); + return false; + } + + QString magic("OPCLDAT"); + QByteArray magicBytes = file.read(7); + if (magicBytes != magic.toUtf8()) { + qCritical() << "Expected OPCLDAT but found <<" << magicBytes.toHex() << ">>"; + return false; + } + + QByteArray version = file.read(1); + if (version[0] != '\001' && version[0] != '\002') { + qCritical() << "Unexpected version number; wanted 1 or 2, got <<" << version << ">>"; + return false; + } + const QByteArray& metadataLenBytes = file.read(2); + if (metadataLenBytes.size() != 2) { + qCritical() << "Unable to read all metadata length bytes; wanted 2 bytes, got " << metadataLenBytes.size() + << ": <<" << metadataLenBytes.toHex() << ">>"; + return false; + } + const auto b0 = static_cast(metadataLenBytes[0]); + const auto b1 = static_cast(metadataLenBytes[1]); + int metadataLen = ((0xFF & b1) << 8) | (0xFF & b0); + + // no really: it's labeled "Junk" in the spec + int junkBytesRead = file.read(2).size(); + if (junkBytesRead != 2) { + qCritical() << "Unable to read all \"junk\" bytes; wanted 2 bytes, got " << junkBytesRead; + return false; + } + + const QByteArray& iconLenBytes = file.read(4); + if (iconLenBytes.size() != 4) { + qCritical() << "Unable to read all \"iconLen\" bytes; wanted 4 bytes, got " << iconLenBytes.size(); + return false; + } + + int iconLen = 0; + for (int i = 0, len = iconLenBytes.size(); i < len; ++i) { + char ch = iconLenBytes[i]; + auto b = static_cast(ch & 0xFF); + iconLen = (b << (i * 8)) | iconLen; + } + + QByteArray metadataJsonBytes = file.read(metadataLen); + if (metadataJsonBytes.size() != metadataLen) { + qCritical() << "Unable to read all bytes of metadata JSON; wanted " << metadataLen << "but read " + << metadataJsonBytes.size(); + return false; + } + QByteArray iconBytes = file.read(iconLen); + if (iconBytes.size() != iconLen) { + qCritical() << "Unable to read all icon bytes; wanted " << iconLen << "but read " << iconBytes.size(); + // apologies for the icon being fatal, but it would take some gear-turning + // to re-sync where in the attach header we are + return false; + } + + // we don't actually _care_ what the icon bytes are, + // but they damn well better be valid opdata01 and pass its HMAC + OpData01 icon01; + if (!icon01.decode(iconBytes, itemKey, itemHmacKey)) { + qCritical() << "Unable to decipher attachment icon in " << filePath << ": " << icon01.errorString(); + return false; + } + + QJsonParseError jsError; + QJsonDocument jDoc = QJsonDocument::fromJson(metadataJsonBytes, &jsError); + if (jsError.error != QJsonParseError::ParseError::NoError) { + qCritical() << "Found invalid attachment metadata JSON at offset " << jsError.offset << ": error(" + << jsError.error << "): " << jsError.errorString() << "\n<<" << metadataJsonBytes << ">>"; + return false; + } + if (!jDoc.isObject()) { + qCritical() << "Expected " << metadataJsonBytes << "to be a JSON Object"; + return false; + } + + metadata = jDoc.object(); + if (metadata.contains("trashed") && metadata["trashed"].toBool()) { + return false; + } + + if (!metadata.contains("contentsSize")) { + qWarning() << "Expected attachment metadata to contain \"contentsSize\" but nope: " << metadata; + return false; + } else if (!metadata["contentsSize"].isDouble()) { + qWarning() << "Expected attachment metadata to contain numeric \"contentsSize\" but nope: " << metadata; + return false; + } + int bytesLen = metadata["contentsSize"].toInt(); + const QByteArray encData = file.readAll(); + if (encData.size() < bytesLen) { + qCritical() << "Unable to read all of the attachment payload; wanted " << bytesLen << "but got" + << encData.size(); + return false; + } + + OpData01 att01; + if (!att01.decode(encData, itemKey, itemHmacKey)) { + qCritical() << "Unable to decipher attachment payload: " << att01.errorString(); + return false; + } + + payload = att01.getClearText(); + return true; +} + +/*! + * \sa https://support.1password.com/opvault-design/#attachments + */ +void OpVaultReader::fillAttachments(Entry* entry, + const QDir& attachmentDir, + const QByteArray& entryKey, + const QByteArray& entryHmacKey) +{ + /*! + * Attachment files are named with the UUID of the item that they are attached to followed by an underscore + * and then followed by the UUID of the attachment itself. The file is then given the extension .attachment. + */ + auto fileFilter = QString("%1_*.attachment").arg(entry->uuidToHex().toUpper()); + const auto& attachInfoList = attachmentDir.entryInfoList(QStringList() << fileFilter, QDir::Files); + int attachmentCount = attachInfoList.size(); + if (attachmentCount == 0) { + return; + } + + for (const auto& info : attachInfoList) { + if (!info.isReadable()) { + qCritical() << QString("Attachment file \"%1\" is not readable").arg(info.absoluteFilePath()); + continue; + } + fillAttachment(entry, info, entryKey, entryHmacKey); + } +} + +void OpVaultReader::fillAttachment(Entry* entry, + const QFileInfo& info, + const QByteArray& entryKey, + const QByteArray& entryHmacKey) +{ + QJsonObject attachMetadata; + QByteArray attachPayload; + if (!readAttachment(info.absoluteFilePath(), entryKey, entryHmacKey, attachMetadata, attachPayload)) { + return; + } + + if (!attachMetadata.contains("overview")) { + qWarning() << "Expected \"overview\" in attachment metadata"; + return; + } + + const QString& overB64 = attachMetadata["overview"].toString(); + OpData01 over01; + + if (over01.decodeBase64(overB64, m_overviewKey, m_overviewHmacKey)) { + QByteArray overviewJson = over01.getClearText(); + QJsonDocument overDoc = QJsonDocument::fromJson(overviewJson); + if (overDoc.isObject()) { + QJsonObject overObj = overDoc.object(); + attachMetadata.remove("overview"); + for (QString& key : overObj.keys()) { + const QJsonValueRef& value = overObj[key]; + QString insertAs = key; + for (int aa = 0; attachMetadata.contains(insertAs) && aa < 5; ++aa) { + insertAs = QString("%1_%2").arg(key, aa); + } + attachMetadata[insertAs] = value; + } + } else { + qWarning() << "Expected JSON Object in \"overview\" but nope: " << overDoc; + } + } else { + qCritical() + << QString("Unable to decode attach.overview for \"%1\": %2").arg(info.fileName(), over01.errorString()); + } + + QByteArray payload; + payload.append(QString("attachment file is actually %1 bytes\n").arg(info.size()).toUtf8()); + for (QString& key : attachMetadata.keys()) { + const QJsonValueRef& value = attachMetadata[key]; + QByteArray valueBytes; + if (value.isString()) { + valueBytes = value.toString().toUtf8(); + } else if (value.isDouble()) { + valueBytes = QString("%1").arg(value.toInt()).toUtf8(); + } else if (value.isBool()) { + valueBytes = value.toBool() ? "true" : "false"; + } else { + valueBytes = QString("Unexpected metadata type in attachment: %1").arg(value.type()).toUtf8(); + } + payload.append(key.toUtf8()).append(":=").append(valueBytes).append("\n"); + } + + QString attachKey = info.baseName(); + if (attachMetadata.contains("filename")) { + QJsonValueRef attFilename = attachMetadata["filename"]; + if (attFilename.isString()) { + attachKey = attFilename.toString(); + } else { + qWarning() << QString("Unexpected type of attachment \"filename\": %1").arg(attFilename.type()); + } + } + + entry->attachments()->set(attachKey, attachPayload); +} diff --git a/src/format/OpVaultReaderBandEntry.cpp b/src/format/OpVaultReaderBandEntry.cpp new file mode 100644 index 0000000000..22af783d59 --- /dev/null +++ b/src/format/OpVaultReaderBandEntry.cpp @@ -0,0 +1,266 @@ +/* + * Copyright (C) 2019 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "OpData01.h" +#include "OpVaultReader.h" + +#include "core/Group.h" +#include "core/Tools.h" +#include "crypto/CryptoHash.h" +#include "crypto/SymmetricCipher.h" + +#include +#include +#include +#include +#include + +bool OpVaultReader::decryptBandEntry(const QJsonObject& bandEntry, + QJsonObject& data, + QByteArray& key, + QByteArray& hmacKey) +{ + if (!bandEntry.contains("d")) { + qWarning() << "Band entries must contain a \"d\" key: " << bandEntry.keys(); + return false; + } + if (!bandEntry.contains("k")) { + qWarning() << "Band entries must contain a \"k\" key: " << bandEntry.keys(); + return false; + } + + const QString uuid = bandEntry.value("uuid").toString(); + + /*! + * This is the encrypted item and MAC keys. + * It is encrypted with the master encryption key and authenticated with the master MAC key. + * + * The last 32 bytes comprise the HMAC-SHA256 of the IV and the encrypted data. + * The MAC is computed with the master MAC key. + * The data before the MAC is the AES-CBC encrypted item keys using unique random 16-byte IV. + * \code + * uint8_t crypto_key[32]; + * uint8_t mac_key[32]; + * \endcode + * \sa https://support.1password.com/opvault-design/#k + */ + const QString& entKStr = bandEntry["k"].toString(); + QByteArray kBA = QByteArray::fromBase64(entKStr.toUtf8()); + const int wantKsize = 16 + 32 + 32 + 32; + if (kBA.size() != wantKsize) { + qCritical("Malformed \"k\" size; expected %d got %d\n", wantKsize, kBA.size()); + return false; + } + + QByteArray hmacSig = kBA.mid(kBA.size() - 32, 32); + const QByteArray& realHmacSig = + CryptoHash::hmac(kBA.mid(0, kBA.size() - hmacSig.size()), m_masterHmacKey, CryptoHash::Sha256); + if (realHmacSig != hmacSig) { + qCritical() << QString(R"(Entry "k" failed its HMAC in UUID "%1", wanted "%2" got "%3")") + .arg(uuid) + .arg(QString::fromUtf8(hmacSig.toHex())) + .arg(QString::fromUtf8(realHmacSig)); + return false; + } + + QByteArray iv = kBA.mid(0, 16); + QByteArray keyAndMacKey = kBA.mid(iv.size(), 64); + SymmetricCipher cipher(SymmetricCipher::Aes256, SymmetricCipher::Cbc, SymmetricCipher::Decrypt); + if (!cipher.init(m_masterKey, iv)) { + qCritical() << "Unable to init cipher using masterKey in UUID " << uuid; + return false; + } + if (!cipher.processInPlace(keyAndMacKey)) { + qCritical() << "Unable to decipher \"k\"(key+hmac) in UUID " << uuid; + return false; + } + + key = keyAndMacKey.mid(0, 32); + hmacKey = keyAndMacKey.mid(32); + + QString dKeyB64 = bandEntry.value("d").toString(); + OpData01 entD01; + if (!entD01.decodeBase64(dKeyB64, key, hmacKey)) { + qCritical() << R"(Unable to decipher "d" in UUID ")" << uuid << "\": " << entD01.errorString(); + return false; + } + + auto clearText = entD01.getClearText(); + data = QJsonDocument::fromJson(clearText).object(); + return true; +} + +Entry* OpVaultReader::processBandEntry(const QJsonObject& bandEntry, const QDir& attachmentDir, Group* rootGroup) +{ + const QString uuid = bandEntry.value("uuid").toString(); + if (!(uuid.size() == 32 || uuid.size() == 36)) { + qWarning() << QString("Skipping suspicious band UUID <<%1>> with length %2").arg(uuid).arg(uuid.size()); + return nullptr; + } + + const auto entry = new Entry(); + + if (bandEntry.contains("category")) { + const QJsonValue& categoryValue = bandEntry["category"]; + if (categoryValue.isString()) { + bool found = false; + const QString category = categoryValue.toString(); + for (Group* group : rootGroup->children()) { + const QVariant& groupCode = group->property("code"); + if (category == groupCode.toString()) { + entry->setGroup(group); + found = true; + break; + } + } + if (!found) { + qWarning() << QString("Unable to place Entry.Category \"%1\" so using the Root instead").arg(category); + entry->setGroup(rootGroup); + } + } else { + qWarning() << QString(R"(Skipping non-String Category type "%1" in UUID "%2")") + .arg(categoryValue.type()) + .arg(uuid); + entry->setGroup(rootGroup); + } + } else { + qWarning() << "Using the root group because the entry is category-less: <<\n" + << bandEntry << "\n>> in UUID " << uuid; + entry->setGroup(rootGroup); + } + + entry->setUpdateTimeinfo(false); + TimeInfo ti; + bool timeInfoOk = false; + if (bandEntry.contains("created")) { + auto createdTime = static_cast(bandEntry["created"].toInt()); + ti.setCreationTime(QDateTime::fromTime_t(createdTime, Qt::UTC)); + timeInfoOk = true; + } + if (bandEntry.contains("updated")) { + auto updateTime = static_cast(bandEntry["updated"].toInt()); + ti.setLastModificationTime(QDateTime::fromTime_t(updateTime, Qt::UTC)); + timeInfoOk = true; + } + // "tx" is modified by sync, not by user; maybe a custom attribute? + if (timeInfoOk) { + entry->setTimeInfo(ti); + } + entry->setUuid(Tools::hexToUuid(uuid)); + + if (!fillAttributes(entry, bandEntry)) { + delete entry; + return nullptr; + } + + QJsonObject data; + QByteArray entryKey; + QByteArray entryHmacKey; + + if (!decryptBandEntry(bandEntry, data, entryKey, entryHmacKey)) { + return nullptr; + } + + if (data.contains("notesPlain")) { + entry->setNotes(data.value("notesPlain").toString()); + } + + // it seems sometimes the password is a top-level field, and not in "fields" themselves + if (data.contains("password")) { + entry->setPassword(data.value("password").toString()); + } + + for (const auto& fieldValue : data.value("fields").toArray()) { + if (!fieldValue.isObject()) { + continue; + } + + auto field = fieldValue.toObject(); + auto designation = field["designation"].toString(); + auto value = field["value"].toString(); + if (designation == "password") { + entry->setPassword(value); + } else if (designation == "username") { + entry->setUsername(value); + } + } + + const QJsonArray& sectionsArray = data["sections"].toArray(); + for (const QJsonValue& sectionValue : sectionsArray) { + if (!sectionValue.isObject()) { + qWarning() << R"(Skipping non-Object in "sections" for UUID ")" << uuid << "\" << " << sectionsArray + << ">>"; + continue; + } + const QJsonObject& section = sectionValue.toObject(); + + fillFromSection(entry, section); + } + + fillAttachments(entry, attachmentDir, entryKey, entryHmacKey); + return entry; +} + +bool OpVaultReader::fillAttributes(Entry* entry, const QJsonObject& bandEntry) +{ + const QString overviewStr = bandEntry.value("o").toString(); + OpData01 entOver01; + if (!entOver01.decodeBase64(overviewStr, m_overviewKey, m_overviewHmacKey)) { + qCritical() << "Unable to decipher 'o' in UUID \"" << entry->uuid() << "\"\n" + << ": " << entOver01.errorString(); + return false; + } + + QByteArray overviewJsonBytes = entOver01.getClearText(); + QJsonDocument overviewDoc = QJsonDocument::fromJson(overviewJsonBytes); + QJsonObject overviewJson = overviewDoc.object(); + + QString title = overviewJson.value("title").toString(); + entry->setTitle(title); + + QString url = overviewJson["url"].toString(); + entry->setUrl(url); + + int i = 1; + for (const auto& urlV : overviewJson["URLs"].toArray()) { + auto urlName = QString("URL_%1").arg(i); + auto urlValue = urlV.toString(); + if (urlV.isObject()) { + const auto& urlObj = urlV.toObject(); + if (urlObj["l"].isString() && urlObj["u"].isString()) { + urlName = urlObj["l"].toString(); + urlValue = urlObj["u"].toString(); + } else { + continue; + } + } + if (!urlValue.isEmpty() && urlValue != url) { + entry->attributes()->set(urlName, urlValue); + ++i; + } + } + + QStringList tagsList; + for (const auto& tagV : overviewJson["tags"].toArray()) { + if (tagV.isString()) { + tagsList << tagV.toString(); + } + } + entry->setTags(tagsList.join(',')); + + return true; +} diff --git a/src/format/OpVaultReaderSections.cpp b/src/format/OpVaultReaderSections.cpp new file mode 100644 index 0000000000..68ad478d3c --- /dev/null +++ b/src/format/OpVaultReaderSections.cpp @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2019 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "OpData01.h" +#include "OpVaultReader.h" + +#include "core/Group.h" +#include "core/Tools.h" +#include "crypto/CryptoHash.h" +#include "crypto/SymmetricCipher.h" +#include "totp/totp.h" + +#include +#include +#include +#include +#include +#include + +void OpVaultReader::fillFromSection(Entry* entry, const QJsonObject& section) +{ + const auto uuid = entry->uuid(); + const QString& sectionName = section["name"].toString(); + + if (!section.contains("fields")) { + auto sectionNameLC = sectionName.toLower(); + auto sectionTitleLC = section["title"].toString("").toLower(); + if (!(sectionNameLC == "linked items" && sectionTitleLC == "related items")) { + qWarning() << R"(Skipping "fields"-less Section in UUID ")" << uuid << "\": <<" << section << ">>"; + } + return; + } else if (!section["fields"].isArray()) { + qWarning() << R"(Skipping non-Array "fields" in UUID ")" << uuid << "\"\n"; + return; + } + QJsonArray sectionFields = section["fields"].toArray(); + for (const QJsonValue sectionField : sectionFields) { + if (!sectionField.isObject()) { + qWarning() << R"(Skipping non-Object "fields" in UUID ")" << uuid << "\": << " << sectionField << ">>"; + continue; + } + QJsonObject field = sectionField.toObject(); + fillFromSectionField(entry, sectionName, field); + } +} + +void OpVaultReader::fillFromSectionField(Entry* entry, const QString& sectionName, QJsonObject& field) +{ + if (!field.contains("v")) { + // for our purposes, we don't care if there isn't a value in the field + return; + } + + // Ignore "a" and "inputTraits" fields, they don't apply to KPXC + + auto attrName = resolveAttributeName(sectionName, field["n"].toString(), field["t"].toString()); + auto attrValue = field.value("v").toVariant().toString(); + auto kind = field["k"].toString(); + + if (attrName.startsWith("TOTP_")) { + if (attrValue.startsWith("otpauth://")) { + QUrlQuery query(attrValue); + // at least as of 1Password 7, they don't append the digits= and period= which totp.cpp requires + if (!query.hasQueryItem("digits")) { + query.addQueryItem("digits", QString("%1").arg(Totp::DEFAULT_DIGITS)); + } + if (!query.hasQueryItem("period")) { + query.addQueryItem("period", QString("%1").arg(Totp::DEFAULT_STEP)); + } + attrValue = query.toString(QUrl::FullyEncoded); + } + entry->attributes()->set(Totp::ATTRIBUTE_SETTINGS, attrValue, true); + } else if (attrName.startsWith("expir", Qt::CaseInsensitive)) { + QDateTime expiry; + if (kind == "date") { + expiry = QDateTime::fromTime_t(attrValue.toUInt(), Qt::UTC); + } else { + expiry = QDateTime::fromString(attrValue, "yyyyMM"); + expiry.setTimeSpec(Qt::UTC); + } + + if (expiry.isValid()) { + entry->setExpiryTime(expiry); + entry->setExpires(true); + } + } else { + if (kind == "date") { + auto date = QDateTime::fromTime_t(attrValue.toUInt(), Qt::UTC); + if (date.isValid()) { + attrValue = date.toString(); + } + } + + entry->attributes()->set(attrName, attrValue, (kind == "password" || kind == "concealed")); + } +} + +QString OpVaultReader::resolveAttributeName(const QString& section, const QString& name, const QString& text) +{ + // Special case for TOTP + if (name.startsWith("TOTP_")) { + return name; + } + + auto lowName = name.toLower(); + auto lowText = text.toLower(); + if (section.isEmpty()) { + // Empty section implies these are core attributes + // try to find username, password, url + if (lowName == "password" || lowText == "password") { + return EntryAttributes::PasswordKey; + } else if (lowName == "username" || lowText == "username") { + return EntryAttributes::UserNameKey; + } else if (lowName == "url" || lowText == "url" || lowName == "hostname" || lowText == "server" + || lowName == "website") { + return EntryAttributes::URLKey; + } + return name; + } + + return QString("%1_%2").arg(section, name); +} diff --git a/src/gui/AboutDialog.cpp b/src/gui/AboutDialog.cpp index f37baedc0a..7abd9059eb 100644 --- a/src/gui/AboutDialog.cpp +++ b/src/gui/AboutDialog.cpp @@ -26,7 +26,6 @@ #include - static const QString aboutMaintainers = R"(

  • Jonathan White (droidmonkey)
  • diff --git a/src/gui/ApplicationSettingsWidget.cpp b/src/gui/ApplicationSettingsWidget.cpp index 2461230c86..fce3522371 100644 --- a/src/gui/ApplicationSettingsWidget.cpp +++ b/src/gui/ApplicationSettingsWidget.cpp @@ -97,7 +97,7 @@ ApplicationSettingsWidget::ApplicationSettingsWidget(QWidget* parent) #else m_generalUi->checkForUpdatesOnStartupCheckBox->setVisible(false); m_generalUi->checkForUpdatesIncludeBetasCheckBox->setVisible(false); - m_generalUi->checkUpdatesSpacer->changeSize(0,0, QSizePolicy::Fixed, QSizePolicy::Fixed); + m_generalUi->checkUpdatesSpacer->changeSize(0, 0, QSizePolicy::Fixed, QSizePolicy::Fixed); #endif #ifndef WITH_XC_NETWORKING diff --git a/src/gui/DatabaseTabWidget.cpp b/src/gui/DatabaseTabWidget.cpp index 61051e3f0b..ce26978028 100644 --- a/src/gui/DatabaseTabWidget.cpp +++ b/src/gui/DatabaseTabWidget.cpp @@ -262,6 +262,20 @@ void DatabaseTabWidget::importKeePass1Database() dbWidget->switchToImportKeepass1(fileName); } +void DatabaseTabWidget::importOpVaultDatabase() +{ + QString fileName = fileDialog()->getExistingDirectory(this, "Open .opvault database"); + + if (fileName.isEmpty()) { + return; + } + + auto db = QSharedPointer::create(); + auto* dbWidget = new DatabaseWidget(db, this); + addDatabaseTab(dbWidget); + dbWidget->switchToImportOpVault(fileName); +} + /** * Attempt to close the current database and remove its tab afterwards. * diff --git a/src/gui/DatabaseTabWidget.h b/src/gui/DatabaseTabWidget.h index af84c0a1e1..a39cc6a404 100644 --- a/src/gui/DatabaseTabWidget.h +++ b/src/gui/DatabaseTabWidget.h @@ -65,6 +65,7 @@ public slots: void mergeDatabase(); void importCsv(); void importKeePass1Database(); + void importOpVaultDatabase(); bool saveDatabase(int index = -1); bool saveDatabaseAs(int index = -1); void exportToCsv(); diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index 178af80051..c51594460f 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -50,6 +50,7 @@ #include "gui/FileDialog.h" #include "gui/KeePass1OpenWidget.h" #include "gui/MessageBox.h" +#include "gui/OpVaultOpenWidget.h" #include "gui/TotpDialog.h" #include "gui/TotpExportSettingsDialog.h" #include "gui/TotpSetupDialog.h" @@ -86,6 +87,7 @@ DatabaseWidget::DatabaseWidget(QSharedPointer db, QWidget* parent) , m_databaseSettingDialog(new DatabaseSettingsDialog(this)) , m_databaseOpenWidget(new DatabaseOpenWidget(this)) , m_keepass1OpenWidget(new KeePass1OpenWidget(this)) + , m_opVaultOpenWidget(new OpVaultOpenWidget(this)) , m_groupView(new GroupView(m_db.data(), m_mainSplitter)) , m_saveAttempts(0) , m_fileWatcher(new DelayingFileWatcher(this)) @@ -160,6 +162,7 @@ DatabaseWidget::DatabaseWidget(QSharedPointer db, QWidget* parent) m_databaseSettingDialog->setObjectName("databaseSettingsDialog"); m_databaseOpenWidget->setObjectName("databaseOpenWidget"); m_keepass1OpenWidget->setObjectName("keepass1OpenWidget"); + m_opVaultOpenWidget->setObjectName("opVaultOpenWidget"); addChildWidget(m_mainWidget); addChildWidget(m_editEntryWidget); @@ -169,6 +172,7 @@ DatabaseWidget::DatabaseWidget(QSharedPointer db, QWidget* parent) addChildWidget(m_databaseOpenWidget); addChildWidget(m_csvImportWizard); addChildWidget(m_keepass1OpenWidget); + addChildWidget(m_opVaultOpenWidget); // clang-format off connect(m_mainSplitter, SIGNAL(splitterMoved(int,int)), SIGNAL(mainSplitterSizesChanged())); @@ -188,6 +192,7 @@ DatabaseWidget::DatabaseWidget(QSharedPointer db, QWidget* parent) connect(m_databaseSettingDialog, SIGNAL(editFinished(bool)), SLOT(switchToMainView(bool))); connect(m_databaseOpenWidget, SIGNAL(dialogFinished(bool)), SLOT(loadDatabase(bool))); connect(m_keepass1OpenWidget, SIGNAL(dialogFinished(bool)), SLOT(loadDatabase(bool))); + connect(m_opVaultOpenWidget, SIGNAL(dialogFinished(bool)), SLOT(loadDatabase(bool))); connect(m_csvImportWizard, SIGNAL(importFinished(bool)), SLOT(csvImportFinished(bool))); connect(m_fileWatcher.data(), SIGNAL(fileChanged()), this, SLOT(reloadDatabaseFile())); connect(this, SIGNAL(currentChanged(int)), this, SLOT(emitCurrentModeChanged())); @@ -1031,6 +1036,13 @@ void DatabaseWidget::switchToImportKeepass1(const QString& filePath) setCurrentWidget(m_keepass1OpenWidget); } +void DatabaseWidget::switchToImportOpVault(const QString& fileName) +{ + updateFilePath(fileName); + m_opVaultOpenWidget->load(fileName); + setCurrentWidget(m_opVaultOpenWidget); +} + void DatabaseWidget::switchToEntryEdit() { Entry* entry = m_entryView->currentEntry(); diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h index 7e012d2c35..73baae6b79 100644 --- a/src/gui/DatabaseWidget.h +++ b/src/gui/DatabaseWidget.h @@ -33,6 +33,7 @@ class DatabaseOpenWidget; class KeePass1OpenWidget; +class OpVaultOpenWidget; class DatabaseSettingsDialog; class Database; class DelayingFileWatcher; @@ -183,6 +184,7 @@ public slots: void performUnlockDatabase(const QString& password, const QString& keyfile = {}); void csvImportFinished(bool accepted); void switchToImportKeepass1(const QString& filePath); + void switchToImportOpVault(const QString& fileName); void emptyRecycleBin(); // Search related slots @@ -246,6 +248,7 @@ private slots: QPointer m_databaseSettingDialog; QPointer m_databaseOpenWidget; QPointer m_keepass1OpenWidget; + QPointer m_opVaultOpenWidget; QPointer m_groupView; QPointer m_entryView; diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 5f5f198785..3999ba689f 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -347,6 +347,7 @@ MainWindow::MainWindow() connect(m_ui->actionChangeDatabaseSettings, SIGNAL(triggered()), m_ui->tabWidget, SLOT(changeDatabaseSettings())); connect(m_ui->actionImportCsv, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importCsv())); connect(m_ui->actionImportKeePass1, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importKeePass1Database())); + connect(m_ui->actionImportOpVault, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importOpVaultDatabase())); connect(m_ui->actionExportCsv, SIGNAL(triggered()), m_ui->tabWidget, SLOT(exportToCsv())); connect(m_ui->actionLockDatabases, SIGNAL(triggered()), m_ui->tabWidget, SLOT(lockDatabases())); connect(m_ui->actionQuit, SIGNAL(triggered()), SLOT(appExit())); @@ -382,6 +383,7 @@ MainWindow::MainWindow() connect(m_ui->welcomeWidget, SIGNAL(openDatabase()), SLOT(switchToOpenDatabase())); connect(m_ui->welcomeWidget, SIGNAL(openDatabaseFile(QString)), SLOT(switchToDatabaseFile(QString))); connect(m_ui->welcomeWidget, SIGNAL(importKeePass1Database()), SLOT(switchToKeePass1Database())); + connect(m_ui->welcomeWidget, SIGNAL(importOpVaultDatabase()), SLOT(switchToOpVaultDatabase())); connect(m_ui->welcomeWidget, SIGNAL(importCsv()), SLOT(switchToCsvImport())); connect(m_ui->actionAbout, SIGNAL(triggered()), SLOT(showAboutDialog())); @@ -815,6 +817,12 @@ void MainWindow::switchToKeePass1Database() switchToDatabases(); } +void MainWindow::switchToOpVaultDatabase() +{ + m_ui->tabWidget->importOpVaultDatabase(); + switchToDatabases(); +} + void MainWindow::switchToCsvImport() { m_ui->tabWidget->importCsv(); diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index 7c10727f6b..950b02ee19 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -97,6 +97,7 @@ private slots: void switchToOpenDatabase(); void switchToDatabaseFile(const QString& file); void switchToKeePass1Database(); + void switchToOpVaultDatabase(); void switchToCsvImport(); void closePasswordGen(); void databaseStatusChanged(DatabaseWidget* dbWidget); diff --git a/src/gui/MainWindow.ui b/src/gui/MainWindow.ui index 004518eec9..0003c70159 100644 --- a/src/gui/MainWindow.ui +++ b/src/gui/MainWindow.ui @@ -200,6 +200,7 @@ &Import + @@ -603,6 +604,14 @@ Import a KeePass 1 database + + + 1Password Vault... + + + Import a 1Password Vault + + CSV file... diff --git a/src/gui/OpVaultOpenWidget.cpp b/src/gui/OpVaultOpenWidget.cpp new file mode 100644 index 0000000000..37b23d5287 --- /dev/null +++ b/src/gui/OpVaultOpenWidget.cpp @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2019 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "OpVaultOpenWidget.h" + +#include + +#include "core/Database.h" +#include "core/Metadata.h" +#include "format/OpVaultReader.h" +#include "gui/MessageBox.h" +#include "ui_DatabaseOpenWidget.h" + +OpVaultOpenWidget::OpVaultOpenWidget(QWidget* parent) + : DatabaseOpenWidget(parent) +{ + m_ui->labelHeadline->setText("Import 1Password database"); +} + +void OpVaultOpenWidget::openDatabase() +{ + OpVaultReader reader; + + QString password; + if (m_ui->checkPassword->isChecked()) { + password = m_ui->editPassword->text(); + } + + QDir opVaultDir(m_filename); + + QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); + m_db.reset(reader.readDatabase(opVaultDir, password)); + QApplication::restoreOverrideCursor(); + + if (m_db) { + emit dialogFinished(true); + } else { + m_ui->messageWidget->showMessage(tr("Read Database did not produce an instance\n%1").arg(reader.errorString()), + MessageWidget::Error); + m_ui->editPassword->clear(); + } +} diff --git a/src/gui/OpVaultOpenWidget.h b/src/gui/OpVaultOpenWidget.h new file mode 100644 index 0000000000..aed96caba6 --- /dev/null +++ b/src/gui/OpVaultOpenWidget.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2019 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_OPVAULTOPENWIDGET_H +#define KEEPASSXC_OPVAULTOPENWIDGET_H + +#include "gui/DatabaseOpenWidget.h" + +class OpVaultOpenWidget : public DatabaseOpenWidget +{ + Q_OBJECT + +public: + explicit OpVaultOpenWidget(QWidget* parent = nullptr); + +protected: + void openDatabase() override; +}; + +#endif // KEEPASSXC_OPVAULTOPENWIDGET_H diff --git a/src/gui/WelcomeWidget.cpp b/src/gui/WelcomeWidget.cpp index e5e6036725..35cce553e1 100644 --- a/src/gui/WelcomeWidget.cpp +++ b/src/gui/WelcomeWidget.cpp @@ -48,6 +48,7 @@ WelcomeWidget::WelcomeWidget(QWidget* parent) connect(m_ui->buttonNewDatabase, SIGNAL(clicked()), SIGNAL(newDatabase())); connect(m_ui->buttonOpenDatabase, SIGNAL(clicked()), SIGNAL(openDatabase())); connect(m_ui->buttonImportKeePass1, SIGNAL(clicked()), SIGNAL(importKeePass1Database())); + connect(m_ui->buttonImportOpVault, SIGNAL(clicked()), SIGNAL(importOpVaultDatabase())); connect(m_ui->buttonImportCSV, SIGNAL(clicked()), SIGNAL(importCsv())); connect(m_ui->recentListWidget, SIGNAL(itemActivated(QListWidgetItem*)), diff --git a/src/gui/WelcomeWidget.h b/src/gui/WelcomeWidget.h index 54827e0268..de49567793 100644 --- a/src/gui/WelcomeWidget.h +++ b/src/gui/WelcomeWidget.h @@ -41,6 +41,7 @@ class WelcomeWidget : public QWidget void openDatabase(); void openDatabaseFile(QString); void importKeePass1Database(); + void importOpVaultDatabase(); void importCsv(); protected: diff --git a/src/gui/WelcomeWidget.ui b/src/gui/WelcomeWidget.ui index a842ff41bd..3cc35c666b 100644 --- a/src/gui/WelcomeWidget.ui +++ b/src/gui/WelcomeWidget.ui @@ -126,6 +126,13 @@ + + + + Import from 1Password + + + diff --git a/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.cpp b/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.cpp index fac85c21e6..4ea30c1f6f 100644 --- a/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.cpp +++ b/src/gui/dbsettings/DatabaseSettingsWidgetBrowser.cpp @@ -242,12 +242,12 @@ void DatabaseSettingsWidgetBrowser::convertAttributesToCustomData() { if (MessageBox::Yes != MessageBox::question( - this, - tr("Move KeePassHTTP attributes to custom data"), - tr("Do you really want to move all legacy browser integration data to the latest standard?\n" - "This is necessary to maintain compatibility with the browser plugin."), - MessageBox::Yes | MessageBox::Cancel, - MessageBox::Cancel)) { + this, + tr("Move KeePassHTTP attributes to custom data"), + tr("Do you really want to move all legacy browser integration data to the latest standard?\n" + "This is necessary to maintain compatibility with the browser plugin."), + MessageBox::Yes | MessageBox::Cancel, + MessageBox::Cancel)) { return; } diff --git a/src/gui/masterkey/KeyComponentWidget.h b/src/gui/masterkey/KeyComponentWidget.h index 63079863ed..b73f881fc5 100644 --- a/src/gui/masterkey/KeyComponentWidget.h +++ b/src/gui/masterkey/KeyComponentWidget.h @@ -18,9 +18,9 @@ #ifndef KEEPASSXC_KEYCOMPONENTWIDGET_H #define KEEPASSXC_KEYCOMPONENTWIDGET_H +#include #include #include -#include namespace Ui { @@ -111,7 +111,7 @@ class KeyComponentWidget : public QWidget void componentRemovalRequested(); protected: - void showEvent(QShowEvent* event) override ; + void showEvent(QShowEvent* event) override; private slots: void updateComponentName(const QString& name); diff --git a/src/keys/FileKey.cpp b/src/keys/FileKey.cpp index da25ef4aef..9c3d41c1d6 100644 --- a/src/keys/FileKey.cpp +++ b/src/keys/FileKey.cpp @@ -24,10 +24,10 @@ #include -#include -#include #include #include +#include +#include QUuid FileKey::UUID("a584cbc4-c9b4-437e-81bb-362ca9709273"); diff --git a/src/keys/PasswordKey.cpp b/src/keys/PasswordKey.cpp index 2d0416af8f..77d2f276e7 100644 --- a/src/keys/PasswordKey.cpp +++ b/src/keys/PasswordKey.cpp @@ -19,9 +19,9 @@ #include "core/Tools.h" #include "crypto/CryptoHash.h" -#include #include #include +#include QUuid PasswordKey::UUID("77e90411-303a-43f2-b773-853b05635ead"); diff --git a/src/keys/YkChallengeResponseKey.cpp b/src/keys/YkChallengeResponseKey.cpp index 759d8d1bc8..03328ef747 100644 --- a/src/keys/YkChallengeResponseKey.cpp +++ b/src/keys/YkChallengeResponseKey.cpp @@ -32,9 +32,9 @@ #include #include +#include #include #include -#include QUuid YkChallengeResponseKey::UUID("e092495c-e77d-498b-84a1-05ae0d955508"); diff --git a/src/main.cpp b/src/main.cpp index 685a7a15c4..b6769661bb 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -82,8 +82,7 @@ int main(int argc, char** argv) QCommandLineOption helpOption = parser.addHelpOption(); QCommandLineOption versionOption = parser.addVersionOption(); - QCommandLineOption debugInfoOption(QStringList() << "debug-info", - QObject::tr("Displays debugging information.")); + QCommandLineOption debugInfoOption(QStringList() << "debug-info", QObject::tr("Displays debugging information.")); parser.addOption(configOption); parser.addOption(keyfileOption); parser.addOption(pwstdinOption); diff --git a/src/proxy/NativeMessagingHost.cpp b/src/proxy/NativeMessagingHost.cpp index 3c401e4c95..44b3ab7efa 100644 --- a/src/proxy/NativeMessagingHost.cpp +++ b/src/proxy/NativeMessagingHost.cpp @@ -35,7 +35,7 @@ NativeMessagingHost::NativeMessagingHost() setsockopt(socketDesc, SOL_SOCKET, SO_SNDBUF, reinterpret_cast(&max), sizeof(max)); } #ifdef Q_OS_WIN - m_running.store(true); + m_running.store(1); m_future = QtConcurrent::run(this, &NativeMessagingHost::readNativeMessages); #endif connect(m_localSocket, SIGNAL(readyRead()), this, SLOT(newLocalMessage())); @@ -56,7 +56,7 @@ void NativeMessagingHost::readNativeMessages() { #ifdef Q_OS_WIN quint32 length = 0; - while (m_running.load() && !std::cin.eof()) { + while (m_running.load() == 1 && !std::cin.eof()) { length = 0; std::cin.read(reinterpret_cast(&length), 4); if (!readStdIn(length)) { @@ -128,6 +128,6 @@ void NativeMessagingHost::deleteSocket() void NativeMessagingHost::socketStateChanged(QLocalSocket::LocalSocketState socketState) { if (socketState == QLocalSocket::UnconnectedState || socketState == QLocalSocket::ClosingState) { - m_running.testAndSetOrdered(true, false); + m_running.testAndSetOrdered(1, 0); } } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7665b0a7c9..a4584aeead 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -159,6 +159,9 @@ add_unit_test(NAME testdeletedobjects SOURCES TestDeletedObjects.cpp add_unit_test(NAME testkeepass1reader SOURCES TestKeePass1Reader.cpp LIBS ${TEST_LIBRARIES}) +add_unit_test(NAME testopvaultreader SOURCES TestOpVaultReader.cpp + LIBS ${TEST_LIBRARIES}) + add_unit_test(NAME testwildcardmatcher SOURCES TestWildcardMatcher.cpp LIBS ${TEST_LIBRARIES}) diff --git a/tests/TestCli.cpp b/tests/TestCli.cpp index 9574f6d32d..23af631e62 100644 --- a/tests/TestCli.cpp +++ b/tests/TestCli.cpp @@ -690,8 +690,9 @@ void TestCli::testKeyFileOption() listCmd.execute({"ls", "-k", keyFilePath, m_keyFileProtectedDbFile->fileName()}); m_stdoutFile->reset(); m_stdoutFile->readLine(); // skip password prompt - QCOMPARE(m_stdoutFile->readAll(), QByteArray("entry1\n" - "entry2\n")); + QCOMPARE(m_stdoutFile->readAll(), + QByteArray("entry1\n" + "entry2\n")); // Should raise an error with no key file. qint64 pos = m_stdoutFile->pos(); @@ -713,8 +714,7 @@ void TestCli::testKeyFileOption() m_stdoutFile->readLine(); // skip password prompt m_stderrFile->seek(posErr); QCOMPARE(m_stdoutFile->readAll(), QByteArray("")); - QCOMPARE(m_stderrFile->readAll().split(':').at(0), - QByteArray("Failed to load key file invalidpath")); + QCOMPARE(m_stderrFile->readAll().split(':').at(0), QByteArray("Failed to load key file invalidpath")); } void TestCli::testNoPasswordOption() @@ -724,8 +724,9 @@ void TestCli::testNoPasswordOption() QString keyFilePath(QString(KEEPASSX_TEST_DATA_DIR).append("/KeyFileProtectedNoPassword.key")); listCmd.execute({"ls", "-k", keyFilePath, "--no-password", m_keyFileProtectedNoPasswordDbFile->fileName()}); m_stdoutFile->reset(); - QCOMPARE(m_stdoutFile->readAll(), QByteArray("entry1\n" - "entry2\n")); + QCOMPARE(m_stdoutFile->readAll(), + QByteArray("entry1\n" + "entry2\n")); // Should raise an error with no key file. qint64 pos = m_stdoutFile->pos(); diff --git a/tests/TestMerge.cpp b/tests/TestMerge.cpp index 4d9aef2110..9fc353ddfc 100644 --- a/tests/TestMerge.cpp +++ b/tests/TestMerge.cpp @@ -1172,7 +1172,7 @@ void TestMerge::testCustomdata() QScopedPointer dbSource2(createTestDatabase()); m_clock->advanceSecond(1); - + dbDestination->metadata()->customData()->set("toBeDeleted", "value"); dbDestination->metadata()->customData()->set("key3", "oldValue"); @@ -1212,7 +1212,8 @@ void TestMerge::testCustomdata() QCOMPARE(dbDestination->metadata()->customData()->value("key1"), QString("value1")); QCOMPARE(dbDestination->metadata()->customData()->value("key2"), QString("value2")); QCOMPARE(dbDestination->metadata()->customData()->value("Browser"), QString("n'8=3W@L^6d->d.]St_>]")); - QCOMPARE(dbDestination->metadata()->customData()->value("key3"), QString("newValue")); // Old value should be replaced + QCOMPARE(dbDestination->metadata()->customData()->value("key3"), + QString("newValue")); // Old value should be replaced // Target is newer, no data is merged QVERIFY(!dbDestination2->metadata()->customData()->isEmpty()); @@ -1220,7 +1221,8 @@ void TestMerge::testCustomdata() QVERIFY(!dbDestination2->metadata()->customData()->contains("key2")); QVERIFY(!dbDestination2->metadata()->customData()->contains("Browser")); QVERIFY(dbDestination2->metadata()->customData()->contains("notToBeDeleted")); - QCOMPARE(dbDestination2->metadata()->customData()->value("key3"), QString("oldValue")); // Old value should not be replaced + QCOMPARE(dbDestination2->metadata()->customData()->value("key3"), + QString("oldValue")); // Old value should not be replaced } void TestMerge::testDeletedEntry() diff --git a/tests/TestOpVaultReader.cpp b/tests/TestOpVaultReader.cpp new file mode 100644 index 0000000000..af332fd322 --- /dev/null +++ b/tests/TestOpVaultReader.cpp @@ -0,0 +1,250 @@ +/* + * Copyright (C) 2019 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "TestOpVaultReader.h" + +#include "config-keepassx-tests.h" +#include "core/Database.h" +#include "core/Group.h" +#include "core/Metadata.h" +#include "core/Tools.h" +#include "crypto/Crypto.h" +#include "format/OpVaultReader.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +QTEST_GUILESS_MAIN(TestOpVaultReader) + +QPair* split1PTextExportKV(QByteArray& line) +{ + const auto eq = line.indexOf('='); + if (-1 == eq) { + qWarning() << "Bogus key=value pair: <<" << line << ">>"; + return nullptr; + } + auto k = QString::fromUtf8(line.mid(0, eq)); + const auto start = eq + 1; + auto v = QString::fromUtf8(line.mid(start), (line.size() - 1) - start); + return new QPair(k, v); +} + +QJsonArray* read1PasswordTextExport(QFile& f) +{ + auto result = new QJsonArray; + auto current = new QJsonObject; + + if (!f.open(QIODevice::ReadOnly)) { + qCritical("Unable to open your text export file for reading"); + return nullptr; + } + + while (!f.atEnd()) { + auto line = f.readLine(1024); + + if (line.size() == 1 and line[0] == '\n') { + if (!current->isEmpty()) { + result->append(*current); + } + current = new QJsonObject; + continue; + } + const auto kv = split1PTextExportKV(line); + if (kv == nullptr) { + break; + } + QString k = kv->first; + + const auto multiLine1 = line.indexOf("=\"\""); + const auto multiLine2 = line.indexOf("=\""); + const auto isML1 = -1 != multiLine1; + const auto isML2 = -1 != multiLine2; + if (isML1 or isML2) { + QStringList lines; + const int skipEQ = isML1 ? (multiLine1 + 3) : (multiLine2 + 2); + lines.append(QString::fromUtf8(line.mid(skipEQ))); + while (!f.atEnd()) { + line = f.readLine(1024); + const auto endMarker = line.indexOf(isML1 ? "\"\"\n" : "\"\n"); + if (-1 != endMarker) { + line[endMarker] = '\n'; + lines.append(QString::fromUtf8(line.mid(0, endMarker))); + break; + } else { + lines.append(QString::fromUtf8(line)); + } + } + auto v = lines.join(""); + (*current)[k] = v; + } else { + (*current)[k] = kv->second; + } + delete kv; + } + if (!current->isEmpty()) { + result->append(*current); + } + f.close(); + + return result; +} + +void TestOpVaultReader::initTestCase() +{ + QVERIFY(Crypto::init()); + + // https://cache.agilebits.com/security-kb/freddy-2013-12-04.tar.gz + m_opVaultPath = QString("%1/%2").arg(KEEPASSX_TEST_DATA_DIR, "/freddy-2013-12-04.opvault"); + m_opVaultTextExportPath = QString(m_opVaultPath).replace(".opvault", ".opvault.txt"); + + m_password = "freddy"; + + QFile testData(m_opVaultTextExportPath); + QJsonArray* data = read1PasswordTextExport(testData); + QVERIFY(data); + QCOMPARE(data->size(), 27); + delete data; + + m_categoryMap.insert("001", "Login"); + m_categoryMap.insert("002", "Credit Card"); + m_categoryMap.insert("003", "Secure Note"); + m_categoryMap.insert("004", "Identity"); + m_categoryMap.insert("005", "Password"); + m_categoryMap.insert("099", "Tombstone"); + m_categoryMap.insert("100", "Software License"); + m_categoryMap.insert("101", "Bank Account"); + m_categoryMap.insert("102", "Database"); + m_categoryMap.insert("103", "Driver License"); + m_categoryMap.insert("104", "Outdoor License"); + m_categoryMap.insert("105", "Membership"); + m_categoryMap.insert("106", "Passport"); + m_categoryMap.insert("107", "Rewards"); + m_categoryMap.insert("108", "SSN"); + m_categoryMap.insert("109", "Router"); + m_categoryMap.insert("110", "Server"); + m_categoryMap.insert("111", "Email"); +} + +void TestOpVaultReader::testReadIntoDatabase() +{ + QDir opVaultDir(m_opVaultPath); + + auto reader = new OpVaultReader(); + auto db = reader->readDatabase(opVaultDir, m_password); + QVERIFY2(!reader->hasError(), qPrintable(reader->errorString())); + QVERIFY(db); + QVERIFY(!db->children().isEmpty()); + + Group* rootGroup = db->rootGroup(); + QVERIFY(rootGroup); + + QFile testDataFile(m_opVaultTextExportPath); + auto testData = read1PasswordTextExport(testDataFile); + QVERIFY(testData); + + QMap objectsByUuid; + QMap> objectsByCategory; + for (QJsonArray::const_iterator it = testData->constBegin(); it != testData->constEnd(); ++it) { + QJsonObject value = (*it).toObject(); + auto cat = value["category"].toString(); + QVERIFY2(m_categoryMap.contains(cat), qPrintable(QString("BOGUS, unmapped category \"%1\"").arg(cat))); + + auto catName = m_categoryMap[cat]; + if (!objectsByCategory.contains(catName)) { + QList theList; + objectsByCategory[catName] = theList; + } + objectsByCategory[catName].append(value); + + QUuid u = Tools::hexToUuid(value["uuid"].toString()); + objectsByUuid[u] = value; + } + delete testData; + QCOMPARE(objectsByUuid.size(), 27); + + for (QUuid u : objectsByUuid.keys()) { + QJsonObject o = objectsByUuid[u]; + const auto e = db->rootGroup()->findEntryByUuid(u); + QVERIFY2(e, qPrintable(QString("Expected to find UUID %1").arg(u.toString()))); + + auto jsonTitle = o["title"].toString(); + QCOMPARE(jsonTitle, e->title()); + } + + for (QString& catName : m_categoryMap.values()) { + const auto g = rootGroup->findChildByName(catName); + QVERIFY2(g, qPrintable(QString("Expected to find Group(%1)").arg(catName))); + for (QJsonObject testEntry : objectsByCategory[catName]) { + auto uuidStr = testEntry["uuid"].toString(); + auto jsonTitle = testEntry["title"].toString(); + + QUuid u = Tools::hexToUuid(uuidStr); + const auto entry = g->findEntryByUuid(u); + QVERIFY2(entry, qPrintable(QString("Expected to find Group(%1).entry(%2)").arg(catName).arg(uuidStr))); + QCOMPARE(entry->title(), jsonTitle); + } + } +} + +void TestOpVaultReader::testKeyDerivation() +{ + OpVaultReader reader; + QDir opVaultDir(m_opVaultPath); + + // yes, the reader checks this too, but in our case best to fail early + QVERIFY(opVaultDir.exists()); + QVERIFY(opVaultDir.isReadable()); + + QDir defDir = QDir(opVaultDir); + defDir.cd("default"); + QFile profileJs(defDir.absoluteFilePath("profile.js")); + QVERIFY(profileJs.exists()); + + auto profileObj = reader.readAndAssertJsonFile(profileJs, "var profile=", ";"); + + QByteArray salt = QByteArray::fromBase64(profileObj["salt"].toString().toUtf8()); + unsigned long iter = profileObj["iterations"].toInt(); + const auto derived = reader.deriveKeysFromPassPhrase(salt, m_password, iter); + QVERIFY(derived); + QVERIFY(!derived->error); + + QByteArray encHex = derived->encrypt.toHex(); + QByteArray hmacHex = derived->hmac.toHex(); + delete derived; + + QCOMPARE(QString::fromUtf8(encHex), + QStringLiteral("63b075de858949559d4faa9d348bf10bdaa0e567ad943d7803f2291c9342aaaa")); + QCOMPARE(QString::fromUtf8(hmacHex), + QStringLiteral("ff3ab426ce55bf097b252b3f2df1c4ba4312a6960180844d7a625bc0ab40c35e")); +} + +void TestOpVaultReader::testBandEntry1() +{ + auto reader = new OpVaultReader(); + QByteArray json(R"({"hello": "world"})"); + QJsonDocument doc = QJsonDocument::fromJson(json); + QJsonObject data; + QByteArray entryKey; + QByteArray entryHmacKey; + QVERIFY(!reader->decryptBandEntry(doc.object(), data, entryKey, entryHmacKey)); +} diff --git a/tests/TestOpVaultReader.h b/tests/TestOpVaultReader.h new file mode 100644 index 0000000000..54d096e437 --- /dev/null +++ b/tests/TestOpVaultReader.h @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2019 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef TEST_OPVAULT_READER_H_ +#define TEST_OPVAULT_READER_H_ + +#include +#include + +class TestOpVaultReader : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + void testReadIntoDatabase(); + void testBandEntry1(); + void testKeyDerivation(); + +private: + // absolute path to the .opvault directory + QString m_opVaultPath; + + /* + * Points to the file made by using the 1Password GUI to "Export all" + * to its text file format, which are almost key=value pairs + * except for multi-line strings. + */ + QString m_opVaultTextExportPath; + QString m_password; + QMap m_categoryMap; +}; + +#endif /* TEST_OPVAULT_READER_H_ */ diff --git a/tests/data/freddy-2013-12-04.opvault.txt b/tests/data/freddy-2013-12-04.opvault.txt new file mode 100644 index 0000000000..d36a78f5ae --- /dev/null +++ b/tests/data/freddy-2013-12-04.opvault.txt @@ -0,0 +1,427 @@ +uuid=E0D293D29B10483F8DFDAC72ED0BE5C0 +title=Wendy's passport +category=106 +ainfo=ZZ200000 +scope=Default +autoSubmit=Default +type=Passport +issuing country=Canada +number=ZZ200000 +full name=Wendy Appleseed +sex=female +nationality=Canada +issuing authority=Home Office +date of birth=359100000 +place of birth=Yellowknife, NT +issued on=954828000 +expiry date=1585893600 + +uuid=F2DB5DA3FCA64372A751E0E85C67A538 +title=A note with some attachments +category=003 +ainfo=This note has two attachments. +scope=Default +autoSubmit=Default +notesPlain=This note has two attachments. + +uuid=FF445AB1497241A28812363154E1A738 +title=Johnny Appleseed Society +category=105 +ainfo=Wendy Appleseed +scope=Default +autoSubmit=Default +website=http://www.urbana.edu/resources/community/johnny-appleseed/appleseed-society.html +member name=Wendy Appleseed +expiry date=2625 +member ID=123456 +password=B8HqCdCMAY8KxJqg + +uuid=2A632FDD32F5445E91EB5636C7580447 +title=Skype +category=001 +ainfo=WendyAppleseed +scope=Default +autoSubmit=Default +tags=Sample +website=https://secure.skype.com/account/login?message=login_required +username=WendyAppleseed +password=dej3ur9unsh5ian1and5 + +uuid=1C7D72EFA19A4EE98DB7A9661D2F5732 +title=Wendy's driver's license +category=103 +ainfo=D6101-40706-60905 +scope=Default +autoSubmit=Default +notesPlain=Picture really doesn't look like Wendy +full name=Wendy Appleseed +address=5-150 Hollidge Blvd Suite 150 +date of birth=359100000 +sex=female +height=175cm +number=D6101-40706-60905 +license class=G2 +conditions / restrictions=J +state=Ontario +country=Canada +expiry date=2515 + +uuid=67979020CCA54120BAFA2742C3F23F2B +title=Social Security +category=108 +ainfo=Wendy Appleseed +scope=Default +autoSubmit=Default +name=Wendy Appleseed +number=555-55-1234 + +uuid=372E1D51AA1D44CB9F17D8AA70ADA9A6 +title=example.com +category=110 +ainfo=wappleseed +scope=Default +autoSubmit=Default +notesPlain=I should attach an SSH key, but maybe later. +URL=example.com +username=wappleseed +password=My4scQNoFw8JcvN +section=Admin Console +section=Hosting Provider +name=Example Hosting provider +website=http://services.example.com + +uuid=8445A23B5740455DA360FEA379C3CC90 +title=Tim Hortons +category=107 +ainfo=Tim Hortens +scope=Default +autoSubmit=Default +company name=Tim Hortens +member name=Wendy Appleseed +member ID=12123123 +PIN=Y7s8WaRGJBAz +section=More Information + +uuid=A2D44483145F4B41A849FE5FEA4B504D +title=Snipe Hunting License +category=104 +ainfo=Wendy Appleseed +scope=Default +autoSubmit=Default +notesPlain=""I went out and shot the maximum the game laws would allow. +Two game wardens, seven hunters, and a cow. + +They took away my license, the worst punishment I ever endured. +Turns out there was a reason, +Cows were out of season, +And one of the hunters wasn't insured."" +full name=Wendy Appleseed +expires=1672470000 +approved wildlife=North American Snipe +maximum quota=Two game wardens, seven hunters, and a cow + +uuid=FD2EADB43C4F4FC7BEB35A1692DDFDEA +title=Email Account +category=111 +ainfo=wendy.appleseed@me.com +scope=Default +autoSubmit=Default +type=imap +username=wendy.appleseed@me.com +server=imap.mail.me.com +port number=993 +password=iINe4uig8suLny +security=SSL +auth​ method=password +section=SMTP +SMTP server=smtp.mail.me.com +port number=587 +username=wendy.appleseed@me.com +password=iINe4uig8suLny +security=TLS +auth​ method=password +section=Contact Information + +uuid=EC0A40400ABB4B16926B7417E95C9669 +title=Bank of America +category=001 +ainfo=WendyAppleseed +scope=Default +autoSubmit=Default +tags=Sample, Personal +website=https://www.bankofamerica.com/ +previousPassword1=speg5nu5di1mol4niev9 +username=WendyAppleseed +password=reTDx8KHhW8eAc + +uuid=E482B70C038D4DD78A0940728FA737BF +title=Chase VISA ***4356 +category=002 +ainfo=1234 *********** 4356 +scope=Default +autoSubmit=Default +tags=Sample +notesPlain=Sample data, not a real credit card number. +cardholder name=Wendy Appleseed +type=visa +number=1234 5678 9012 4356 +verification number=543 +expiry date=201905 +section=Contact Information +issuing bank=Chase +phone (toll free)=1-888-888-8888 +website=www.chase.com +section=Additional Details +PIN=000 +credit limit=$5,000.00 +cash withdrawal limit=$1,000.00 +interest rate=29.9% + +uuid=D1820AA8CB534AC6A4B5A2C0263FD3B2 +title=What is a Secure Note? +category=003 +scope=Default +autoSubmit=Default +tags=Sample +notesPlain=" +Secure Notes enable you to keep any information in freeform text format while keeping it safely encrypted along with the rest of your 1Password data. + +Just like the website passwords and credit card numbers you can store in 1Password, you (or someone else!) cannot get to your Secure Notes without entering your keychain’s Master Password. + +This provides encrypted storage for your stuff that doesn’t fit into other areas of 1Password. +" + +uuid=D8F79F17D6384808848B213EB4946ECA +title=The Unofficial Apple Weblog +category=001 +ainfo=WendyAppleseed +scope=Default +autoSubmit=Default +tags=Sample +website=http://www.tuaw.com +username=WendyAppleseed +password=tiac1nut2jab1eiv2oc5 + +uuid=F78CEC04078743B6975511A6FDDBED7E +title=1Password +category=100 +ainfo=3.0 +scope=Default +autoSubmit=Default +tags=Sample, Business +notesPlain="This is a sample software license. + +1Password securely keeps track of online logins, generates strong passwords, enters personal and credit card information with one click, protects from fishing attacks, and more! +" +version=3.0 +license key=1PW3-0000-000000-0000 +section=Customer +licensed to=Wendy Appleseed +registered email=wendy@appleseed.com +section=Publisher +download page=http://agilebits.com/downloads +publisher=AgileBits +website=http://1password.com +support email=support@agilebits.com +section=Order + +uuid=F5F099B210F248348E22934DDC3338B2 +title=TextExpander +category=100 +ainfo=1.3 +scope=Default +autoSubmit=Default +tags=Sample +notesPlain="This is a sample software license. + +TextExpander saves you countless keystrokes with customized abbreviations for your frequently-used text strings and images." +version=1.3 +license key=TEXTEXP001-1234-ABCD-5678-EFGH +section=Customer +licensed to=Wendy Appleseed +registered email=wendy@appleseed.com +section=Publisher +download page=www.smileonmymac.com/TextExpander/download.html +publisher=Smile On My Mac, LLC +website=www.smileonmymac.com +retail price=29.95 +support email=support@smileonmymac.com +section=Order + +uuid=F3707FA58EA7480884BC6A662658E039 +title=Business +category=004 +ainfo=Wendy Appleseed +scope=Default +autoSubmit=Default +tags=Business, Sample +section=Identification +first name=Wendy +last name=Appleseed +sex=female +birth date=361778400 +occupation=Customer Relations +company=AgileBits +department=Customer Care +job title=Manager +section=Address +default phone=(555) 555-5678 +cell=(555) 555-1234 +business=(555) 555-5678 +section=Internet Details +username=WendyAppleseed +reminder question=What's your favorite application? +reminder answer=1Password +email=support@agilebits +website=www.agilebits.com +forum signature=1Password — Never forget your password again. + +uuid=F7883ADDE5944B349ABB5CBEC20F39BE +title=MobileMe +category=001 +ainfo=wendy.appleseed@me.com +scope=Default +autoSubmit=Default +tags=Sample +website=https://www.icloud.com/ +notesPlain=Sample MobileMe account. +username=wendy.appleseed@me.com +password=iINe4uig8suLny +Member name=wendy.appleseed +iDisk Storage=10GB + +uuid=4E36C011EE8348B1B24418218B04018C +title=Company's FTP +category=001 +ainfo=admin +scope=Default +autoSubmit=Default +tags=Sample, Business +website=ftp://ftp.dreamhost.com +notesPlain=Sample FTP account. +username=admin +password=auj7r5?u61ww +path=/home/product/secert +section=Provider + +uuid=5ADFF73C09004C448D45565BC4750DE2 +title=Tumblr +category=001 +ainfo=wendy@appleseed.com +scope=Default +autoSubmit=Default +tags=Sample, Social +website=http://www.tumblr.com/login +email=wendy@appleseed.com +password=vow6wem2wo + +uuid=72366D161D9E43D98E58EB801DAD1EF8 +title=Last.fm +category=001 +ainfo=WendyAppleseed +scope=Default +autoSubmit=Default +tags=Sample +website=https://www.last.fm/login +username=WendyAppleseed +password=dowg1af5kam7oak9at + +uuid=D06307ADA44C4031BA2FF4B174DE79CB +title=CapitalOne MasterCard ***3456 +category=002 +ainfo=1234 *********** 3456 +scope=Default +autoSubmit=Default +tags=Sample, Business +notesPlain=Sample data, not a real credit card number. +cardholder name=Wendy Appleseed +type=mc +number=1234 5678 9012 3456 +verification number=123 +expiry date=201411 +section=Contact Information +issuing bank=CapitalOne +phone (toll free)=1-888-888-8888 +website=capitalone.com +section=Additional Details +PIN=234 +credit limit=$8,000 +cash withdrawal limit=$2,000 +interest rate=19.8% + +uuid=27DCFA2810B24083A3ECC7CEABC7C0A9 +title=Orders +category=102 +ainfo=10.0.1.50 +scope=Default +autoSubmit=Default +tags=Sample +notesPlain=Sample database account. +type=mysql +server=10.0.1.50 +port=3066 +database=orders_production +username=orders_app +password=tgOhmpU9HgC5Hz + +uuid=358B7411EB8B45CD9CE592ED16F3E9DE +title=YouTube +category=001 +ainfo=wendy@appleseed.com +scope=Default +autoSubmit=Default +tags=Sample, Social +website=http://www.youtube.com/login?next=/index +username=wendy@appleseed.com +password=snaip5uc5keds7as5ocs + +uuid=468B1E24F93B413DAD57ABE6F1C01DF6 +title=Dropbox +category=001 +ainfo=wendy@appleseed.com +scope=Default +autoSubmit=Default +tags=Sample +website=https://www.getdropbox.com/ +email=wendy@appleseed.com +password=vet4juf4nim1ow6ay2ph + +uuid=0EDE2B13D7AC4E2C9105842682ACB187 +title=Personal +category=004 +ainfo=Wendy Appleseed +scope=Default +autoSubmit=Default +tags=Sample, Personal +section=Identification +first name=Wendy +last name=Appleseed +sex=female +birth date=359100000 +occupation=Customer Relations +company=AgileBits +department=Customer Care +job title=Manager +section=Address +default phone=(555) 555-4321 +home=(555) 555-4321 +cell=(555) 555-1234 +section=Internet Details +username=WendyAppleseed +reminder question=What's your favorite application? +reminder answer=1Password +email=wendy@appleseed.com +skype=WendyAppleseed +AOL/AIM=WendyAppleseed76 + +uuid=13C8E12AC8E54B1F873BAB0824E521BC +title=Hulu +category=001 +ainfo=wendy@appleseed.com +scope=Default +autoSubmit=Default +tags=Sample +website=http://www.hulu.com/ +username=wendy@appleseed.com +password=frirp7i1ob7wig4d + diff --git a/tests/data/freddy-2013-12-04.opvault/default/1C7D72EFA19A4EE98DB7A9661D2F5732_3B94A1F475014E27BFB00C99A42214DF.attachment b/tests/data/freddy-2013-12-04.opvault/default/1C7D72EFA19A4EE98DB7A9661D2F5732_3B94A1F475014E27BFB00C99A42214DF.attachment new file mode 100644 index 0000000000..04d6a3ded9 Binary files /dev/null and b/tests/data/freddy-2013-12-04.opvault/default/1C7D72EFA19A4EE98DB7A9661D2F5732_3B94A1F475014E27BFB00C99A42214DF.attachment differ diff --git a/tests/data/freddy-2013-12-04.opvault/default/2A632FDD32F5445E91EB5636C7580447_8FA293F2B001459D8F8F78C21E6BF9F6.attachment b/tests/data/freddy-2013-12-04.opvault/default/2A632FDD32F5445E91EB5636C7580447_8FA293F2B001459D8F8F78C21E6BF9F6.attachment new file mode 100644 index 0000000000..c56f141b7f Binary files /dev/null and b/tests/data/freddy-2013-12-04.opvault/default/2A632FDD32F5445E91EB5636C7580447_8FA293F2B001459D8F8F78C21E6BF9F6.attachment differ diff --git a/tests/data/freddy-2013-12-04.opvault/default/E0D293D29B10483F8DFDAC72ED0BE5C0_898CD4CD00164930A2E15B159CE65E8F.attachment b/tests/data/freddy-2013-12-04.opvault/default/E0D293D29B10483F8DFDAC72ED0BE5C0_898CD4CD00164930A2E15B159CE65E8F.attachment new file mode 100644 index 0000000000..77282dab17 Binary files /dev/null and b/tests/data/freddy-2013-12-04.opvault/default/E0D293D29B10483F8DFDAC72ED0BE5C0_898CD4CD00164930A2E15B159CE65E8F.attachment differ diff --git a/tests/data/freddy-2013-12-04.opvault/default/F2DB5DA3FCA64372A751E0E85C67A538_23F6167DC1FB457A8DE7033ACDCD06DB.attachment b/tests/data/freddy-2013-12-04.opvault/default/F2DB5DA3FCA64372A751E0E85C67A538_23F6167DC1FB457A8DE7033ACDCD06DB.attachment new file mode 100644 index 0000000000..a1c1dfd5a2 Binary files /dev/null and b/tests/data/freddy-2013-12-04.opvault/default/F2DB5DA3FCA64372A751E0E85C67A538_23F6167DC1FB457A8DE7033ACDCD06DB.attachment differ diff --git a/tests/data/freddy-2013-12-04.opvault/default/F2DB5DA3FCA64372A751E0E85C67A538_AFBDA49A5F684179A78161E40CA2AAD3.attachment b/tests/data/freddy-2013-12-04.opvault/default/F2DB5DA3FCA64372A751E0E85C67A538_AFBDA49A5F684179A78161E40CA2AAD3.attachment new file mode 100644 index 0000000000..78bcad1102 Binary files /dev/null and b/tests/data/freddy-2013-12-04.opvault/default/F2DB5DA3FCA64372A751E0E85C67A538_AFBDA49A5F684179A78161E40CA2AAD3.attachment differ diff --git a/tests/data/freddy-2013-12-04.opvault/default/FF445AB1497241A28812363154E1A738_16684B74F26145169EC03B950DC68E95.attachment b/tests/data/freddy-2013-12-04.opvault/default/FF445AB1497241A28812363154E1A738_16684B74F26145169EC03B950DC68E95.attachment new file mode 100644 index 0000000000..badcd42358 Binary files /dev/null and b/tests/data/freddy-2013-12-04.opvault/default/FF445AB1497241A28812363154E1A738_16684B74F26145169EC03B950DC68E95.attachment differ diff --git a/tests/data/freddy-2013-12-04.opvault/default/band_0.js b/tests/data/freddy-2013-12-04.opvault/default/band_0.js new file mode 100644 index 0000000000..4e62ab08d8 --- /dev/null +++ b/tests/data/freddy-2013-12-04.opvault/default/band_0.js @@ -0,0 +1 @@ +ld({"0C4F27910A64488BB339AED63565D148":{"uuid":"0C4F27910A64488BB339AED63565D148","category":"099","o":"b3BkYXRhMDEIAAAAAAAAAMQDerODSnrtEVkZHp0tO5qokNWe+77F7yjsHcCvBEdxYL9DPSUuPV4FDv1F4E3VXWoY4BBYZrm8G3IUekJhL3E=","hmac":"SP8xH51\/qYBOoiCzKWDDmyNluCdPiP4bzOPR2+eTTh0=","updated":1386214150,"trashed":true,"k":"6MnmUT7fNchO0lIDNYGITOAO0cubw8Qsad1dEBZFCUSXrUOR7IkFUwddSA8QBJTH7P7iJytKB00KclFRNR\/zf+AC+VD6aCQiznj1zx8uKoxG9Wv1v4YsnH95NbC8UvRxCn+XA+6WRZII2kWN10IN9w==","d":"b3BkYXRhMDECAAAAAAAAAO\/uG7Zs+1OHwr82PByk3Scrlb7f1QGT0EThuhBdj50T3qyvt\/uoxBb8APNUDjTV81dTjBoNvLCpvuAEyQgdmlY=","created":1386214097,"tx":1386214431},"0EDE2B13D7AC4E2C9105842682ACB187":{"category":"004","k":"A4kIEzE7ypBL5lTeguPoFPlD21Uv5akEeosVZQ8u98BIBnMqScGmLJTlCoAgvfn+1YjgxQX3vZJTMDUcmt678UuBVMMehVg87Pys4hMFLNjwhhJaFGSRpSfWDlVB6Rb5PGrkkIDZBPkK4kFbYMN1tg==","updated":1325483949,"tx":1373753421,"d":"b3BkYXRhMDEYCAAAAAAAAOX\/h3yw\/qsvS8loinC\/IeaownXcDlKuIxDWIhQZJ+wZSmV43jY7n4iCxG6Fg8qIQm+l1Tu7M3oTOwsRREhbqqEsQHnJSts32+nxh5K9hgcCKYfKMbPB13pQlWamGUMX7tCLno8w+8XQnI8izoTE75klF8z+jF+LjGK3IhQ1wm4hCqWje0j9brjGId8KPrQoVIorzROtYfBKYjEMu5bvhCI62KWUbyBodAKoYdnHK7bSs01GvY\/tPyXPZ4qyQ7qrou5uDJNclYQ715Ajbm4sIDbfW0qtrYeSA6+uFT6ClxDccc9+RvW40vgaZekx8yEa6ytrZ744JlnKGdYQrecV8WDjIiVzgZrTV9GthzPzrUb8JUA\/naBufQNlQVISvnFQUXM+S+E8B+FR8OJDY0g0VNMkQ4BxeYyAlZB9395DcJfrzu7378PSy0egyNoWKM8PZH\/HHYhUlWMWMkP90r+iIIFnp9XpAXyetSUfIHV\/nRP0wBxvgBtcz7BBsjMwHa965K5KOQxZm9Nb9118IaUiXfG4jU65M1keJBa4fOUlka7QK8Q9cYHQZNY86PMrdYjDvG6YhL\/aNjQ+oWUpvtyZnFAdwe7+5Zw4TuAKXf2SiWcKzkGfbNLZxJJY95eVPfv9lSYrZay4LZKtD8WP\/X6G8w8+NlAMESiZkwhx+w33HgTzVIbLqvTFcIAgXbcCmNCfmIW+VlnvXtUZCjs9rI0KC3rXLE6OUBo3mJTy1+2iFHk3ed1gdlDWX0mWe4+CI\/4Q1pAxsXnqATgLM8dep6fySXKYXf44mj0t03jQXnm8t02FPK7lhPjjGddntqz5idk1jVFp\/wfDB4j+E9EvszWJyP14PYZRIyIOS67wWs0mKHeLdkoOeEGxFf\/h5IdDEOxm4xe\/+8ZfzTjPPrKX27XJlT\/XbVShvLbru\/ToP8qLqaBq\/7c6tmKhUvOLg0M5WX7oEEq4Rqk2qBaRmNSfd9ke\/AECrVzlqTRubVgfA95G7wAOERT6aa0wJZ7JxFj1ynQVgFrSTHyNSeW9n42TWGO\/\/6w\/hmDv5jJ\/IEJlc2eW0wPRBRCjWE\/cz384nVU0d0ixucYzLlsxyXn1GzseMa+u0WqyKHvJrXCj+6L1GKokp42yLDJVg6WO7EiS+sVcc\/WnTOdxfh8WMrexEfQS3jlL+d1IHt58c3kfjxhTX48Tlhj9Ih3dWW5xwK5JiVM+Lumk+IKhpEHpIu46YOQfbyK6ETqHNdKYiBOQByjCPq\/MftDPXKH8bAyOe5pMH89SYs8Y0TdSqIRsSyVWBKYkdcRp\/bpMB0CRJcSapkpQSMDOpioE6PkIuhGXNENT8EDBlM477yPxorYxHxLdzusOsxzzRBgc120ezJQALoWTgCy54LMYQlNj4Xajw00V8EnyVaKD7zfkhvqo6bTveR89mNQL213bGeOvEbOTDizNYgWpFGJb8WgD7Ji+Z6qd0vfBm17r0A2SNCrtHG8Fp1q+Qh0DR+94nLdN5R0Ann7LTgLbi2LhzQyr9KdBlLA73SRQFvaMsmPoopO46Bf21LbY3IeVjHDRa8253zs2oASHrTNFnki7j1byyVZQDRQwMoAXJnNZre+CzhCYdSA8pERPKihODRpXpq4NSSitWKMAKIqqoDYWzrZmBiTLFwF0SxmYGpkTn6AdMjexp1Xayx+7NIOTui6yaUmIf\/MEm7hfOfN0SHZOLYA3FMOa8mLCLU4qdnQTZlK\/v5QLcBTy2WB\/RZlbyX3nXb2ooE4kheMA6dtPI+OnBSIkbZ8nRzJx1eNfuOqpxS8H8M1oQ96I1g1LyX58VDjlHVcgrpVXcta4uXb4y9ZbWCiS5C3DEPlx9FAb3HwKgsvnArkNP8k0QIvX6w2xKAOv03bafVETG3LUh5OhQDKZkoRjR6sOcBstWLoyL8yJKj6YnoVNcLdHW3pvlbaKvETb\/Q5y1AE81XYADWWbMQo8AHg7lMpGyG98KuNr3WI9X1T7GyJAAaSXIYDepa\/l9icfqEUPyYyxxOUBjTjtdhSHFGCwoiu5rnGcA2Nq9v44ZGOGfy8tLzbzoMmZf3+qjmoSgmCRDTgCuWuSDixEfo65BNC9sRYPgf0JyYrnW9oBB\/\/g4lkzEv5B1V6leXQonJQ6vPKJbOWPDsZ1R8\/3\/dxPoNOjfp0J59ndoboOX5E52meVIQ99GqAAmLSWCNU76IZWsnGQHBmsaZqgHjE5E86D21rSVgOaKzN4ngvXd5fbaJn7zVvaQwh26uBT5vaTtZAc7UubBzj5FrnXC0j8Tha6nAQ4ZYkqhIQK\/FjWgpnF61D3v0TYwECNQU5xNOaSGaS4jMsrX77PnrNUnAq7Zc3ainZtZ1fK9A0UevqonpqkH3RDC1r5QcAU+aLTV4AyG50F16KMgv\/Hkib\/GoY67qO+3IJuYXPdhjHRgZajl3XC70d9Agw0uMEFhhvhaEEJ6hL6qKXDzQ\/CjddIiz2l2tb+7nnugCggc516CXoGQIkTEjS5vBeAqkhtcyBS3F\/W4toATCIZPPm8U1E7Q2tURWA9P+lKPoOvFxGLANTVh6BxiasOMKes8IH\/6E3umpV5ajzcZYFeoNDrUcYe0nXRbfOnhM9VyuIcoJnCfJHZLXJ1MUCdmht5sSy78SVHI8ngwOjukM60fHK4mqjHL6qqexVa0+7N\/iKNdF4m4\/Fpx5CKoy11nDEhAq15MrYk775hs98hRLX\/h+WPccbwxX3+iDVLiLrFoujVLbzKg1\/ZqP5NQmEuN3hi27rA6j6kyPOs5lxXqG6EgDBGVLyeFlacXK1tC1ELuW4\/HlVGC0GLACo7x1OfU3VK+y1efUrSTTzgZn4=","hmac":"NFYnSILBYIuaRNngAmgenlKVIzQjrNI58924O9wVtP0=","created":1325483949,"uuid":"0EDE2B13D7AC4E2C9105842682ACB187","o":"b3BkYXRhMDFSAAAAAAAAAFx\/NqIo8EXowE0JkyOXYU9TwZBTupG5WKRVaYrA\/nU6Jy2xC2eyZV0SGmRVS8yt0A0eRVEBXGww2UV928lrUYGpT62kMa54yPHQ6PJ\/SBw6BITIoZqX91ohdcm+vUDDwkoNx4Vm+0VMFkBHRnAtT+cavKUMMmjdWrQ+0rEoWIVtZF47tOOUhh6HdGiY43ihsA=="}}); \ No newline at end of file diff --git a/tests/data/freddy-2013-12-04.opvault/default/band_1.js b/tests/data/freddy-2013-12-04.opvault/default/band_1.js new file mode 100644 index 0000000000..2743389b67 --- /dev/null +++ b/tests/data/freddy-2013-12-04.opvault/default/band_1.js @@ -0,0 +1,24 @@ +ld({ + "13C8E12AC8E54B1F873BAB0824E521BC": { + "category": "001", + "created": 1325483949, + "d": "b3BkYXRhMDF8AQAAAAAAAIj2+ycIIdHiuA8R2GDHcmD/kq9Bski/xVY/MzB5rKTOf2Ok7u9iGyq52/H02zob8xQAlMgVgCT3b6ZCkdNCiDM9G0Io+cOC1c9Z6KwY+AWjAf6N2gUdVthpHGSjSWHeQg2I+B9rZw8G+5hDWoKCNkz59sHLDVzb1utpTX/yqG//rBwdReGMcsMLdJ5i5z6rCNxDOzauJsPOmdND6Yl8qN8biW1Zi69j9yecKlGKbZGnkJ/BLGcyATw+kFTUjSIqpGQuJjRil25+iSyOQIRyaFZuI8LY0VzNwDXt630uVw4FCG/BlWDEFMxWD2MByKVFyMCXb/4jyL5EatoSfuUfcgUNjzcxMUDkZ5arCDy3nkycKUnnYEY/8mxw4TB8FmQNjtDcSuV+CxZXtEdnkQtxNh/j8DIFpxBqDcKmEkXDFkoBzz4JMttkJI+VUAH0rt6xFHEQBIrVMJiqc1oCLBpDCM2ttz5Bex687+zjo1PkqNC7rNKz5pc+2oT34i56z9BOSCUJNnPu3AbXqjTodWi/t9uQ8XbzozvLHgbz/YX4CwcjvNShZUywWScls8QwrCyHvQ==", + "hmac": "rBcqgatstCubnHbtCS4hTZ3iyh6Jx8c1pxJCwXy2dAs=", + "k": "Q/2/LDLfd1NAkd0nTzKgs3WlDvPtLU4iZ8RjpPCD4BCcuosWxud2r1vz07B7T2glcdW1Wm0eK1jGaJsSZ1mZARiUfkY8yyFjKHaw5K/S+TYHV2ypQaZwIH+qs9qpzRKgTIriMK6ZfWHOJAMPrB991A==", + "o": "b3BkYXRhMDGLAAAAAAAAAPKPZ8R5CHeY7YrgLUdA5b3Ay63vr9yrKHxyHmBgeHCmSQVM5RwqwEVQrbuNDNqQxekF6gKL1cB/P63ZSMfjYVtRGomifY3WBNsjbf8UYffI5mb+yOaKB9U6m300fbkVQshrVWTSwibhZe0nl6V+OqqyMJ1lWDRSt5fnuYvsDBUlggQ5JyZTGTadGAwpi12LihW/hpSEqQSoKX8KTkHivnTP47TETjYqTdP8XBUBQqz9XXb5R2o3GcRrID642HAzgg==", + "tx": 1373753421, + "updated": 1325483949, + "uuid": "13C8E12AC8E54B1F873BAB0824E521BC" + }, + "1C7D72EFA19A4EE98DB7A9661D2F5732": { + "category": "103", + "created": 1370114995, + "d": "b3BkYXRhMDEKAwAAAAAAAKRrSzqtu6qyKtAvRqceTqZtWt2ehX/OlzSgl8+vLzoU0FjWtb6pTvUZd+YexvO/1GwwaI28vEWHiaZoBsXMWjJf7gdJkW7vEZyXvuXnpb7aIy7teoyvZz8t3xwugKIZ6pkdqoY6af+qRpWpIZHeKMfOX/2RCuh2V9c4tpO1ZfVqHHDY68yL35SknbRxS4oLdGASa8WiENbLdl4uWZhgEqkQsB0HqOACRKilT8+/TFC5Nhf7zKKIZk3b6mSgGmhgTshUs0VQG/IyvjN3GVfI6Q9cZyEMItow2XVauVAsLMry8x1qALt+C5yTmly2eAIhzh5EHgEE8IHGyYO8qHV0S/rwdAWqUlljVQ3jzxwpPkaBq4RM8zLiJ0he5scOSV/qxSLvBy2OJ4lx9tpuiehKKI0/kRjTXgirsEA4O0lMBn/J3jNdAco4DygSD8OUzhA0IZEz5TWv1UCrMQ0QVc7ljlQLb8H5KAZ4xIue5Wd5FPi+7SVk6VBhJZFZBtssy23tklpmIxvlrt9axmSgLxEMUuTSo7Hecexh5+/1LVQeDtHjOXA6XrSqcFkr1t2Q94mXa+hVtLLPCnf8DYSgdUMevMgNqgKi/TWh64GqoPvxx+HnrBLH4WEIpBZhgIKRNuww9vkMaAYw+RtpPMcZzxenJ5htM8ep9WRfwYg+Mn6yCijYZgQseZFnz5EAqTzXx1CzP/vPK6IQlGmZy0kte8VLPbngSMaUoQFMnlQNTZhHwm85N+wdvDVZStas1oFUUtHLLSt2KCPMwVmuoiFevO6Zlstys3uy9ATLOXiRTP3MBEu5D1ahC2+Mvi0FDtf+YSi9UbbCBF/4/gNt3eSveNBz6q9LsBLTdCMVz/HVjQpn350irDKr8+D+6PLx/yisia0L4kNKUOIgRATpIGgODaBDjslZRjntAzC9eRT52hbbSyvAsTgNnsriBMXE4UimwwHe5RxPBOshuHrCnHjljqYVdgd3UrrTWMwQSorEom3i2kyryOEWrlZGCz/aL2D7T0Mw1SLxDJvfkVDBhX4wdSLQXXa6smgyRagvbxFzx9r3fp5zLJuwzc9aS1tKB2Pzvj34xTcTl9lzqzDtiMNkNQLYkHg=", + "hmac": "/wRZl/I6nGCLlYVALC2sOHr7GTLXW8PViyX+S37Gnsc=", + "k": "NaCkXC9d/ohnFWkalmyV/CfGxg1f9JqCLOdN96hnybygx4lPuV7d+XOhNkpUybQwtzCoKwGVPkp6IqOfa0s4aIjxbmHZ7JKX4YolqyU/IJRIJK+zVFXKdkH4jvSnf9WLPw5odKypNLAkWwmz0rGNuw==", + "o": "b3BkYXRhMDFHAAAAAAAAAAz2opVfQ3X6CoxN8yqzudxCe4dcegs++FcJQKDDzGY6OsA/3yP3Duoj6lbDcAkFfZ5s7OGg7HqmiIh8q+MhMUWFjM20cstKN4ghssKJzaSJgYaA7vj3OQj7H0/Y+/frnfy8E1jxpVim3sdihUcw8TbzGHgLATwIsxdQCK+KqO/L", + "tx": 1373755270, + "updated": 1373755270, + "uuid": "1C7D72EFA19A4EE98DB7A9661D2F5732" + } +}); \ No newline at end of file diff --git a/tests/data/freddy-2013-12-04.opvault/default/band_2.js b/tests/data/freddy-2013-12-04.opvault/default/band_2.js new file mode 100644 index 0000000000..5a27848ff9 --- /dev/null +++ b/tests/data/freddy-2013-12-04.opvault/default/band_2.js @@ -0,0 +1,24 @@ +ld({ + "27DCFA2810B24083A3ECC7CEABC7C0A9": { + "category": "102", + "created": 1325483949, + "d": "b3BkYXRhMDFDAgAAAAAAAEyAoreOe6YJDuid/4d+Iku6EpI5wiM21L7QyH4nQCdqsFff7G74sUOfg2959KS+c/m93Bzsq9cm4iczcQ031F0pI24q9u4Qdo7shN1lyglnMXdNq8SURd7pP80XACSeLvbMwV4AovNe2tQZwt04wIAXWenafWb3ooEg1Z+1///xsNu+LuNn1s1OVozao6Ko8CGmMkuNOEBbcSLJyWJoMubdqnTPZPZB/Hb6gYmJSuvEhgi5UlkJXQThT+zBU7OxjIyn3Se/Gbb8ULiW3SyHqkk+ND8eglFFowfg7DzTW1b/bodK6OoiYthSEMhS2YMOL2eCN7QMaWXRC006StHEbk6QsK2lVNY61aCj8aOxVDdxbJLVh2XWHrjyoXeUX5y5MZ6DMJP10KNYnhsB13GNmJoC6LgP9FkkhX3RmXY85B6xgP7pyuoc2qYJ8IhEtR8TUBclRd4OTtaW2ppP3rkGLpvAC34WvjgsXfJeGCLZl8s6hO0RfGvlL3ssFYfRk3YqazUX65gwgWZ+ahXcT5lPavGB43GSmKykNMKhpG7GGYc1rPCxef9PyhBhLXJdDyj7XOAA6EZuq2cP39+bitPfhbBO82vmXP8ozFYcCoRZfa/nuJzXZ0gsHIFlW+MmtfTQ2Ig1rBIcQVWssWuFgi8q0dCnJyotl1jvM70BwNP3bG54v3y7sglCN8rCQbk33ECGsAsaSDhQhYHN/JV86TGvYcF83WN3W6wyVfHaPlTGMttuQQFEUpJSyLHw7jipmTg1KgxhSqpTXDkAx8bS1960Ody1AOi6wQDk+XbHRR/lkF2FFd88JlD5V5+P4OVI1av9w8lGlbkZapT0px54tRzfCHg=", + "hmac": "u2yFnVEILAki+YP5dmN/gbSflq8vyKd9Aaby+/R82BI=", + "k": "2WbOqqNF5mqW9d7d/Npd7CpSTdEL8wwzUIHYpvJNsInJ1DuXkPnKxxWDiLVaVn/kMFo0ssD9U7+GPzp6gJ4kzxKw3OM5W6XfJdMiByDOwul6RZFAgAhH0oiv5JeYUsN7AJKkBWskZK35txYvHROFLA==", + "o": "b3BkYXRhMDE/AAAAAAAAAFlrCvy386Pid7oc4h8DdsFwoeK+rW0c3dtT75fhdL5CItIh7DHMrh1FAnXEeQ2jNlnxNjJpif5Eg9YD4+gzdYTMp9shvndwSHCgjGwZxkeoV/5Rf6TEDZq519yfwz2Yz+qHfoAJwogUokFwjP9i110=", + "tx": 1373753420, + "updated": 1325483950, + "uuid": "27DCFA2810B24083A3ECC7CEABC7C0A9" + }, + "2A632FDD32F5445E91EB5636C7580447": { + "category": "001", + "created": 1325483950, + "d": "b3BkYXRhMDFDAQAAAAAAAFAC8wz9EOSY6x0PZoJnNWbYplXUkyusprslpTLDCTQc0hPEhAZRUQU416rdA/SutvsnG9AUhSWSQe8CX0gJqwCUGQRmo4+krdA7vrR+CmWzQb/AP0JiTew2aRE4lc6rq1NaO9JbnPWa9YwAv2PddpK50sN1tQJn4VYJGesuYIiS6bdiaY1KQ1AdHilZe2VBDUIHfN9KVfWb3fJmCq2nwnABfBytNXENHvOhFg/nJDr1Z0abxc0amS6oTJ2aXhL8EQ/3diLB2qklh8+LMdxvLeF6QLlIlZuxPwQr372sGPrJqoz8uvKVToEYNtoY27W17xEsB+hIDARkuJBXfElaQ36F3peUuaGrAhUCRL2g2lDkLXuKUnfHtanONBOD6dayQs7RiDlBmrPc5/YNLTCwTrxdgNECiYiAhD/Mm2D6Si/umojN08UbFhltKP9YDjM6jgXdGDbYuQ2IKRwZKpHFVNtohORfQHYmud0HqqmMIWrwSyLwq8KpjpmMi8TftOusng==", + "hmac": "MgjPy9MB80eoreKhbqI1tjwQ8EpU5W6PK2hPMcW60b8=", + "k": "btfq8UDLgzCLQw+xNAAIc8cdzXZUxIz6PILHwGhbY3ude0yfPSSksiYGa7p2weiM2dHv30hgIoBYqTgZbzVz04WmJOskO9CBjPPrqRzEm7OcqGS9UlnHv4+E/aBk74s350oTi36u2w7aIEjqOY8R6Q==", + "o": "b3BkYXRhMDHZAAAAAAAAAMvusYvnxIbWpwLZDSSorwy+jIlf0Og+FkkoIW5vtG+ASwB3oyLqlQHpsGFMhFjrqYcBr8lIz3aBUM559wAKzfLpY8cGjldTgRQaXwuYj+N2MhMgPC8OXAzn/oIEg2WF8tD2KXmbzBb7CwyW5wDJclx8XqGft/rQhKIjz1eZ5qqwK4OMbS8m5kSHp8AKivCFIiVX+7Y+QDUkqKuQyTVCHacKUpI47IEdNxWn/FxKNE1oxRKLv+biYaB4I9ugPvCr9UW/lpPZgnoVD407ZSe9GPBw75NL/tbyzK6s140qxXhscUKwesLuCoXhWyD/5DKHrtMlErWM8I8CGb92WQvVzrLyszr2Vfg1V6sdL9YmTkN6", + "tx": 1373755400, + "updated": 1373755400, + "uuid": "2A632FDD32F5445E91EB5636C7580447" + } +}); \ No newline at end of file diff --git a/tests/data/freddy-2013-12-04.opvault/default/band_3.js b/tests/data/freddy-2013-12-04.opvault/default/band_3.js new file mode 100644 index 0000000000..dcc4471f87 --- /dev/null +++ b/tests/data/freddy-2013-12-04.opvault/default/band_3.js @@ -0,0 +1,25 @@ +ld({ + "358B7411EB8B45CD9CE592ED16F3E9DE": { + "category": "001", + "created": 1325483950, + "d": "b3BkYXRhMDEGAQAAAAAAAMxxfx9Ei8WUX9X10+6Kxgydhp1zbbdPIBJ/MLSii+gEhLcmDMUQA1AcTKEygw2nNqCrdJh98c2noGtV2qjsZnwiUCEhWMNCTpu0jeKXVBu36zZLwKb367D4cU20GDO8nMIXxlkFKwO/Ni9oAFGkl4ZSsSgTcmDqNtbTa+EXWTxBX5ZL3WawCtzYG3RmgVoU9hATk+kM5ZRJaR6s+mE+DFvVMW5U3g630EkGzf/gZnq3x2KHAuHt4PIWdSO+8lwCjbPKYbLoSS9FtqDkmLsiVUcOtW2uxwxKtBkJ0OUOLgkUCDCNG8sUKwtOtAs1+kO73m4DWAEOhe1994PNT8Qu48HkrmMh6KeG7buftl0vMf7Duq1JzAQ37zmPZ9cJlqpQRJ1H7oj6fVKPhfPFHxSKWcXUHJ4MwDc1KAxphi8gRzbt", + "folder": "379A3A7E5D5A47A6AA3A69C4D1E57D1B", + "hmac": "/FLC0nyA/eYKQ6vIRDDMScZLrJLTFExu4CQ/A2R3G6A=", + "k": "AGZoorj389dIbQQKUEgPR3gPuh0T7N3RNwbY5gBtD50yrLByRtc9moNTgeHeayynwiz52vspjCF9214k3CXUJOVRHcZnWFTb12xXomrfkTBR0cd/wLsoUZufzoG0vCUlYDrI5CaFBY4a0vgkdaGBpQ==", + "o": "b3BkYXRhMDG2AAAAAAAAAAUcSSE9Pr1ma4sGgZPQzjSCCDwKleyuTm34b0i1k1OSX7WJeqRo4B1ToIfoxSxgFhZ4wGvzkQvBS7/E3IZXND+b3iKiGYDWBElKz1wZiraRhjEaSs1turD7p35T91asK05uqQlVehHZuEGXgm87aQIMoUvgkAND0Q4jYd8aGMechyQURQ5vQdKaP4iqlYGaFqEOh1nRTfZUsyV6yuFJLAHLc6fb+eE7Hum5iwS9fS1JE4KZdgZmETY+0JvIhMEp+vsZIYdVlTTaWJxvU+imEee/V2cx9sn++G2nnuM9obrGaklaj5W1ysu4BGEKnYw9dw==", + "tx": 1373753420, + "updated": 1325483950, + "uuid": "358B7411EB8B45CD9CE592ED16F3E9DE" + }, + "372E1D51AA1D44CB9F17D8AA70ADA9A6": { + "category": "110", + "created": 1370116532, + "d": "b3BkYXRhMDGFAwAAAAAAAJIAigy3ZztWl46Kx16K7KgQOG6mQpq9dv0LWtLF7vbFaK4wZU9bq9kv4FFt088kLAjAH2ToJMyF0QiUiQDxix56mahLDjee22iUbvVaza/QSK8SfHFEpyR1Ecg2MRsXvn2DrwUPNsIrJJ2X6kdZLN5duXZGuhqLDITxx7eBOF+J5UWyjIBGDJNs4q9kd/B+W30YBtolRhzaHNonaNAEwEKOYxBjOnEE1oO3TwVRYqq3IT/fqHpj2yVfTMKa8WtLy2g4rGGe+8NzkiXPMND2cYRo+8jwsCBY1Zxvqw0149k/Ly6cUam0nAlq7NuDfpoT9J5rCC1UdFNKjE88Cfoarxcl+Kr3ZbYFQA3POhVsgFusQX1YyKZdxZWlfyPWb/SvkiD1vQmM5DhOs5XLONnXDTKr1xbWL+zlJxruYSWRxM5qD9oexnv1U06FFOUVDGpFg9fzbWmbkS9KBSGsUIyeqmmNFMa+5WgJ2Q4olZXUE81WTsPi2FerncvHnGd95n5m5BW85icZwIH+0pPUUlYFljhruBVXa5+D/GMX1DAzpBHRaEmgDYMJrhsLgaArXJiDw9drHKP0gzqVM4Ma0TX9G1Cr/mMEW2DaVtGNVywiTNSMMoqazb7hxTgITiTttShLv9nBUiw94vDKhcigD05lVXAsbXPqnZUwwGz4yCkIkhB3dH6u/9UBDqtB5hXFz/3taMPu3dr2G61Aqe3EU40ihz5D7Bp147SoBcuCyPeiOGzdfGa5zuGwC/IWg2Ii8nJgfhBAD2Va8hsutTI7Yc4yU4Ufla4cX6d0NH/bU6ajZIb2oFw3ie5fzyk7pInlWUhR41z6CISwPRmC9SseTKQzZ3FsMqKZ5KcBNlYAu0v9IFawC+kjKrMwKl3W6NKUkAR8AgM4HCyskjx6cbs52Jac5J3UIaUwj3zjoV0dH+6fOEuu44Xr3sk9VBJ6zUHiSV8OgNV1XEFlrqal+XfP60Rr5RaeztFWT7y0q+CFoS3ZKzNItudi0y1zY0ZnNsUbll2RCYlHULXE3Idxy/gsQJ29Aj39DTfbxvqGB9u6PUxewfvHnLkNXp7cjl3wE4IsaVHCNsL/ZBNgqwcaVVos9Wx1BtIWWhkOnmt2nrRIFz9vVdry8nEW6G+/IIFe+1e34oQmBSkGD+4OCGxGq1/Yctu3yG4QlumKa7SDIATG7q0iX0IMfFE9ws2RMBa7YawjMkRItFw2gn4Egzz8IPyvh328JssIW9oo2K/O0if5+ITWKvHvDDbi3M3FjJKMxDGPvAr2GA==", + "hmac": "ebtQCFBh6f8fdxLuO/3B3K8qoQrH2U96t+5yFqrEc+w=", + "k": "3Yy1faUq8AlFc/zDAcePMDmbqw/Y6vAs4bjjW5Y7enTU/ww9XDh7HEpVFiffEI1ETzuBOF2mnj4pq5/Y2dOiIwFS0tUqLwTSrIwHx2bnIohKygGz/52SpsiAeo+AB5D7UEVCaQG+RENvlUcD99cZvw==", + "o": "b3BkYXRhMDEzAAAAAAAAACY1JR1jdIYnvKtyFND55QYVRufOfs+Ple73caqc7u6VWTX77qZlqmrp+ihfaQQIm0AWFLvXWZHsp/08THTxBVgx0GwQ3iXJnGfvkhmeKiJYy3Lmb07alTphswv73ZZq4mlkpVNimYnpjbg+4S37v+k=", + "tx": 1373753421, + "updated": 1370116687, + "uuid": "372E1D51AA1D44CB9F17D8AA70ADA9A6" + } +}); \ No newline at end of file diff --git a/tests/data/freddy-2013-12-04.opvault/default/band_4.js b/tests/data/freddy-2013-12-04.opvault/default/band_4.js new file mode 100644 index 0000000000..99171a0848 --- /dev/null +++ b/tests/data/freddy-2013-12-04.opvault/default/band_4.js @@ -0,0 +1,26 @@ +ld({ + "468B1E24F93B413DAD57ABE6F1C01DF6": { + "category": "001", + "created": 1325483950, + "d": "b3BkYXRhMDFcAQAAAAAAAGlGNJ9in9DhzbvCPbVZnE5f5STx/5WJ7lB3irIT0npLHKxXvYi+bcYQK4kYbJbksYHsVS5mudX500yH5G155wpmlWfFpC5P+bHrZf0Ex6jykiOkpFOdOA7K9CIv66+z4613nyZujcslvAgKjIYB9zNbfep2kZwiW6ymkOvLiVrL31H+On6kDnF9K1cm/eoX75FJaVOVIZWHnTeyjbQ6BPxOTjTviDCFo9cKqlI5I7UzoeAXatgeyLhtZmwie2YAKRbZhzddo86sUnbG22Fb7PXxHpRQZC+rINqhyT1tM5a5dF1zpG3b56+d9APmaYm3bLtNvSV+z6/ueuwi+WQ5nDYXG5WzrkeDbidNkZQY01XU/YkNZt2rlZ+e49tDEvxA4Sr/0rFFh7j2+zSVYE4GJNiMMKXdHcnzyH+Rldf1zb3OJN2Pgtooklw2d1Omj1zmJAQu71FI1r7bGGxFR1/P910nnIpAHyD8nLO50srqkv1efJo46Biz7cp7/H5ZDMcKgfsBCsIEJCVLHd4Oz6nU3kU=", + "folder": "C8CE328220DF4157961787FBA30DAB96", + "hmac": "acLAZabMMQAHmZgNZ1nzTGzV3sBD974BHboOQr/EkBc=", + "k": "i/YaJr4KrTaO4/herbFX6rYcGmzFnBLyPSuyFhlMxLPcFP+c0a6x++BZ/DYQ8qPubQOa4HjHigcQFawcqkpUmAnP4lbeAPxXGjRcGp5TEORRp2WWzMItfL5f9IepUgjx8rlInWJEwSoBvz9uY2pjKA==", + "o": "b3BkYXRhMDGcAAAAAAAAADx6jShnR7SI/Qj/CJyyL6LSrCrFfvW52sSsdvg/fKG36swaSi62yrCrmXzKp03/bSlRSyNY5YsRzPU5weHdBk+LE/klsqnKyI/Pp/HHOFlZzr+HQoSA+PIK49HfFxRJNM7bFQQ3FQD6OXYmlY02hgdtfEFM+rXU4UrE2Zstv3lHMXLtzbNq4XY12qvoxjtDspQyMCeRmudOtzktiI0zzCs+tKUbd7vWPvwehx+/BpHmAmVkQ1RqFBsHmXWo3LRtEHBRrAsE1Vk0F6dh4+lUZsI=", + "tx": 1373753420, + "updated": 1325483950, + "uuid": "468B1E24F93B413DAD57ABE6F1C01DF6" + }, + "4E36C011EE8348B1B24418218B04018C": { + "category": "001", + "created": 1325483950, + "d": "b3BkYXRhMDFcAQAAAAAAADP3hpn5gvc0E6c/jeTCZ3+WYfg5i0bvAV6/aL6Mj//jGn9Tn8pioTKNAEwqPqW4QyRDbBeNWasvnQc0W28JO0zVNBUGP3nJkboX7Wk0tNc6rOC1C916yhwxNbQtdXMzT9CFdanEnRxaoVEkOk6iz421A9qMyvDsx6d5PUJx46Q3dmIfvTvlmuaVvq8f0rkwjric5gINViNv9GzcmL8wOdBHuiB23uvoZZ5zsGo2IzfR/6xaVFipav45j0Dvgj0jY3dluqf68TdTfM4TyRHcgCIvd0dn5QmZIxB1PjRbH/9oECMKWcEWBCxISX8mpCVVqva44HErSF68ooPdelgLQkzMbv4Rf7seLnckyvfHqPagY+ENSixRcmMlh/eWtddkqj7uMSkPKJ3pVrymG8Oypw8o7AwEMP293S1fRLyt0kcq8srHcehEs3gVpnQR3pIYfvzxXv9YcxMeUmCgjCvy/B1VKSvoCRvMv3E+9d+djN595WiUKQ+dajfNFO5/cDYNtfeNzQopzuzGoKX3bMQUrm0=", + "folder": "617F428170E1455D9503EC75AA103859", + "hmac": "n/iVpXSy31QWaWlMvvurm9bWez9Xy4xJRZLklYbMIKo=", + "k": "E/icM7PfmTWXDfzNZ5qg9I8UFbhBl/PeE8lEAXbQWtaNA0ZcxHlpS/FA8g2orTpkHgdtWcGEhTYWLBacYJBKZojvFbBLg+5LQW6G4gXaqo1Axb8NWrH8s0e0a0S7r4smTZeOse4aBFnRPom2npSd+g==", + "o": "b3BkYXRhMDGMAAAAAAAAAOoU41poJnZBcj5J5rn8sA13uFYnvtvJsopeKlAgSByFPUXLoNDlieO3Z4WdQ1rJWc3+SgUybS6wbgXejydTAUxRIqEReKoVnN3PdLGRKix0LffTCNRwPHy8emssJSIz01QYG6b10eRQ/97tTrSyvUOY2LP6Ja1dm+TpT0AeTUpTIKtRrScbWBd67uPB2gnaBOVvf749JkG5/fukkIm6NM0oxUWfOEhFuneppHRgiYx03xxyOFLo0hkfhdI8pXeFMg==", + "tx": 1373753421, + "updated": 1325483951, + "uuid": "4E36C011EE8348B1B24418218B04018C" + } +}); \ No newline at end of file diff --git a/tests/data/freddy-2013-12-04.opvault/default/band_5.js b/tests/data/freddy-2013-12-04.opvault/default/band_5.js new file mode 100644 index 0000000000..ee33c83cbb --- /dev/null +++ b/tests/data/freddy-2013-12-04.opvault/default/band_5.js @@ -0,0 +1,14 @@ +ld({ + "5ADFF73C09004C448D45565BC4750DE2": { + "category": "001", + "created": 1325483951, + "d": "b3BkYXRhMDFTAQAAAAAAAA9fJBca6be+oz0ye3FsSVyJCY0AnQXWfJgYQgYThr4c7eiW5POIskdjdle535X+cfFdriq6OUxNv9VIbUn0QUI1jP+V75VDoa7pDIL+zpR22VfXsR6RY5S5JINpEbZ+smIrMFphM9+ToD/Xli8zxmcqfywSekbLMkITwpFfyv31ZlB0I2WZCABQ5H/P7+UIRfK+jnwPc5VkUpQW+Bf81OfTNKa8N2OH0XIUzQEKcAG6ZpBohM+V8RdVIR+7Zg77uskQU28n4gIdJi0jsoFyarM1NeCoysX9cpGJATcNNI8XKKU0LxC18yBK0ST6INXXQ9hSrG9wEv6cpsdix/GNkma+XaQp74Dar55+DnauAZpMEfJcGe6PKSLA8QfUYtiqqD9Voh2F60dyyIVqXcgdpeBfVf7jdygOXuG07dKp5qW1w1eUH8I7zLX8Y+msuFGRyHXQ1WvB4qU+iiHyy9nxP+HH4fJbl27fUJ5q3L01o96Wdi/2MVhAYcoY6RD2DlRVUJgXDW/abRFmYnyXuL6MnnI=", + "folder": "379A3A7E5D5A47A6AA3A69C4D1E57D1B", + "hmac": "U1LfhLPcGrQT4s1vq83f5ikspRc6JZyUFmzpwyX0Jo4=", + "k": "CcSDvXgNE+Ro5U+MXx6VoYgA29o2mbTP45K5GORJaTgb3lGvFLZs0Gs7eecAaCQw5w/fJI9Frl5pl9/ntH+jJy/SOyg5KBxsGtnkjG3LXOcEJck8BBqWI/T2dfwfwSIcIji9dzZvACWifGNgnMdzBA==", + "o": "b3BkYXRhMDGbAAAAAAAAAKl92atkS8UPkld5AfENoWUe4WN4E8iVpJ/bj95sFeIthtaAHWk3OFHRB5XuBunuM43sUpiAIbCVuoosOMMpM1dJX7gctwSgpHen6ObUx85NGpgGvk2rhII1CzPhER+ACkIlopVBJd9tZsXf9sR24pO62soASghk397BuyaEkobgEnS7x8pBdQ3rKnXBSO5HMdLmb9Iw0YFZYHDORrJoQN19TFqIH2LQHe2/yvdMGD7r/gCbHZR/cqPCnIyYOqvT9GSVcsnLwxC9y5PKLV6Mng4=", + "tx": 1373753420, + "updated": 1325483951, + "uuid": "5ADFF73C09004C448D45565BC4750DE2" + } +}); \ No newline at end of file diff --git a/tests/data/freddy-2013-12-04.opvault/default/band_6.js b/tests/data/freddy-2013-12-04.opvault/default/band_6.js new file mode 100644 index 0000000000..f5cbb07dd3 --- /dev/null +++ b/tests/data/freddy-2013-12-04.opvault/default/band_6.js @@ -0,0 +1,13 @@ +ld({ + "67979020CCA54120BAFA2742C3F23F2B": { + "category": "108", + "created": 1370129714, + "d": "b3BkYXRhMDHAAAAAAAAAALKcrmbSK3N10mz8SnKVCpdQS2cYLptNG47UL3OT3kJ3HFTlnEZUlC+RgPGWt1ZTSiC+vGBFMIltHU3o1sJ/LxO7k8nSuX3Iky4BadclqAur8ux/kH2TyfBdWTu+sRSskE5tMb3SB0z3Yfv+w5nj3c7amD2eClrxwFyjW/Jv1reHAI4p3HD9bbDxVlVxHFuqsVlwsb8fiAdIXmhtf1ZQv8XM+Vd1KBSHaKC/nVcwyG/ZS0r4CyGdiQUq2bEvdERssRR1nzjT+g/sFseD8q4jrXVXhezXQdstl81GM3WSvVSm5lT/z6qMbCUrcPW7AZsFIcAMqtRHexBvKwfjpn3Tj5M=", + "hmac": "AVY2ZVXViuYtgfnSKShK/ZbbVn6T9SMfugz7F89Kd2Q=", + "k": "NwsqfULiH/XRz0LPCNJ5u1Kv4Onmqmeu1Ye4UKmipo6YspWDQ9zswlSWqgtjhKVzsv+eq9G6qQftYwG4cHbid18RdZksQWqDCrnE7arx9zwR9mYdxB9Eymb/nSU4o03D9pkAk/niM23vS7qkbbap8A==", + "o": "b3BkYXRhMDE8AAAAAAAAAPnQNt3DIzXvm/rjmdk/NHmfWLgOs+/hvM6nFutXkkSPcWK2Xl9NAzyoMV86XJviJF2wYd74eJFXZgFDgflquGnrK6xQifFqMj6zxVF4r6EACcNtzHgsrv054MFtKKiZm073KEQStDhnI2dwtRWQQjM=", + "tx": 1373753420, + "updated": 1370129765, + "uuid": "67979020CCA54120BAFA2742C3F23F2B" + } +}); \ No newline at end of file diff --git a/tests/data/freddy-2013-12-04.opvault/default/band_7.js b/tests/data/freddy-2013-12-04.opvault/default/band_7.js new file mode 100644 index 0000000000..52e8e918be --- /dev/null +++ b/tests/data/freddy-2013-12-04.opvault/default/band_7.js @@ -0,0 +1,13 @@ +ld({ + "72366D161D9E43D98E58EB801DAD1EF8": { + "category": "001", + "created": 1325483951, + "d": "b3BkYXRhMDEbAQAAAAAAAHuKL7sJ20Yz8sgns/j9LqJDQy9lms3XaDZBwYt8bmEKK3t2nQxNvUQVqxISzoRj/nX/axFvqcEOSOzZhxN7CvszP9eBPmTS2zTZvz4iu4NQ/LqXlUJ6wpf4HTjnhaqqcunas49y9ahK9xSICfo1mbmVyUI2raUoxMIQCzthfS/Wqr6J2uk6I0RraEO/eJBvOxO+buXfDQ5Bt9WoZREy+o0qcGEUs6kIMcTG5PmbOWV0DH3/Y29ggzzrUiaAbmvcu88e6warGI5Ii9gnW9iLt3AIFtIvuZQNhwyDDH7e8LPYOdusu7MfQGannWoc4QGTLnUkZrIozo3WTsoCEFv1Q9sYjyucSaR2Q2BEHVwiKzPsu4YaUADQSl65IgyfMRhjpU3qNsIxtu1gLjjRWwx8YV7BTOOSgz5MMwesZrMX5WcsulOgH6/TRH6mQtbi9d/kTw==", + "hmac": "A3fS6NKkoS6T1vTDvd+mVUWweXeNnWdPYb+T81WfgwY=", + "k": "du1CJ4AKSNBWoORyfTICsCJ9ltR/Jdy95IwZBXPsxD2fs+LmzTrFPB6sXeoB8Or7aaISaH6fzf5PJfhwIYs0WwtiJNMsHQOJ0aijvDMmpFvE1EHle+E/V9aPK0f3nws5opwfcUAxQVKAoZCg6VFXng==", + "o": "b3BkYXRhMDGTAAAAAAAAAExAg52C/fG2dWHCUgSx+8mg6eRc4M2Z0Qb9+ievEU6lNLuHqQQAEnJhe8zJTNfUm7bKMA9aqNrR9EpObuMR1j+uN2pIFJmD1pDtHsemM1vnSr5tZ5jUYPjOC7pWJyvC16ap4zBPfDMcrUjCVjgnZlppyZ3cJuxwVJNFRHUqShpX7oetObnVfOeixiUsvdSFjEGC9dbzvnQHrcv4G+nwmHtSLI9vN78SCWkX8I8DKZd4QZt/94Am9OWArX3r+s3Yvq4HTvNto/kC1q+a3k55AJw=", + "tx": 1373753420, + "updated": 1325483951, + "uuid": "72366D161D9E43D98E58EB801DAD1EF8" + } +}); \ No newline at end of file diff --git a/tests/data/freddy-2013-12-04.opvault/default/band_8.js b/tests/data/freddy-2013-12-04.opvault/default/band_8.js new file mode 100644 index 0000000000..5eb3eb3f79 --- /dev/null +++ b/tests/data/freddy-2013-12-04.opvault/default/band_8.js @@ -0,0 +1,13 @@ +ld({ + "8445A23B5740455DA360FEA379C3CC90": { + "category": "107", + "created": 1370116459, + "d": "b3BkYXRhMDH7AgAAAAAAABhbMvlFeIwLjjtGPnVc640YpM/uYRO4JbNRIxb6cSpo+FU9mOanKkql1Ffwu3ZRPSQZ6vloEUDRMqhGr4YpWM/v6lIO1iCVtegauIEVdAy+uDSAQzvC2+2NL3X9s7WgAcYh2G94JVbj7AharSB4VCRxc5NwDCViTkj0TTYDa8wDfJfQPhhM/+tlfZgcD9ZIUteFMtTPjoJwduphbScjhyRzsQM9jJ2scrYjasrnp+tFjUFQdTmuvkSI/10hoo3htLLF4bYpUGa4LPzu/CkjIotaJrJTAXZq22SDtrmYZEDPyR+epZW/2NW8di0T8gnPbx57nBKmvx1QWej3noEFcDRwtjwlp9tapyC0VFTw8xG7GaVNHc/xzGNbzQhxseGo2MPyIaUfx/9/zt0X20Il+wxX3kfn97F3DXY8KQOAHBrTeME1HD7HAvHyDnMtFX6IjdfUU488OuGVPWypk623Lmdv9KRSJoYi3bLHhR9tD4Txl9/kL+gLoyPosnr0gkua8Lfss89hohkqLVh+/VlNgwob62oBhSuS3lIJwyNl0abVrQ7jWQhkuRCXcN2sJce68cWYgvYwe/4+h4o+yfURqiLTMvts257etCN2dBkRucuNWNQJAPYw5HJ5lhwgVWVXKqU0hx+TeCRaxYDlQT08M7Pen7tmNZL6rR2f/GCz2ek1ZSoErabcFOcGExNJoLS/yvBAVVv9qOAa+m+fH4rVRt5eVrie4z5Z0G4IiL9Y/aGKqZ9JJ00h32+k/SmHmoYn8z6u6jBTJmnIVSIRzhQpn7YCTT7bu1b/WlYIeYA+/bNAUaqO6yAjuIFiQipocGyvkmkxkC8kIgsomdpyllNeJYz7ACc1bJVKUs0ZdYWqk73KlQEXB9GrbxCtPqsDqnYwvFlNgfc9ebmxUIKK3HsG4rKk03dj8JHQa5K8E96/qPxNLaxCX3Gvx0vKI8zzn8vCmUkDzHTC0Yz2GT4zm0A8ps3HvuX7gRqU5vs/E/UkpTaaHIM3CabTRzPLTstxfkMo9Z4Kll+g3VvQeOHazW+ifGYhPVVFk3y82loPrqpxpy4H5tlAB3XKG3Xgai+3LWqFdQ==", + "hmac": "6NNDqDnF+PHFS/RYa84na8Uo0D8FFiKiBC3VhMTUfSk=", + "k": "PILmWgunfIvnhZ2SUnP85rywepsURtQxOXs+/+KV+o+0Of3dpWiH/2vbSuQtA3nWgTkzAFoKMQozy3ekHgIq3X56ggJZBaEGVUd0yvqZcQthcdDjCTdwo6jlj6yqnaV1gUxV+xxi2KNN+OEHoF/F9Q==", + "o": "b3BkYXRhMDE0AAAAAAAAADDeatvXdJlmViVGv/hYR+L8uv51h/sRlE4N8bLbsekQfnOq4Zjno1XOMGG96V/DdTut8NDlTpESo50Xr0kJDR6nhAZ2GibT1c1PEM2M+qYahg/vr0rbIThGlitfOmHwOXyzIqo/GQUrTJkNkR88Mz4=", + "tx": 1373753420, + "updated": 1370116516, + "uuid": "8445A23B5740455DA360FEA379C3CC90" + } +}); \ No newline at end of file diff --git a/tests/data/freddy-2013-12-04.opvault/default/band_A.js b/tests/data/freddy-2013-12-04.opvault/default/band_A.js new file mode 100644 index 0000000000..92845fc9f1 --- /dev/null +++ b/tests/data/freddy-2013-12-04.opvault/default/band_A.js @@ -0,0 +1 @@ +ld({"A2D44483145F4B41A849FE5FEA4B504D":{"category":"104","k":"AgC49BNLEAcpFwcIcZ4VAJN+tIHosv9RAxk0ROK6qilWWFELXJyJXP9KmH4pBREjmc6LcRw4BsFwKpVUm1MHbOG6khhzqCRwQ8rjIBY+f52L\/r\/YJo9XGX569D1AHqoIhPMTyvhiV2nhjhFxmVuERQ==","updated":1370116182,"tx":1373753420,"d":"b3BkYXRhMDH1AgAAAAAAAL0hkkKKdOztTeMZRdhtIPQtooMTL7xdcKM21KA+g\/K\/ZBOXRstNaIF7GPqycQOAOknUfsPUrPaNQabE3hwiPRQ7vAgqEDgtmHyrn0ohgYN8TteVANoMtsAo\/8B2UeiAigkhQMVY0OKEZE06PzHH1ez2bPprlLWocSk3y0PB06h2N9Rl6n+JuE\/bYxlb2cCpWrutM5Xb8U8s\/GmLkyTeDv5ZrqPP4zWaOPyPrnkSRn2AUxXspVzRKmCDuIKZmmXh6XkPe1I7yrGOiAH4G3rDz7AZbsdzEIKaT2CEk6MWet+U1mnJoVzmQxKJnwSzs\/atnQ+yOlSfDjRdK5YzYfyD5nOVuDJR84bnGEGMCe9cyI+jcmyCIkHP6HDmGFTLDhvvnXXIziuh05RPAIrCOG6gpxkHY34mTh5AYfsYnv4FZw6ceEegbHEABNejvzSv3fBkh99bA+h2Y9ueOMWtomYw7S8P7tjEPaTIHNtPLJy6llqgLQqksBu5YdkwSE004L\/qRJ7lNiVNn5opHyxXOeGMo+c9pcXm1N+cARTztjlns\/wCb9lWQKZDRCQus8zO4222VOVsgoFEUZ6YKrDdgz\/fraQOtSzcov47lq4cGwZo4EeMGRJmPQ2pBGQF4csHYTwNdN1dkdTT1KV0mLMYtwn\/6tbybjlxT5MtcEPAqwXP4khV9v5VathptiqsxnXl8r2A8ovQoo14l9JzyvMOlvTcJHe3L9AnDoWvUmXw8rzOoUdOifcz4dpJs5yJDVB28Aog671wt7gca0R0kAj15tbdS0RNjXN3rWdki7B9wj7f6l8JiJodvi4Xt3av7eRQSwCbfYdQ2jZ1yTaWpZ+yUF8kTR\/4zUYbemSbi\/LWIqcH9narEexyBE5mA64PXBpVvjjXS5d2JLwmXY1jK2fuofLTmNcBZsJ5cMWSDBaYjp76rgnGtG3UQ\/0AELPTKcv2n\/MVp8XFKwAsngiHP4dbfFOWjXIMMtfz3zJ11v5zJTrXuG1fCWQrewjYC3iwiWgQEk2Nv\/a7aUG87vcgXMipXMr8\/dt6+c8YHNP+cJN4TjmuTGCLa7RmfUV3I2qN5CaI0LzQ5g==","hmac":"xNhX2boeIjZSYRTzOQBw\/lj7VX\/iucniYCtky+gFnkI=","created":1370115875,"uuid":"A2D44483145F4B41A849FE5FEA4B504D","o":"b3BkYXRhMDFCAAAAAAAAAH87YuKjL7BH4gfLKRT0MfiJDJ1URxjfNmThc\/UkDXdoAD0jixu3Q30YG8xR3itHYp5Gdtr4T4scp4HAtiKJ0UYpkGbLt3bmhFGtpuH\/AJqVUiRW+t4kGzoLdV\/nvsL76oae7KgQVqSOabIUMvdIVapjb+uA9BVCOFV7fnj\/4+rj"},"AE272805811C450586BA3EDEAEF8AE19":{"uuid":"AE272805811C450586BA3EDEAEF8AE19","category":"003","o":"b3BkYXRhMDFzAAAAAAAAAJ4FWzFyJZW8+caJC7cqRq0prmapCi8G2cYsIvOxolmZ+O8WUXknbg\/IdgHcZvquPnWehKt1qlx3Q0b4wUjkH20uZBkJJADB4EixRs7gjcNEFYr5rJhcocoV\/LoXcFeNNZlSLSu9J0v1o1IO6dgdEgOdzF5irAdk\/0WxkZJ9jN5EYXCsIFFecabjAimXQZMZJw\/gGIOpSGDQWLKCDuk7pv5xRuII6pmR8jif4T35oM3R","hmac":"JMoh16y26Yghanste1vPLKPbTCOWBgBaY\/Eu08LRk+8=","updated":1386214835,"trashed":true,"k":"10078IRI0KYGRKgbidYmpqdgeLmsXzl\/Rr0t6Db0ZOV3GQGMk7n36Qcl5r6facvMwT1mgmCOSpLAsQ4MJ173G78UF9VPeqfzVxu11HQYXpEPr\/X4bRWfPI9jbWcL1sQNuYvoKFr6AGyeGxfRrPv31g==","d":"b3BkYXRhMDFXAAAAAAAAAM\/pW84fBukYAaFjZQIRf4CaloTNI\/UKT60EczrN8fSGcekBOnuxx3oNZkTIhRHhqimNXXjsikYJ015pU81u3S33G00SeJZGt2ybXPyQ9LshOaeqKg0kjIxKL+GYIk+rPkAIDixNuumgx6nb1C6s8\/JnryP4bk5mi4e2avIbO2ddFZCqzK48\/\/7nyb+zOVlukQ==","created":1386214759,"tx":1386214835}}); \ No newline at end of file diff --git a/tests/data/freddy-2013-12-04.opvault/default/band_D.js b/tests/data/freddy-2013-12-04.opvault/default/band_D.js new file mode 100644 index 0000000000..ebe3e19fbc --- /dev/null +++ b/tests/data/freddy-2013-12-04.opvault/default/band_D.js @@ -0,0 +1,37 @@ +ld({ + "D06307ADA44C4031BA2FF4B174DE79CB": { + "category": "002", + "created": 1325483951, + "d": "b3BkYXRhMDF1BQAAAAAAAJaT1yZYCCUlp8pOUU7XbdTy90boGVyyZAG71tm/5W3N5ktaZZFaL+OxS4LOpxWQGGcLrWTLXekyAwh8lKcGUtx+gmDlMpUR7teTvgX7jl0UOhS+NOcXZajrdPqhJ17XIeq7ze2HveKyLEpJ7zoNMkf6yHH+hEo5o6LVb0mDoskDx0Tb0FjSzDzvlxMInfFZrNPa/QdMg7au+2lj9GtTY43lX0CN51HrkJqqC5IJHti9PcXFglakVxATpT+h66xoCDreWoObCOdMlXVLhVOvYGelapHk1hIxSWdIgP0Is1ubEvivdhBVWuG+FubZVOk1WaLQJ2l0hw6fIL0yIRfeiA5eWEUB3zYYJbl1kLnYhiz93LMUbaaAiwPVRhFRjUBstgUVipLayTb3QCg0um4M4/qYIwPO6htj043Qn5iG4WSEuEjG0YjXytqdrIeS3h2ghzBPkMsW6OdPixfruix5+HSMV53+vzVLXi+ko7ScjsWgSZtYEPxc6kjcnp/4xIzhbCG7zCu3j4HBIQ4Cr1TYDWGh571zE+tfewVEOPriBLvTmcEdvQUZky3LesqXdJudKgAhGRmLLAKiyYJTOFkB36MLGh3oDqu1GIebQSi19hA+RPSZ6T+Ouz4GYdN/jHyA6/WqKv3fg/RdipaU1AbpryIoXd9xHprJ5poyaAiYdBNAuvM0a1SDndGaV2ABHwKTwzXmYhDvMdPgRg30zH7hw6LUGNEXDTlmmz4PjGndUQJaeWDGwexOOetoBLly5jYZ+/f+ZC6ZuNVyssODUDbySBVbTyrv8rnagVSRPWGryXtFH8YDfbRn3bflyX21vRYY82aCMezYuaLxAgeaX7+VD8APJtQIAm5b1Ob3OEm0CpJvn0h8OwDedebq6w/3pQPBz0z4jDugXFKhA7UQEs2w0kHvRI3H23h/TYP6VpBfu0Cc6RJ/6TcRHAJ8dyRl2ag7vEGMoGBafmik/68lEvG9CjbbOhbMw3JUcDglmSCuAFCR9etrxO7+PdlyOaCWOoBvF6+8rgRymdP9vXjnoZxTYmFINjY4OG2qRNQuDuDHPa9YYVonjCXPAJq/ty2uLD5JVEXJhsevWn5YCM7gOtR1g0lEywYHlUiQhGwO639Qoez6x/nmDDCap9AYaYumU0aAs5878aT5pXOHnWpdJJThIs2/pKYrlY8IMyM96mvBeT0odol7WhGCKEZ9O/LW+xqxqs8kyCmwnWxO0rJwuJfAH1BCYgynXr76Aus+wKqIF1cjgTsEVIaIeznKJpxMpio6OkSm5kXyEawOKW26yQS/KXeyXUWCcVrCp87Esx1EQ0i5thq5iVC6UzaG9i1AnJ340Fu875vvNg5Cfk29pUFLAMPm08V4Jlfq12341GiBs8zCrlq+3DJiqK4G2A6i4sMRblWYU/OJOhWpHTmCTs8Lhk79ZCBpJKtOdmHsyFcH15HqJ71YhX1r+Rba/beMaJlor4w+2a2Txvtb0/Khsi0RrfrHOr3juR+5h5x7cKWS0ep9wVTh0eRtuzxYRGpwE5Y2n2FuNfmPzDuCtagea3INjvp79r27UqxO61F1BxvnDRTHSE1P2tV/5SpkVrpA0BvEwpEUhRskw0LNGSmZxt0dZzFjAcxH3vopzPvxdvqOutoC2ISliInAxc8CiJw0rq2vLh/T+XXUd3hwy5vdn6leUu9pLvi385cHS1TixYXGJ3ewHa2V8fmglTjWsMjdU/v8JywYeND30/0BAY5trF5CTI3E9sO/JrZLLpTUjDhNjILGVD7fA2wRvmrmzCixzID1TukS9hdLxDnO/xvZMDjlvPdnls6MMEhi8sTAzzU0sKwX3GZBfE1H2lY4fotvF8OEkt98X6SMC8t0SQeiMx6/j3ikoVceUtkn7Lu53izp+H1KBRzxQDf6qvzw8j+7d1FkpvhZUoTXUuoIYfQwDXSiJAQ=", + "folder": "617F428170E1455D9503EC75AA103859", + "hmac": "Aw+IzC+E5wXzIONUOx2T3HkpEuOe9GJd9B2VKTDQE1U=", + "k": "XX/7JtI7dGhIowwC8kdeCJgSEQBz2473RpR/Sd/zBpefXOqSW3O2A2ar6JQoBKiFaniMFzJuCVyF3u5ho7imWT9JkvsB3N4vRNe5W4Ks7mTLnbZFjDPYu5NGqN2mRGH6i/W3wh4NHILNE5jTxMwvTw==", + "o": "b3BkYXRhMDFiAAAAAAAAAKLESLkbJBDmrKAD8sBqerT2BhZsYb2r1kEhA6bj0st8B8pOhDDAGeg33G+wDS49SSTIU0+qQd5V8NPPoXh2dOwGB/xj0h0LNOlx7sBjzb6dZ4JWpaazFoO3PSgeZBaeuD9VZ69LL5QO88Hv66yNuPKtwqxTe65lZKbX5OrVG38GxJj+qtLI32ORaZpby2N3A/UcvqKJBcd7jVK+xhf8huU=", + "tx": 1373753421, + "updated": 1325483951, + "uuid": "D06307ADA44C4031BA2FF4B174DE79CB" + }, + "D1820AA8CB534AC6A4B5A2C0263FD3B2": { + "category": "003", + "created": 1325483952, + "d": "b3BkYXRhMDHSAQAAAAAAAOtI8mRSS2ZPodOzN1vWoS97ViCd1s7x2aO4FUF4U9fYznoHNE0UeNlZyj5HVpZ+LYcAm8/wHhFt5YH9rqIDtADcjRTzI+b5UZ56uUycJ9Oeh6V6oyMRyrv+0sknwYVlsQUZVXRaP7v6G0PxcJNpLH4RisxxVF2y+r023GT4EI5pncgs0J8LaqA4HG8yjy4ie0orQEyeSSmuqeaMIlGpxRapvakdpexjNMzWpdFUEnl4o+9MwE0QvgRilchQuGxD5hEUf/nQwjEU7BcQSEAbzn7aDhmuVMh6Laongd7K8XEDLDQSNJYZdY54xmIajkifMNXA40vSqhjRDB8vbLNUWS9h0uU9TV7mWUujZTGHhFPlrcxmisAW44tXuJ/nRNYO4gljVEWCdPEjHVPteECN0/50OvZvM5X9F1MYNGk/z4S3u7D1YBvBGnVxGn9TXR0nyT38dYq0GWW5xBbDUz5vgCe4w0gZ0EQ13z8VCjnM7v6uKAW9ajsQqIoYCVRv/bjXktb5CmzC4phNSH0RYYD8ByEY/Jahn4FdouLab/NoPQmx5ZgDZsKNX5D/iuXv/hQ/wkmgjaCppdPJeDmEHmwTqTvFrlCqvu0WJosC+PiOVkMq9oIXByNf5kFVLqpMEiKxPRieZWcZ8evSkrTPuSHE3EmKzzevDGNR4TEBP/zLv9leUecOqL2tlZ87iclIywOp+A==", + "hmac": "NtgrlVsQUjVOS+by88aMb2744M0TFC+MNv6h8gs4C0A=", + "k": "ELybuWAxQX3rIZCsJ5cgt9rImCIoDoqwGRzvspqEjs/Nh3m1LH6MIlI2L6NeVZNvQJvqfNQgjlxB1G7f7D/b0sUjv1DN18reNSYiEfC6J5i9OJPb6AFH+n3Pamu5azE171E3IOJy8Tcm5EonoZanxA==", + "o": "b3BkYXRhMDFGAAAAAAAAAI4XtY9Rhit3UaBvo32Nx9g9fSH7COUGvj/JrZfpPTfI975k26woxzapwHjdBVlYEmKWLaD1Cr1PJKuJtDJui2RT1q6hGv3+0yDsnCXsoM6OkSPlFisKjWA5FuaZjZVxKyZHSLHbyNiR+hj0PfSVVdFVoJs/WNfa8KF3NpD1+LCG", + "tx": 1373753420, + "updated": 1325483952, + "uuid": "D1820AA8CB534AC6A4B5A2C0263FD3B2" + }, + "D8F79F17D6384808848B213EB4946ECA": { + "category": "001", + "created": 1325483951, + "d": "b3BkYXRhMDEHAQAAAAAAANiTotCslHjA3lDSFq67/PAjJqHIN4CLgBq7B9w3/vp41wIDjd7HeH1RlG1QShODDhMf+NGbN/TCK8snRjUfKS1F+XgAEOKjeTBpnY3mA8FIdmKwJm/VvYXWI1bzd/ndZA1wDniaFy6+qBOaCDRCh5ccg3PkghZqSzouDY6VpQ3RtrtCd0cIp61PyQh9fhsIcaV1kuzOamAtTRrJdfQ9Kfd3wN3WYmvQ76KV2CtV17eohR63tJk1uPxa7YfEd0zihPc7vVJX2yKyHmO9Xb2yva+CDVExKjv7sIh3D+oT2J7vqfJocuwjzhs5C1c5rxMipOot0FSEYQ76YPb+Rz22NjKfEcuu+49s6+kq3XoJBoqVohF+uFjM76v5gDeFPbV9Im5zxFtU+R0YtiGCFA7NPQ5AFL1oQt2fGxDw06jhUt+z", + "folder": "225014A4FC654BE19531C19E5A3F8D5A", + "hmac": "kH17UD/ltmyhXda3DaIpzQtW3NYXgzOcMCPbOvY21uY=", + "k": "nHYWK2zu3rqnDsrZV9y3WsH/Hd93Ci+j4GSMLNSW6S/6DQVn5UjhFIwLB8qHKCQSlZWkieGz4lMaN7F7SKAj/OjJE73NfsGgjQHhwBy0W3/Ty85XuGufE89gikFNs1sw64WQQG3ZM692YdDd2QY7EA==", + "o": "b3BkYXRhMDGbAAAAAAAAAP1DU3jTImWEWTH9q5cDG9fw5DAVO5fvavTSShwoDQmrLryQL9R4fMXL+r/xjUg2EiyPG1OPNkmPq5kF1vrGNTdblojCWmpvhh8DIV3h8vt5gwd5+JaoTeQbvYqayfPXpzVcZ0fDEZQd5HvpmQJJGHMpU2mU8W9XiXlDK66tpLIJwqPLSibSE/OV/FIUn2wb3G+eEKi6rPZIoIZjMMkvk8S3H965CKUr9T7QrpVP6zUP6oQLeRIuQt3WUGPfi5LWfnUY7suMxpAYRBCWVnti4s4=", + "tx": 1373753421, + "updated": 1325483952, + "uuid": "D8F79F17D6384808848B213EB4946ECA" + } +}); \ No newline at end of file diff --git a/tests/data/freddy-2013-12-04.opvault/default/band_E.js b/tests/data/freddy-2013-12-04.opvault/default/band_E.js new file mode 100644 index 0000000000..dfe1e0adfa --- /dev/null +++ b/tests/data/freddy-2013-12-04.opvault/default/band_E.js @@ -0,0 +1,37 @@ +ld({ + "E0D293D29B10483F8DFDAC72ED0BE5C0": { + "category": "106", + "created": 1370116210, + "d": "b3BkYXRhMDHvAgAAAAAAAMwXinLLoArWq5bfBXIorDeMP6Eg4AIrjDMcKauM87yrq8BgJB4yJlw0CmHzLiaul7vLEuTFoT8Y+ElWPIB6x6GOTYyXX+9g2O6ffOMAt/x+fBVtMxTfUnK8Slf9h+lHkkmFZWKd9Q19W6YN/DRQD65yO+PwQ5ErqAZayN31EijXzS5vbtnwXkDvK2wXwXu9l8ye+7kFGjDT9BDPCLdpawqS2BEMoH9w5dkxqenf1pixt12XaJVaZ4x7MPsVLGpS6Vwua6Y9+Y6OWSgV1xjUTz1DXGmT+13yG8d3lgAM+McJEHRihzq60NUIK/7XNdIDsZwyTMBAk8XM0gxvInQbRmOGAp3JH1UdvhMrRsXlF0b8yNJfVXb4H1QCkIzxlkEbDlsL0dP4TE47QZZnnQrPNSb51FxTCsdTIjnG4DClXijrR0YknI86cMsaHC91zzf0oIz7ETMZ8vkxmIwBlq559IOAns1hUyoQAWuAbOBNQqw1TYQzIT0CPtd5cDPEfuERj1/SGyXdFa7SWqvGNc5z4ET6tujoG7xZzJg/JvifW75E0xel0e8ZfHJvQUaZx2Mj0ukjiNsorB9O2p0DQq//P5W1A4rHAg/oeLQB1926u62k9+FcRG4ZrBgPI0HGszprHzF5T+lm7JCZamf6waPFsIgIAh+1RMq4RUqpmaQPsypkB2GtkO8h83wKDaX5fngts3CJH9OZI5NGqsOpXjUyOcqUFM7GMHKsXVLSXPXG1snKCSFph3iLoH/bnryPx4Mf1eYbgqDrdQoJ61Youiw/qo64xMA3HpW3EZ3p48tTOwvJBa335o9QdSgIOdnmhpAYJmhrS/iRyoAhamG6QxRxIk43e17UbQ1Y1Pv6lgMOfsoWhkRM2dL6rjUA6O1YUnPQNdrjYA/HL/qWEvDe1C0JbOR3xWLFSCIwkTFvYD3CQ9NmMXAnZ5Fu9vzgQRvjhxQSWPE6GwkykKYOS96RXOeP92HkB+Jt5lgKFSuDNwBYXDCXu1KwVvCRuqV67SLyJmOG+Rf/c8ThbDTIwYh0Q1EZoPMlx+/koEVZhn89lzrzqQJJ", + "hmac": "GPeCKCNyGzGIdpakTJAIjThf6j8ZLubbbsYDsyewllY=", + "k": "ikCOxk5d2VpVu7slo/9KBW5GNAsXlBKHJSIOV4rChQsWG+GdLzWekfMbc8liT3Xw08lR4aY1VFt6K6+nRHuazIvnIK415aD7wBIcL+Tw9gT/UESQv8ogns16SOXzCJjt6M61sO6jpzQzZENWfqK2CQ==", + "o": "b3BkYXRhMDE2AAAAAAAAANuLreKDkN5RxxpiuHlR+MvbU2JJls5F38JDUnuIqXvBVKYlExHOLSTB0gMjcTf1pvGglvQqqGaNox2GOnQ5F/MSFmHg/eFVsewgeXj+IMs8Bc5itgsYeSwZw7Po3fdc0RgxWvaWipXdhsejk6Fd1uY=", + "tx": 1373755376, + "updated": 1373755376, + "uuid": "E0D293D29B10483F8DFDAC72ED0BE5C0" + }, + "E482B70C038D4DD78A0940728FA737BF": { + "category": "002", + "created": 1325483952, + "d": "b3BkYXRhMDF3BQAAAAAAALd6CWU7JR10MTbZRSdznA5VPiBVK3JYI8liAbhUQdjtsxVxazkwiPDhPTVeiD0F3hTXF+KzJjrTK8S2eoSaXHL1bFjwglj8r0hMEf/34/AytILXubb2MiW64wxsDMbuhDJFj9dobm5OWFKUYix7s2v8DapYgbcA2M7xRcZpqqUOJOz6hdLFVCfvdHfRjfxi4m4e2N/1XhhECDrtB7fuBCHDvollW1ohvC2wmXvcmp3CT2w5Dzm0FaxlysIIg9OJRgP+uxzEG3hYR2bp+cxF+x/ddPbEFJ7ckx/Mx5vb6TRtzjn/We0O+HfVYwCkaV1pN3EYnjMYlkfPR418qg+MSpRbSU2ufgIQm3TT+lGIB35d8LhMi2xd3geYwMCwDshvgGFusYyGlwLVqmC5BTYiMgpQFkestz+pA4Gx9Y9zrYCZ0X/hpZ6J5E34ss6XvnWVlRO3Y66d1MRN18y3+/zOeBphqxmruKxdojzm9wY9AxYNJuETV456Sx/dguePkOxCA115y4LOlcLijw1HRVEN6jLsuGpabENXmDyZoiPXnGiyHarlLqUfIRvW9uwWjdg+f6WFDae2rOjuOGTL8BsUhBE6xaV38qC39zbme45tpe+xGNatERPcnLPFehjpB45Oo84nGl11N7jSqqAlqjkFQfb78WeDlanx/EvXah+pTmTm7ubaoALOPFK4rCBsxiftOpgNaHR2kgkU0+TNKexbwtWflkfDrm94UAe+57n6jkW1EZ8Eb2lTQ4GgdwZXguYm4l+R9gcQ3ssePKkTzfxMhsNRJmYWIZZnzUc8uLYLnK3YUx6QzrI18fn7Pdx2S4BiR8xf91lhTl4ph0MAQ4Adquy8s+AmCMFYg5XfjHH+XxfxK1VB9jFYkJ1yQNNMOETPDXYs+ST+GnBYQrl139Duba4KMudGWgVFYtXIrroVJedzEekpf7nPfHsvWoeu+//cNR72WETUnGQAp+Mwa/lWHw/IUcifRSKbgkTEJ0JDCMfP3DJhT4VU6D7WbdGBfYE+ly9q60TTG1XUrdgBwVpswJhAOtxq6mjZ7s6ncxf+93dbmGV/ZxyH/w0iYNR2wk/RTZuhv7XRej8PWXkpvDl/EwDdCbwIXupp5FbNK00cqF7IPjqREYca4MeO7/D265K6CyfmsUyNEQj95X5W06vL3wIToaC+Cn9CMzQkgGOtq46FK5WXmDYaj1GFRLfC353ekN3kvdl9DfTGhAH+oPkbMWGFCIZIoMIZeeIfJJgQNxA6pRH9TyA08hkK7NiuLtCXLZQOjpCfB91qfz45eWHErLPxtJeDJw3M5lo/tejXYZz9z6RzMWFiVWagmS0gpfUfOJZlUk+oGM1vUKsK8PhdE/bA8/3WfXrlcHQ8M9A5meTE5Hn2sTuBdKVQI4r/sYOQjFcYIwBTdj222F8hAKnZ4RdjUYKIMrF16OZKqpAWxzfDiMcI1kfw3LUF45Flq9G5xdNxtrAtLmqwkutOTZ0qabET5qF9epae9fucCc9H3Y4q9Jk4xUoV0m5NrVjRDF7ZqicB/09UUI0n+3kd1M9kTcx1E1XPlhuUGtUGGCr/YzWuVxyLrn7zAzT7Sguih43vgm7kUg6p32SgIJt/6lThxWoHy2w3MO4zKHA3AoWSqrEZIXjADFJejG8sfrwuQg8oLSR80M/hiaozyostqfki5fQ/Cj2A6hJdkmJf4lcfwtgAbPuMsTsY0QRhm/EMfcsg/pNcwLd01h0XdsKsiYP+39QBYnupNpcxE3BGxnsQFj0CgOM6CVEWu/sfrS112SHiJXtg/FHAMC8MxJPZUAcA6XM45tuduPm9CaMP3ASX13LqJKDuhJMHdMy44vKi9P1R/Rav83H47MGyxXYwtvz6FkRVX1oZjcgD7RNNa2VcBIrHZfUp8M8Fp2PuKrR1RQDNN2Px5HkAblsKvwfPfzT1zCs=", + "fave": 2000, + "hmac": "4eHvwV3DwH+fmk3ISBpnFfYVlVlFTB5bJw3AXHKS2SA=", + "k": "yr2c+hmtrTg6kwcio/GTlS8bogJj0znYD3cNB7jXQdQICYG888AHzlB4UrGsMZPhLzUFLC7Vfd77Uw8x7TivyfjRNzTQpvTtGoIbhSoEYVU5TWz28CrwDZbmDYWpz7z4r1ElIWYQtRCrgILez6c8iQ==", + "o": "b3BkYXRhMDFXAAAAAAAAAGXKYq+ozGVTUMOS6QEFBbvmbltPopkg/3pQUwXR8pRRs90QqvsIk9JlL0zWrV33N8bWzAUWQyr5U14vEMZNyLBRSQ26SpAEkdQllj0SwD7Nm3p9SpR8TkvY4sbnm3rHzy4bm8DvkSNM5Lau2d/SDSjT1vjK/8Yvi2u+6pMdniBqkBy3uJiTof/cHPDd5KwH+A==", + "tx": 1373753421, + "updated": 1369148598, + "uuid": "E482B70C038D4DD78A0940728FA737BF" + }, + "EC0A40400ABB4B16926B7417E95C9669": { + "category": "001", + "created": 1325483952, + "d": "b3BkYXRhMDFTAQAAAAAAAF09tCe7uipDcmMo71+IeR2y1xC3e7iX6qFyWSXfMHyYJ9jA08+3rVf8L96QYgjzLnfkyQi0++mIeDkclKhSVNQ3wQ2H41oRq+BB2sFpl9LZioIZCj9y512XR3eXHb4/fPRVhZm5Xy0EISl1lsKzykoA3CMiAILTTJeYMJQrwom3akyZnvrrM8iizDb+7xfIq+MRcvnJVgQsYCRJaL6QkXGK9RJJ4P0F8FyhpQXbDCZ0/x+dUch8bUHwRFjuW8a7uJfr4bHGINCq2JqrJMjWDtADgBZG3iipoiiRNsXbDYAGIVohqIMgbJh2NzAgyptIVnNFR7ArvBerV7Ka3g+s2PqFCO8LmPMgVvPW9fLQJdl+ZBQ4WRJCQh7ZDDm5s+MkyiKvCwOzI1RJeVpSh8F0aZiESZI6exIswxkErwK2tGV1JStQku0WhM/qCKbYENgRT1dwFKNCYSsSaa/PRdLZujwpb0TU+MijZja+fTkpLKWkpqa8P16mzpwFZxFM3MsOaGENe+xYFU4PfGBEH0LG5AU=", + "fave": 1000, + "hmac": "WJiNQqN3uDxc06QAWI23wNgn/FYniigGlgMfLZge/Mg=", + "k": "rIpJwTVdfuqy2sihZc7bJJHb50DVQIAyYbZdHPDUXveJiLdwSTzx3XmLrcSUcMUruoP8RdyU0NbK5ix4lFwC9vcb4y2WOmLsCxARe5VxfiupFqXKvnSILsSIo7AiwXUT59r7syS/s7/iejyIqKr3EQ==", + "o": "b3BkYXRhMDGwAAAAAAAAAJqgRaKvByRAogZbxxBOv8R6bNVhESt2/WFvCY6FFUoYP7hziYrQhOgBv+EM9n+x9CdkK8O1lhzHoc2U4RO+knh7VFTIJDtnZuedxgILfr+6izsiFJZn08KwwEKNGP5BdpuOzg/a58vhr5qPUO1hOVzVLmTYuGCvnaQ7REinkwLwaX4W22uYo7bFRo60sFcrVcva/KTEn81013B6Gc3fcDtFnjdG17QkxvG3rhc6yzJA9JPCi52UA7q+rLbsn1jd3fcTCykI9T5AfAki00RDXG9qSbZcQLjD2GBs5IvtTrdjtYaah2yrtc8lBpmLyUhFrw==", + "tx": 1373753420, + "updated": 1370115423, + "uuid": "EC0A40400ABB4B16926B7417E95C9669" + } +}); \ No newline at end of file diff --git a/tests/data/freddy-2013-12-04.opvault/default/band_F.js b/tests/data/freddy-2013-12-04.opvault/default/band_F.js new file mode 100644 index 0000000000..94cf3829fe --- /dev/null +++ b/tests/data/freddy-2013-12-04.opvault/default/band_F.js @@ -0,0 +1 @@ +ld({"F2DB5DA3FCA64372A751E0E85C67A538":{"uuid":"F2DB5DA3FCA64372A751E0E85C67A538","category":"003","fave":1500,"o":"b3BkYXRhMDFYAAAAAAAAAGVZGfAZ0hOzvSYNXpQTzEDSu8nHDkXbBPNbVJfWncJWvSOUjg+YAFMrjszVOl7YVaYJCE3EUVIARSyhUN7Dqs0rolTkXRjEpIPLmoW7LoSyt11STlFGo5mhZDW12CY92BJiYlTrhisnmaqL9gOxHUE7W4Kuflv8oGZn2dp9xwUkBd0dhQ9Y7vS5O6PTUzmzBQ==","updated":1386214861,"hmac":"hVOwATMlnTQTAdiXrYz8zBvS9WZ3O5KjQnTuF7uWJ\/g=","k":"9ab7Vb8k2b0lu8NSXzSF6JOQFbtoxyctCfcmULCamix5k0c05f5d90witDZ++Fa2o0N9ZQab46ed\/tidZm\/Vz5MllzKu7uETy9NAtwPcIMGszkNB0jh8+1MfszMRcZp2q2S5jo4+ChhdWVTsD1UIuA==","d":"b3BkYXRhMDEvAAAAAAAAALynfnuH\/GcDtho1hIFNF3WDi72NY3G5y3bCUgSCGwNfPLijWAGkllE9tl9RUa7hCCtLrTvAoBDNWGdEhTgVKD855n8rTjngEDbIgsmEZLeELGPwNB2XWblP4Lbh4Rjk3g==","created":1364999176,"tx":1386214885},"F78CEC04078743B6975511A6FDDBED7E":{"category":"100","folder":"617F428170E1455D9503EC75AA103859","k":"T4nySlyP3RjvcPxEIo3pVw0IxF4i8C8MnYubYaTcWplV+r+h47mHozmiLqS1TNwrIZaC0OxfGvqfr5ymr5FcVNknwPA+K5d7KypQ\/vA7ICFkY+q+igl1h2tgnLjwWbRgaktqHnQuHdOdolP4Z0WB2w==","tx":1373753421,"updated":1325483952,"d":"b3BkYXRhMDH\/BAAAAAAAACG+iJKIB+TaegDRNiZFhsZinNcC7IoA0M2XwJOqrshC7psJ5H3VBD+y\/X49GgXwVWsnWvzw+xoRV6LFeHdfhXBEE0TReX61Qzd9y4KgpRemIuls8ORTeEbnzZxZafE5O4\/WOwAl+K\/gkGpTZ1YrnIwGBbos+5bWZCh8gTG97xohRTMXtM251fNfpSQBVAPpjjiW7+vr70DIavdQg9WSUaOwpNz7mBiCEtaN\/zdRcb9TQb\/STEOtG383nF9vcYEC4hxKXEA2zRMPBO7WWMvquPq08nrEVBqNuVFSo\/O2wPMBWjE1txg0XcBJQtCv\/mFRumrx1sxdQR1T\/EQKYasFRQmSNo9xHvytqqJsu6lTtLSHlKGqB+ifIdtgPsPQJi2RRtcOTsED+Tz6EgfmrN+p7FBciQcbsq49NSKLVRjbvPWO3U\/bJTHOyZQFuGgoecRlWgkfhpB9R62enV53QYq+VcEmJev7hAn+PjbwqTekBLfDWXx9YxqgqcJuzEcPBq50plkKzrktNxzUHjyyNlVDvi9clyd0HQHR\/V3QMGjDG7czu1uc11NkLPOHM0GP6gvbuxA8j657lQ7JBQ8HXD34JRqeBX4dEJXBw5nFbWpJwEO8bdCmdxBhXVfW\/04RQLyNcafUG8YzcjZBcUSRTcvRBpy67rNiHHpO8vML\/R0tpM7E\/BTYmXekhhlKpRK26axDalKIfAyuwbftvP0Q0n6ggONbvLLe6Z6+UI+2YqIA2IuY4M4DzdOoMFQDRzCtV77tTfIamPkpQS+PqV2Yr+c2enBljfXNbnArPoKj09hFEYjqZCmh4\/5OluSqZn3Ht17NtHACK2W7DGCxLdH0CW67IAbyQWdyvGwIaE3tC9CxjW\/SGEEy8JponKH7enbU74OG1yLDWWkc0coYPDHAEmXYcvSVHaez+2uPzlj0HYTY4hZJvDmgqVOvObwji7xJVwVw+r2mqg9b5e1qDEOqo5Vh9srXNQAMsyM4NNfGTWWbi3nCZ0FPNo0G4VXGazWpQx4Y+qk0dwtFL+wncMO1H6mSG+Re2592XJ5joxQ+5H7l9cdoz9nWKhn0WbzwTZ0wJU7bPTDfxGkfzdFJ3\/UdyjYPPdorkJ2NbECnpG7ZGs00rl8sX0a5mse6LBE7rWbPhl98jJrlaeh5\/BV5fVHxOXL3s+\/6NE8RwG8h+i2Nvh91\/D5VKQ833OfLoaNJckGw0nCQQDv75oS32AXjBzYH\/gxEYtdpj6A59P7ZtvKymulGif0WgIYWMI4s2rlIDNRaZ37Wq6w5vNnmZtUoY4Vme2VreZcTv9be656dqfg7SSm0+2xM1E4MfurncA4VZM6xRoZdPmTVIjTbEoFxghSGutVl04IYgBJi6QyfSt5g+zpq1XCsbOPBaPZzO5Y6P6F+Xf\/nIrFIe0MvMbMV+S9JP5OnYyp5G5d+NwF3aNU1IhnzLFt3EKK7zx8uO\/3GONU\/5N0Bshekk8a2EjRTc0ajkfYDUq3Ehu++So9BUiBriIeL7qS1FjPqH85mXsb9lfwpx6mmxQAMboJ\/X2+65I0x00afg4c6nKdZ1XWstgw8sRcHsFlYbQlZ+G9\/sR3mAd4jmZalMFspA3trKY00Ia6EgtTZt0cb\/jErWPwHHHT+DhSYTpMlrvw9Ea6AG3lb5I\/xigJHFIwXDhqP2winTKdfwYbjADNdJXV34YhwZ0aEBStIhTAcHa8L5jYo00j3Td40Kj4e0ZN7ig1D0s4qzR\/r\/NhzJJPXAMuLKKJ2jrlUU5lbQaaN","hmac":"wPsd75UlSe3kXq0lUKFDQRkCykCFYEZRSgHs7o2U9EU=","created":1325483952,"uuid":"F78CEC04078743B6975511A6FDDBED7E","o":"b3BkYXRhMDE8AAAAAAAAAJomnirvNYqHCY2ZEm\/m5n8WYCNRsXxtt72OnnAC5mxf5u11+GoGT4HxU1lq0AFowmjB7Po4iq+shdgvd\/uVQqLYyjhD0oxhdTElpzyOBF1l04h0d8gbbumDojMLy9WRuevLJkyqHenSUoSfxBz6XUQ="},"F5F099B210F248348E22934DDC3338B2":{"category":"100","k":"uLUIwqVIzBKRuPIl+Acp\/Vh0A4TGpgrWOAUJjx52kiHxIFOKV5gkmpasiWA1Jb+1o9ElNaWMgPgVTpN8+F\/M0w4ODtFIaW5vXlFLgCiN7AYOCL+k3ym5b3TZ8w2\/+omTHvvMqwEpHcOD80pkrqThwA==","updated":1325483952,"tx":1373753420,"d":"b3BkYXRhMDH7BAAAAAAAAM6BGZoTsdH5Eule+fD1M9QiwPF4bm9vmthdGFtfElmzwr+aJgNeIhq1ndSlIkwCPPBbhSV2EML7UNVjKeo65hYnlmkQ4QijK\/fQC+YkHJXs1Rxu4fr+QoT0ppPgPgedPfkmH7NImtL5I29Si\/CPKK9G6DEnVlsRB0Hpt0cxhapHB7ZqS0yyetTUSYbkOADeYee3DIt588s7jQHf1zTPRyilW7ogpLsVbo\/tHpEbaOPZv5nmLi0QKzHpL++7b2gP5J7eBT2jaVZ76MCxcQu9vX0EHnjLdJXLJsfCf\/o6GQ+lx1x4YU8151YpVJ92r5yUtkDG4Ry+gZbJIZWSPwx3OUcNW6WIf9atBFuQiynah+bTJzWViYsQaoEzLYKJ5KoCKb56HqG8B5O9kTqO0OXBaanr55W+S9eIuw2ntTImF2RivMoeM0JlrBxRc\/SgIWYRjr\/tRHwh8n\/tqrTQSku44DdGQFxQeia\/Nx0p2\/1y6Eh+kFj\/pdzBrPYFJl96q22+jXh\/n68nnuZa7B6OxTGmMe7SsRCviD8dLWTgwPw193hBHPXbjaMtjCAI4AsI\/\/zf\/bAEY\/4G80JXXqldEkrRAz\/6DYZHUj2Kh2HwjKE5HiJ\/Ml+FjZL545KHj3pPorRlXpzWw4HgAlYORcAuTDMKA8uQM6XuBgYzjUzjqeXjWz\/JXNXR+FcA0nhIf2\/XTKzqNMwpVeDwutr\/1tkTz6xs8tmcuHJHM3x1IaffzxD4R4f2tRw74Nu6A6\/vp\/byJfOAInBaBqTK4ZwbTT2AogzmIsQG0tAbs88fXglESwZeWcATHHDcNh84rDkbEygOYavVta+te5ZyWlUkGPzGm3mDg1UcGj31ev0E16pWzBVtr4l2xNhe1Yqse0KYUD6lYJ+vlcFMlC6DbMrxMiJc9RWqqlaPe2INf2U9JhYTDcCoBnveUaAB52Sb0PsUZrEFuHVr9I7Yzt5AFxWnM284lCLXeWzOLlHGY\/4FMs59rnyQPnoOU8Lb1Gl2G\/iDufM0hnvk3OiAF0zilJ3s9IAv2mTbvrCjFtwfRkhzrwA3oK13FOCFA\/09N+85nCuzINipLaiUEK2s+bViJdAvzZuWaZRfLPyw2d3rostrBn7fzV70IsJg7nmkl0SnRUP63MAqHbX3gIQzS\/jtcBRrs7MA4UCwxQg5ZRE8CYVi7cz6eWwpa8Cy4yzCe81IrC5qAr4KPx8HoV16dQa1g8XLY2UxSHCA\/k\/guLGT6z1JqBDsKwFTMwXaO74e\/4uY3JN2ePAwnfCcwqHvBfzMQhgM3jlL9T6E77Df+nj87WB3yzMesyA+iNod06A0LTumK90ORSNeblvZ7c\/0UxNmHTJqPkSlMj0VSJPsjXAdEEbY3Cka4BB8ed3pdg+XAO2cm2XioLpN2p4DnsnXkpT0VjBP+4wg517oD\/pHlwV9UevdETTnd1m9w1bzb4WyzK\/7aFiPf5SDOectbc0d6inb36huDD+KrpbAI67KRI4e1Q17u378y1RrUjbGrOqv505j27czAGMxCy4Jwc6RHFULXqtU5UwXoomgiHxtRwyjyrrdTgbhtDMd8UZZ5ez9qtc92szk9yV4jLqVbk8VJuS4yTSSH3gkjQLhBysRzkFfZMQk80KR4BLQ\/O3HUqyKMWfRbN3EUX10Oxi1VkwkN86+f1o0CuZhxp9YqOW7tyB\/dTZEaHKv82HAwfCBmSwPVnAiuuLvEYpouaVMwEbPTYBFI99xG9tsg2k\/K0yyItn\/MWOijZRhBYjz8fM0","hmac":"6v1BQKXgFWz71ifnpKQQQeGPGOlmdu8XxMroYSZT\/kE=","created":1325483952,"uuid":"F5F099B210F248348E22934DDC3338B2","o":"b3BkYXRhMDE\/AAAAAAAAABgY9\/LZi+FMU\/4ddpd8dj8fvGCr8iXRYvDepKFGZ7\/reaoxv0uAMSBmScYUcX7kNcrpv1jf10IAtX2YtrMS1PaaIY\/oi9Ws9RWjI5Q7Gm5\/a0f\/zgfe9WUSWFtc4WDy33Rsp0wPXWzsuh7qp6Cd9SM="},"F3707FA58EA7480884BC6A662658E039":{"category":"004","folder":"617F428170E1455D9503EC75AA103859","k":"m+n9j3ulyOcp0n6NLWM2dT0xNSswKOTGBcKSZf0P2963eaDj+pZVEKUOh\/\/6Ho1a1E1Y2J9GTBG8sZx+rJCI7xt9ADONQ7ULhkSPe2APC3psC5T0Smpz2ZL44n64QSGssJMKaMlDsHU45NH0oa302A==","tx":1373753421,"updated":1325483952,"d":"b3BkYXRhMDE4CAAAAAAAACy3jlcycUCtAnGAye\/OaoRkd497OIPqGGRgdLGpc1Ac0kNA6IBguAkLQgInXP9\/Ekw9VO4C+nXkoruT9hGWWfzOOk6fWTWYC7B\/aQ7gwLoTVeUSBDFTaNf8ik3TM5LOVFYS0YYlEGAU5y1\/TxT74wYCKjq\/tIHVrM05Y+wHVtHC56CFXt\/6YMJ27mM\/qOhP0CAHqsQdJRORgtyGHghlTWp4xfzvzALKQJgSic5EnuNaCLZgQ2C6jet\/HFzyYu2qSb4iRkx25DGWuksK9d+GnGOt5Xj9Tfvfh\/l0L9c+0gBtwkxfJPbQCE\/hxsomzmZIVnIvJzJ+2oXcFpkI+6AIlcx71L+hYmQjz8nlMfbfml3Sa9\/w0zk3i7pncuOVtlXzegH\/KzENnOHxZymRUkAYfR+iiOWUxAQzkSvWxvi3sxq\/EKG8N4J6HCY4jC5eFuw+IhCXBSz\/4KPqhYhglGMj1S0QM40P3fvXyG91vD4gA18kJ4iTc8BfZWixtTktg+rVjigMKNlv8cXfv07MM2xA3nVcDkuE8VYld7jySQwKoVSGwK5yvgWUr9Z6VdKcqY8TCRtHX6sUd8YSUGP1b+JCn0qIBoF5kazJurkmGHN0HWuMO7Fp8DIPQURgNm\/JqnIrav7hZs3ZMeT2niMDWJLNuD1li68ivSX9jEOVCsD6TpVoXOrBATGr\/AoS44glYh06inoWxgB3teOwOZcc06LRH71UbT6tWt4WX9157+4DQ0pCcHh2Sw3SuDRxsBaQ1E5\/2T\/yp3WXpFecHKhqEsGZEbeQSMJ7BLxzFE4EUQWbMuooLRUy0LuzoNpfuWsCyxIYrIMImlvztJbNNlfdXZ\/pjqFRkGUzkFry0yDYEQQlX+6BgaIaN6j8TtMWWgldyWixiYNGR2WGClAJFnNVbby0UuvEqIDFKaHXTm10RjrjcVyRUrSIEAnN1ZpLNPi7bDTWp3iJP8GypFiY17jQQgoC7oIiVIKG77P6FqvRZr2JU\/oYiABKbzZoMDE3jzbLgLcytp0o3Q89qm8ixQ2DvwLT9W07BJxZ9lOUG2nJcYZNXzOzOrRoSdkqpwjax0LKswsr12THtvAN+QvwWQsNzKUIHDmOspHuVcI9lWJSiQWB+Uz6+wR2UMJixriz7FqdrAYmAsqa9C3F7IhsOYfav9gpsq6h\/43uIE9MQkpHspcBGHoi5i9b+QkLzjF6oNhmxLz+jZ\/rWuc\/fGhq2arNb08MxPKJe8bmrNkDvLCMfRVJi+Ga+DU9rPIYqrfmDjBRFUjtyf9fq+RfIyG6xRN\/6uL0DnmA+6mp5Iyk7RJLYqupelxpf+vF1YNPI\/BAwffW4GoK3abvNrgtIjL3rypmcyA6ZaUVrp9euvp7zLGsMtKpdOML42NIl0EVzyCvrNoa9Et1G5X95ZN+eeaZIDPOcPOtKaPK45oOSZu4puji\/hO2yGhthlsvmDhE1hm6o1ACgo4QZHpr6rb9X9YUbHMmKzR8dR1oVX3qzS2vuvU6maj1iv5OZnabEWdizKmcRnpjUyMyZY1Su9fp+gktr003EM7ysjsj1TxloBdxYcxn5JEzrem4BG0MV3l+o90lhyx+BfeXPEnQQ\/Dv6t9wlirwCUeYOOsdoMwXjFzowIyMfcfl+W9jgx6r7d2pLgTjThf7Nw\/+F30HZL0zVJuXVi3nmxGY\/6hP19yeCQFKDQx7bbuHsxIpLwqWh4P5pUomkzhGeJPAHAx9+eeNQnENX9rES0mg7cZOzc1U1\/SDeASXjAdHrxj06p9wtFgR1lhw3SEKjccELfP0PIF0tcErXeBiuanPGj0dvdpRkMhizk5AN1h26xemTP3hepLzxb3ACepVmB12vq+avp0HI6NQTJQyMh7yzNU5rGJXi4BUSXwi+MZzfl5WEXWTEeTqZE1JjPbkdwlSJwFXfjtIOdYtJx\/2TeYNDnCsTHtvK21M5C9aC0uvagNnjySqeEwK9YZvjoaMBFnpkLqiAJVpk7jeAsuMVSy\/7IZGopbpR5IUhG3JoliFJY8SwIIw2P+RFKx+fhZjwJKl+xuumXTs22uLSqajDXJspcxYaB4ZTvos6Od4ryXifdSFkjh+CrOn+LSVeK\/lmrMqxoAk6k05lhRuWQ1C+e798FKy9wSMNFC4KcEk1mWXDrkRam8tRJiZ874V1LNC8jDI+tCyfqXWH+VPJ9VWTcCGvXCrLH28qKUdABEjTGdcsrxulpwH0OsfSvDv9KNgoFT6hopbfLGMEm\/y6mm8mjXXUM4lHXWII22bzRZWYURZKsoFR9Kn2ei911u1Ocp22gcr5S+kbaM6NYdF3I31cEpLuVJp67LyJqNDz4YJFfVlOp91xyAnaADGSYW52VCOPjzcZPaRsvSPhnEzysIs9cVbFYvz6UV7PQLvqxRAfoV8odwtHumMzV41WiQ\/+D1LjmngYWRbT+W4Y2E5RepBXi+errOWH9RjH6mzoM154RK38UqW7H+3z2GvK10vtFdMYGpHWZCEZNMg8fb2EPgBr01U4Mc41TzXSVYQR\/M17oGOmTU51xahsgyuFAkWBirgt6CkSfc4h7CXTyhEPgYhcPRvCWz5+x1N97JykChjonwwaevKwvIrk3f6BDT0KluFGwiIAjJKxCoNJZJWljnUfcrdUquDiajCCZUX4yTsQrbN+bh3HOmn7rNaCrl8wL1hsjCu1zNWGP+xoq\/W7PNs7QuBtU\/a77fWjtafH9aCJaFOTOdlJYTcnzl0x8NHvk6OtSdiPPf\/eohtIDfWbfMhzWC8saUL1BNovLfGFmGeyaR5yOfl9GN9wPpsghMhf62noIsk4rW1ZSBqvI8NySai1pEkm2342nYLWK3ptbpxYiLseKzQJ3nUn60KZjb03+fadpbE8g==","hmac":"dyDETV4dtoJY6ObOQZMlbeygLuJLAn7qJmLQYV\/iLKc=","created":1325483952,"uuid":"F3707FA58EA7480884BC6A662658E039","o":"b3BkYXRhMDFSAAAAAAAAAMc3YaaY9aRpLJ6pTvh2zpTuYPE6E7drcD\/4jLFCpfjwbOGhnb4RgXxf0SJLqhOWjhbibcrx88xVR0wb5jf55DcHAr14d9yHAXHRBDLUvSBw\/M2w09aYYIJX1IuziJu2Tw6IYMNZl18kk5A+HcGnMEoHkzX6XmXNU+csR4C5Q0MHpmrm75TMUxVvnCE6nQrB6A=="},"F7883ADDE5944B349ABB5CBEC20F39BE":{"category":"001","k":"fwEzHy5Z\/KX89M\/0ui1W6jm3KLmqdZ6Cx6qBGRratWGVtsgWpK8rdjnzP9HJ8kERCZ4smAhTPJLLGBNZ\/uDTJweSnyR1ZjKvUfrh2nZEQjmjNOJih\/h9OdwjqVNoRhvHqIC5wDqAA6MmoDbpso8Z0g==","updated":1325483952,"tx":1373753420,"d":"b3BkYXRhMDGYAQAAAAAAAOqwscy8\/oxzwP9rnBpW\/mr8bwiM6LRh4vng7LcB806hOttQ8dFvnd\/PIYkqnvWTwZvLbxtTKk2WrcD8uNkvIztDNBGBLk18rrRcZsosa3Hsdsp2jgpI2iEvaE\/2Lk8IJmlG\/GmhM6K6eKwJ7lefAwGYp52bg0aYfSkB7xxoiffbcDx+Klia+WKV72LIaTAgjz894NBkbcPQ9u3mPXpgjlyk8a5SLnIuondnLt69VBOR81wNVqOxT\/IlpRjU6UAPQEjnOogrNvaGFH7j5Zgw4Px8g60U4ZHysa224mAral6mBK8PBm1Ks5p3y4tw3oO4sqwlpEA1DE56MS1UhstHzK4J4Z31+C06mMFTHs5sOpMKSpAqqghXvTOA8sgRql9p+NDAiSs\/eaaDRxfYmiQXVWbHvae2edLtMiNjUgxvbXhyvM8ENl+Pla9VxNYbg2s6+K7Ct2obx\/oQAbZpo\/h8AUbgugc3hTIvQuMw\/o2DoIuhtmbQqxyc99eeRn6ZUE03XB9pnT\/FM4AUbtgqprV1ebMlf8pS+h9R3zNgB4EVjUXftTH27jFhvn3D2sIIz4ZR2A7nl2KGfJLNjGRpN9LBf0qP0KziORNg1lVlEMwGcAYm","hmac":"CUavpN2qu7Kz+05aLCHNDUYGrsp0D0wFGIShzRdefxc=","created":1325483952,"uuid":"F7883ADDE5944B349ABB5CBEC20F39BE","o":"b3BkYXRhMDGYAAAAAAAAAGRUHVTI1ig3dPmw3gdUasxYzglzq53+WXaerBgPS44zyc6fQEOjLHfD\/qP\/uRqwQvuW+PlRC9gKqFoTrptjy\/ImutcydczWYgEp333LL7KMi7XEy5aJmxrITgHytmdguNn380ZmygliXTvWMZm7N4TkkOPWZ3FXRSGoJ77XEQDmexwJsVdoxUgATnYUwubBLaybVJkcVzGGeAeeFMJxo8Z\/wbsOWV84yxVNRwW5kLyO73x+WMC0zAZG1utUy8qNPW30II9Rs5KRQcYa14Vvl48="},"FD2EADB43C4F4FC7BEB35A1692DDFDEA":{"category":"111","k":"G0L0iCrB6JMFIx6ZwUEAW2UKTDx\/aL0LTZX0EcuPCoU57RRIBRgDuqEy+U7tnyFXYBKc9WDV+cHDVoDpmqTkuHhiYS1t9wR14qVt5mtFMz11sxmLkV+v7InvRE8YzlURVFyuiuUEWvIKhW4rGID0ww==","updated":1370115699,"tx":1373753421,"d":"b3BkYXRhMDHsBAAAAAAAACz3bCFPhL4eK+SSnXE0eXtTyiwpheBzq86mnRW4zOioqHB6KMFfr6yeqHByoAegxYwwoyw5nnBzCkSI0XNwAKIJvRvQq+ObMWT7+WmLvCw+I5DSLN\/+vuS43f7v5IH3u\/UHylJagWPiumCDNZnKmVXQh6J3C0B3VrR8t+YV55lOYgtWMKqwlpK5BRISjO3o6YZolQ5dKqKILiaizHBEru0N35MvaYiTRzm\/n7o85qAEDpr+aw\/1Yjp3sGn1uN+ErRckYTVjd1\/+NORb+P8ZxK\/T7QNpj4tGoWZ9u1UByKVYES+3fkRvB7AVsyLVBCKwT6Yv1D4OmNJ8Ph+nch9JsxotxA5UN1hlDmpLm2yL+dEM3az18sQ1+hlo4cdpRuIxCgBLmEhe6uPnngGuXely1RPrpmbvNJHx5QOZ4YIQTtfnKHXEgf\/e52kVSX3+Uz+\/llP9RU8cURtezE3HUbV\/9cM30UlT+\/kFHAu\/wQVqM6p93Y3KZx4+dCsATkvphIr7t5Zz+lfY4a2BIdptX7tUYyRuhbgWWHtSJVVvztuPbrIhj3qZ00m2YmFtXxHpyyh8PWcTliHXEQMCCRyYvAFEwSLp2B0R+cUXyiAu8jh175x4BUoTgJMOODnVv2vYKW4xGzqm09hXTG3S18ayJ9d\/ndngB55JDd3bMk53pTvUah8Rl46p8pHzd+99useAgpsCIv6RngW129s5l7nEHkmjmX3xnJk6M6yf\/1R8rTGjhlpIsODDTn1L0pJQB9jtJS+D3cySHeqJ6M5sJIS36DA2Oo8bqbWDbW1uzQT53Fu9AmnZOv4I5IkcdsGgAaWyQCsDptNGp+YlE4361wEXYLklTefkrno7YtXuCpTKItEUCyWOgv4csIzTBi0IJVBQ2cqwc72VWonXWRE5sMhKAQx1xe9GMqbUIjYvJW5bpYHij7y8SRtdDNJUHsK2P2qbRflSa7WbGHI41qJnQw\/fmnHwV6VgpGl83wl9rEM3qOFXffZPIDWQ5mt6SAp2244a4a3i3XY6NQe6MMov6yFP\/DCVRJtGEy7OaOioG7c1G4zY+VOIXmsoStCwEMBPVpMZz9hJh\/N\/\/Hug\/4t08btFfP0CY90O6iQJ60SwjAgwQvpumEhGjeU7toSI8ReIsHDIaKlKblw+EpuV6AADys0fPHxodic+z+oaK8y+p7B\/PIW3ytooAleEzmiqnPI89Jem4N6zgpyv4xDK0jdY\/d\/LLN+7\/FqKjd5j0mJloApj\/5V+cpm2vyCPW3t7Prb4WKlfTLbTh7jwxKOBztXfzw3EzF\/g4xpLRJ6BRiIfAM6aCGR9HKtiDjHSvb81XECTLCsCVXW7VfUPBcP9T2jgEI1pR\/Qux5MHcGR02lqjuJmd40zfLB9TRTQqGUfY7tH70QAdOBCLseNTANFizbugu70I1MqlTBSmQ41CGW5SWHQ5iitAGoYODSpoGp5egR1h7Q3NweknKLwpRQQqshYcu22SyFFk3TsFLon8mwckhOcepbtsNZuF021ZFW8o6\/\/L3eTBZStU7aZe0CScWzbLK\/+m057zEy\/cFxPhcXpNpK5gAmJw7GkVZCsmEwatWbTaUiycfgmV+EADmUPMR54IlKMySsLCSiA+lG8\/S6IqhceCUII23NLZ8nlSlL+\/NpZ+uBVVb9QoERUvdAacdHTX2clrwHV74l51JaoswDWw6Lk7ny\/VTG9bijbp6OhMz7yCoN7ec+XIYF3KWI9TAXa1cERLSqXKOjI=","hmac":"jnwgVbZzxznprZqhlBpIe1btAc0bEX2OsuTMjOUVj2M=","created":1370115515,"uuid":"FD2EADB43C4F4FC7BEB35A1692DDFDEA","o":"b3BkYXRhMDFBAAAAAAAAAE9TT\/jOvqEv0b0N2k3f17qRVeaknpZsmka8jOntpaHCjLINsJkYDslj8XK5Y5vXOLFzFyLgL3NdidDWGgaqrg5hL1XL+90ZJpLbLCtNhlicdYzUzXZt28PtvMzGLpZqWJOlYIqiIn+PjQQsn8Yo9cryI2UkssWYr\/\/KpHSpcEeV"},"FF445AB1497241A28812363154E1A738":{"category":"105","k":"Tlqi7ebwFFYMvH1pe76LU+RmWUXkFbjS2lMwNJELGYrHCvhXf3DwJ8lJ2yEReHUz4EeCYymYGKG4As9PFH+5LeOiUD0IZJZCBZFEqf6xgqwLkaGgno3EbFMZaBANg9oxaXGBQEZhizZMCYK00zcbsA==","updated":1373755465,"tx":1373755465,"d":"b3BkYXRhMDFGAgAAAAAAAIwMlBAM\/WYGiSbR3UCrYG3bIqjx1jijY4GqwWY0ylXOCdOB8xVDi6LDCq1pv17mwNlhRzVcTSL4WBzsdHFh3rxblyIiUGtxruvnRV2u\/yPrBLKgsTR1cdF4FhTgWvCyJ7AOrlgY11+Fa3ExjXiPTn7\/FEiOHu8safR9LyRpHKdkW7BcD26BbNCMLfZduO4MDeMds9jTZRZnn3a+lzN4SwCRl7oXpMDgEl2kUdeN3UJVenpHS1hIHzC7miLYSQk5W1h4CXVdEbYSK1u8KBoZ8IRx1WmBBMcUg4h8+gQNIHDff8t9lg8oGWqmNFsNK00\/EkvVleOIiEuwmmb\/FkC0kQGLu52\/Gw03\/Kj3UsC5KxqAGP7ydpXuf3Smyklze+8+q24VEUqs9k8p\/aKkYIWW1RWUd3bWLusN70F7rjNv1sP3UT5LaLybJ+TwepzQV50btqDy35gWtDh2JwWxgqkog7MfpY8baM5Os\/Kd5wvROcP\/Tb7ChhKDF5FF7heSdyQKoDBzbI9u4jvZDhvcJawF4rU8E\/p8cQiran3ueCRHGmWm8DffRHOn9uS12O5yWPPlBM4EWbP7B6JSWv1EtYETCXlq7YzE22xan559zMvvFnEwCSPBMIRouu\/sHfG7UVUA3gg5H\/cuhQYwS4Y9c7pOAraZVhTc3O\/1IUspgiNzN9VQ+U9pT5pNwuX3TBs45nEF56SlxPpWJ+ICUUcQqUKqyC2Y3D0m2KY8yn+fplTZ\/+u4bM7WBEUihL\/dBcmGrwPrpLXi98iVGuHBMLflP3ZigYGagoUX5+OxGP5NDLxJIAVZzpx8sSftRkk8NaOCwQ0iK6vLAyojmkY5Tj2Xc62aWT8=","hmac":"CapriDUPvoQ74ZSxmvSFhaOUs+iYaJ8cfkwEv8XJTq4=","created":1370115718,"uuid":"FF445AB1497241A28812363154E1A738","o":"b3BkYXRhMDFFAAAAAAAAAA+up4RSylo4eV4ZhWai\/wfDemjKUxTJ3v83kNYX3jQoLAyO0Tu0QjSWEpDNZ4YsuxClGlGDgmBChugQS\/7SSLzopMBRccI7F0ED6LgQs1\/MBvOjXovrWFFt9OS9SIRuqPeWeK+gVyHUp\/bvQ2Fy6VfHOdwF+CgLEXvl8bJCMPR7"}}); \ No newline at end of file diff --git a/tests/data/freddy-2013-12-04.opvault/default/folders.js b/tests/data/freddy-2013-12-04.opvault/default/folders.js new file mode 100644 index 0000000000..be2c74dce8 --- /dev/null +++ b/tests/data/freddy-2013-12-04.opvault/default/folders.js @@ -0,0 +1,24 @@ +loadFolders({ + "379A3A7E5D5A47A6AA3A69C4D1E57D1B": { + "created": 0, + "overview": "b3BkYXRhMDESAAAAAAAAAHw2J+nRQ2h7a9jZ8kH4ser/wKowBqgkJxv+RPujmrB7X53ooYk2wxyfiM2par2J44pCxLcNesV9F+jFCIecxGouN+3F033Ktzm3fKC2pGXy", + "tx": 1373753421, + "updated": 0, + "uuid": "379A3A7E5D5A47A6AA3A69C4D1E57D1B" + }, + "617F428170E1455D9503EC75AA103859": { + "created": 0, + "overview": "b3BkYXRhMDEUAAAAAAAAAETNJnQozPInk04UjWvSpyh9PSWcFbetAzkMB+Sh36BPB6nk/FyqwEp2jeuMA0GTuZ/6AChqo0DYSnj3F6E2890seFFtufva2t+j7CI4Ft6J", + "tx": 1373753420, + "updated": 0, + "uuid": "617F428170E1455D9503EC75AA103859" + }, + "AC78552EB06A4F65BEBF58B4D9E32080": { + "created": 1373754128, + "overview": "b3BkYXRhMDGQBgAAAAAAAPk2tT/+vjtAMX6wkwAs8gU284CfdqWHKsN9kFXY40rSTv4f8E1daHNMt2hLAbvR3BjP29168o1taEIYcXVHaZFdYTQ5yl+AvSn+T8aqkQ4sPrggr2Zi2NKuo75Nd/Cd0cnDGBDE3y4q7Gqzoo/Hr0mSfjJFkIbg3PEdY6LRLR2rnHBiqKAshdVyKBXO2maK0O8M7a0fGQN5OG0iHYW8Fo8gJcfBzfVDaUaPhZHj630ONuIVOAXhk6zobzBuYEo9khv1w0ueBrJqvT6jiLa5LhwPfhQmGBBc4scBqJqXFEB8lHoj01V75wkyn1GgVo7CGoSqEbaogFYoa6F14rIFs5pJ3dSbK5qxrz3aAVVZsJ8ouq8fniiRPUmaC/2f2PCy3EB2P0YtlAoE5/fxobq532a+SkAPey5fPxrS7kdgZwBf6zqnbwe8/pmmcNviVxuDz9/19c+Q/UQu0nYg0MS0rnTb3Eui3/UZBMoNkUT9YKKSS4ZZCLEvq88QYCrDTq9OBIMYDyiq3t6Jlq2+ynz9V0rs2uk1o7UpmCr7V7HA2OH9RmpEk1f6LGf/dPocvjPpUD4RR/DUD69EhwCWNkkC2PjE5cRLNnRMjKwb5rhxjkihr9/jTQMqKJQT3/gYHtfZguWrXy1xPB6UmxLv0mwY3BTw+PZa2q/znhyam4xA9Jf425JttoaectShHyMTwsmlcHqkeMLvTdodgOsNAHP+rCy0uuq6CUOlZrX6BRbg6WBs/kPjL8Dg4BwHBw2wh7XvkJkLo8eyErD2jg2Dd3a4s8jErPRZEmDbQJbVYut9fTAJc09LAGAS4O9hg3Esvl5yEhQrC0zoRtHpm/eHKRy0uDTQm5+EhmEAX3zyWjM7O/BoW3q1ZrbVkgmK+814jBzo2/agpiNm9/ZDd7PJkZU1H6M3FMdPamx5qxe7rnrngaxnVvjL6f+1OgYHJ7Zq51Y21Mx4We4dwHU+czMpCcJNoaLtQ8ZBWhNpAYWey6eseazChk7HS2lRSQYdocIgNeWrziXksTIuMBfUsmIGqqzzSda3oMv82Tz3eKKSfgD5MNlB4rGxko9WHZO+pXYZJjhRIvEUTSMPwntfyN9jqZUKgCZgfCI0TjS8NHxzCnuonfgpzO7qNi2kG1U8N/JWf7IfVL5qOAHU5YwzDOgiDAS3Oy5CHvRVvNyi7D2EAP+oSgn9CfsK0OhhhyBkxygPbuhdvljIqJOhVItOtNYJYXeY3QM0Sb1xE5U04wA1cySTRwsiTMO/k5HyTU4V9WySQAB58WtneaAciRNzZUnFdhwC9JlkPNgyZjBrw84wpjdxAbJK1D0T7Kq8l5w5FgwVDGq07JsmDYx8W2ReyuwkLNq7oaSwj+wTpSy7uqVVafa2RoFOLVWNyPnrwHYgF65emnO30ZYYda0wVrOPVvoHB6PcftWdmnBPY2ktd2eGaYeEOE5Vhf26GMOrdbkH0aM0jDLEmFE/O3Rw4ILsRpLXcxxiS/y7MlEAuW6O//sZ9Z5/CjLfswBs9zU+EeduObgKUN78AsPXBzGHcY4QymlcUwMdVdptoa5rJ6+dzrm0l/xyqiAQWhvaVytc5zDXu1b8clfMe4xQEVAxpA5IJXXWUIUoxHEbG95P9rjVeY+Khgjcw0XMkwPBstYka1aZUKe06Cp8fc6XC59Ti5xQC5eywjfNZwHfXNovTx6jeiysDcuyTkDOqG8+B8ls+e73hCPbBljV+dIijAY7DY8+R3bziCRNX73PLH+LfLGBll/IsAY8m+Qoo5jT6t1GS6mVztnbNLwL/NPrOW1ZeEXvO1gR/b2WpjyEc/dmIo6zg8qbYxMRwyAeR7RJf+eRJ/AVj32eZPAGDhhqaE2QAdNvROKTZsLhchTJBN/Xk7oZPrCT2rZ6cJv8x/gHwiyQwEL/g4gldE9/PPFZv39XPEvA62jnHOyzoiToo/FMhtYxCKRxHzvhz8KcBXp5eW5hQMXgmUPouHTtc0S5+rMwKbRYcqEvkYHMWNyU+0gEOWTbvf7OLvFl37c4hCHencsGSeTzf1pbENjO9nx9nV92F6+KrSYXnNJlXUZvXvivr/wnruwf1IDWzPAuLRHfLiuXyPde767du9RVHYR7KPcQcLAAApM9njDlXyMRhW1vK8G5t/PvSdTcbk+lMdeMkW/DhlYWSJv2dOe3x9aFpvLMnP+P8j5XIyMFTDhopQ1LvUIGwFu6WjDpMIo+i/3sK0q4YFOhGF40BquOMsE3Z/Jp4xMRfoq5IrsL5vdOWK/NTswzN5zDOQUcLEgbm+Dt2jwciMm89XbiaLNTfdp/VLlY8M7PGlhsZC/RKBM=", + "smart": true, + "tx": 1373754523, + "updated": 1373754134, + "uuid": "AC78552EB06A4F65BEBF58B4D9E32080" + } +}); \ No newline at end of file diff --git a/tests/data/freddy-2013-12-04.opvault/default/profile.js b/tests/data/freddy-2013-12-04.opvault/default/profile.js new file mode 100644 index 0000000000..90425b5a17 --- /dev/null +++ b/tests/data/freddy-2013-12-04.opvault/default/profile.js @@ -0,0 +1 @@ +var profile={"lastUpdatedBy":"Dropbox","updatedAt":1370323483,"profileName":"default","salt":"P0pOMMN6Ow5wIKOOSsaSQg==","masterKey":"b3BkYXRhMDEAAQAAAAAAACN8JuE76yN6hbjqzEvd0RGnu3vufPcfAZ35JoyzdR1WPRvr8DMefe9MJu65DmHSwjObPC0jznXpafJQob6CNzKCNoeVC+GXIvLckvAuYUNSwILQQ1jEIcHdyQ0H2MbJ+0YlWEbvlQ8UVH5bcrMqDmTPPSRkbUG3/dV1NKHdgI0V6N/kKZ737oo+kj3ChJZQTKywvmR6RgB5et5stBaUwutNQbZ0znYtZumIlf3pjdqGK4RyCHSwmwgLUO+VFLTqDjoZ9dUcy4hQzSZiPlba3vK8vGJRlN0Qf2Y6dUj5kYAwdYdOzE/Ji3hbTNVsPOm8sjzPcPGQj8haW5UgzSDZ0mo7+ymsKJwSYjAsgvawh31WY2m5j7VR+50ERDTEyxxQ3LW7WgetAxX9l0LX0O3Jue1oW/p2l44ij9qiN9rkFScx","iterations":50000,"uuid":"2B894A18997C4638BACC55F2D56A4890","overviewKey":"b3BkYXRhMDFAAAAAAAAAAIy1hZwIGeiLn4mLE1R8lEwIOye95GEyfZcPKlyXkkb0IBTfCXM+aDxjD7hOliuTM/YMIqxK+firVvW3c5cp2QMgvQHpDW2AsAQpBqcgBgRUCSP+THMVg15ZeR9lI77mHBpTQ70D+bchvkSmw3hoEGot7YcnQCATbouhMXIMO52D","createdAt":1373753414}; \ No newline at end of file