mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2024-10-01 01:26:01 -04:00
Implement 1Password Vault Import
* Support importing 1Password vaults (.opvault folders) into KDBX database * Entry attributes are filled based on section and field name * Expiration dates are set for entries * Entry URL's are set from a wider array of fields
This commit is contained in:
parent
e121f4bc28
commit
125a81f2ed
@ -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
|
||||
|
137
src/format/OpData01.cpp
Normal file
137
src/format/OpData01.cpp
Normal file
@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "OpData01.h"
|
||||
|
||||
#include "crypto/CryptoHash.h"
|
||||
#include "crypto/SymmetricCipher.h"
|
||||
|
||||
#include <QDataStream>
|
||||
#include <QDebug>
|
||||
|
||||
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;
|
||||
}
|
57
src/format/OpData01.h
Normal file
57
src/format/OpData01.h
Normal file
@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef KEEPASSXC_OPDATA01_H
|
||||
#define KEEPASSXC_OPDATA01_H
|
||||
|
||||
#include <QObject>
|
||||
|
||||
/*!
|
||||
* 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
|
478
src/format/OpVaultReader.cpp
Normal file
478
src/format/OpVaultReader.cpp
Normal file
@ -0,0 +1,478 @@
|
||||
/*
|
||||
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#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 <QDebug>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QUuid>
|
||||
#include <gcrypt.h>
|
||||
|
||||
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<CompositeKey>::create();
|
||||
key->addKey(QSharedPointer<PasswordKey>::create(password));
|
||||
|
||||
QScopedPointer<Database> 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<uint>(profileJson["createdAt"].toInt());
|
||||
rootGroupTime.setCreationTime(QDateTime::fromTime_t(createdAt, Qt::UTC));
|
||||
auto updatedAt = static_cast<uint>(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<uint>(folder["created"].toInt());
|
||||
ti.setCreationTime(QDateTime::fromTime_t(createdTime, Qt::UTC));
|
||||
timeInfoOk = true;
|
||||
}
|
||||
if (folder.contains("updated")) {
|
||||
auto updateTime = static_cast<uint>(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<QString, QString> 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');
|
||||
}
|
119
src/format/OpVaultReader.h
Normal file
119
src/format/OpVaultReader.h
Normal file
@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef OPVAULT_READER_H_
|
||||
#define OPVAULT_READER_H_
|
||||
|
||||
#include <QDir>
|
||||
|
||||
#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_ */
|
250
src/format/OpVaultReaderAttachments.cpp
Normal file
250
src/format/OpVaultReaderAttachments.cpp
Normal file
@ -0,0 +1,250 @@
|
||||
/*
|
||||
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "OpData01.h"
|
||||
#include "OpVaultReader.h"
|
||||
|
||||
#include "core/Group.h"
|
||||
#include "core/Tools.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QUuid>
|
||||
|
||||
/*!
|
||||
* 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<unsigned char>(metadataLenBytes[0]);
|
||||
const auto b1 = static_cast<unsigned char>(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<unsigned char>(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);
|
||||
}
|
266
src/format/OpVaultReaderBandEntry.cpp
Normal file
266
src/format/OpVaultReaderBandEntry.cpp
Normal file
@ -0,0 +1,266 @@
|
||||
/*
|
||||
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "OpData01.h"
|
||||
#include "OpVaultReader.h"
|
||||
|
||||
#include "core/Group.h"
|
||||
#include "core/Tools.h"
|
||||
#include "crypto/CryptoHash.h"
|
||||
#include "crypto/SymmetricCipher.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QUuid>
|
||||
|
||||
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<uint>(bandEntry["created"].toInt());
|
||||
ti.setCreationTime(QDateTime::fromTime_t(createdTime, Qt::UTC));
|
||||
timeInfoOk = true;
|
||||
}
|
||||
if (bandEntry.contains("updated")) {
|
||||
auto updateTime = static_cast<uint>(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;
|
||||
}
|
136
src/format/OpVaultReaderSections.cpp
Normal file
136
src/format/OpVaultReaderSections.cpp
Normal file
@ -0,0 +1,136 @@
|
||||
/*
|
||||
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#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 <QDebug>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QUrlQuery>
|
||||
#include <QUuid>
|
||||
|
||||
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);
|
||||
}
|
@ -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<Database>::create();
|
||||
auto* dbWidget = new DatabaseWidget(db, this);
|
||||
addDatabaseTab(dbWidget);
|
||||
dbWidget->switchToImportOpVault(fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to close the current database and remove its tab afterwards.
|
||||
*
|
||||
|
@ -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();
|
||||
|
@ -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<Database> 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<Database> 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<Database> 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<Database> 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();
|
||||
|
@ -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:
|
||||
QPointer<DatabaseSettingsDialog> m_databaseSettingDialog;
|
||||
QPointer<DatabaseOpenWidget> m_databaseOpenWidget;
|
||||
QPointer<KeePass1OpenWidget> m_keepass1OpenWidget;
|
||||
QPointer<OpVaultOpenWidget> m_opVaultOpenWidget;
|
||||
QPointer<GroupView> m_groupView;
|
||||
QPointer<EntryView> m_entryView;
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -200,6 +200,7 @@
|
||||
<string>&Import</string>
|
||||
</property>
|
||||
<addaction name="actionImportKeePass1"/>
|
||||
<addaction name="actionImportOpVault"/>
|
||||
<addaction name="actionImportCsv"/>
|
||||
</widget>
|
||||
<addaction name="actionDatabaseNew"/>
|
||||
@ -603,6 +604,14 @@
|
||||
<string>Import a KeePass 1 database</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionImportOpVault">
|
||||
<property name="text">
|
||||
<string>1Password Vault...</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Import a 1Password Vault</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionImportCsv">
|
||||
<property name="text">
|
||||
<string>CSV file...</string>
|
||||
|
56
src/gui/OpVaultOpenWidget.cpp
Normal file
56
src/gui/OpVaultOpenWidget.cpp
Normal file
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "OpVaultOpenWidget.h"
|
||||
|
||||
#include <QDir>
|
||||
|
||||
#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();
|
||||
}
|
||||
}
|
34
src/gui/OpVaultOpenWidget.h
Normal file
34
src/gui/OpVaultOpenWidget.h
Normal file
@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#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
|
@ -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*)),
|
||||
|
@ -41,6 +41,7 @@ signals:
|
||||
void openDatabase();
|
||||
void openDatabaseFile(QString);
|
||||
void importKeePass1Database();
|
||||
void importOpVaultDatabase();
|
||||
void importCsv();
|
||||
|
||||
protected:
|
||||
|
@ -126,6 +126,13 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonImportOpVault">
|
||||
<property name="text">
|
||||
<string>Import from 1Password</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="buttonImportCSV">
|
||||
<property name="text">
|
||||
|
@ -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})
|
||||
|
||||
|
250
tests/TestOpVaultReader.cpp
Normal file
250
tests/TestOpVaultReader.cpp
Normal file
@ -0,0 +1,250 @@
|
||||
/*
|
||||
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#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 <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QList>
|
||||
#include <QPair>
|
||||
#include <QStringList>
|
||||
#include <QTest>
|
||||
#include <QUuid>
|
||||
|
||||
QTEST_GUILESS_MAIN(TestOpVaultReader)
|
||||
|
||||
QPair<QString, QString>* 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<QString, QString>(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<QUuid, QJsonObject> objectsByUuid;
|
||||
QMap<QString, QList<QJsonObject>> 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<QJsonObject> 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));
|
||||
}
|
48
tests/TestOpVaultReader.h
Normal file
48
tests/TestOpVaultReader.h
Normal file
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef TEST_OPVAULT_READER_H_
|
||||
#define TEST_OPVAULT_READER_H_
|
||||
|
||||
#include <QMap>
|
||||
#include <QObject>
|
||||
|
||||
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<QString, QString> m_categoryMap;
|
||||
};
|
||||
|
||||
#endif /* TEST_OPVAULT_READER_H_ */
|
427
tests/data/freddy-2013-12-04.opvault.txt
Normal file
427
tests/data/freddy-2013-12-04.opvault.txt
Normal file
@ -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
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
1
tests/data/freddy-2013-12-04.opvault/default/band_0.js
Normal file
1
tests/data/freddy-2013-12-04.opvault/default/band_0.js
Normal file
@ -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=="}});
|
24
tests/data/freddy-2013-12-04.opvault/default/band_1.js
Normal file
24
tests/data/freddy-2013-12-04.opvault/default/band_1.js
Normal file
@ -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"
|
||||
}
|
||||
});
|
24
tests/data/freddy-2013-12-04.opvault/default/band_2.js
Normal file
24
tests/data/freddy-2013-12-04.opvault/default/band_2.js
Normal file
@ -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"
|
||||
}
|
||||
});
|
25
tests/data/freddy-2013-12-04.opvault/default/band_3.js
Normal file
25
tests/data/freddy-2013-12-04.opvault/default/band_3.js
Normal file
@ -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"
|
||||
}
|
||||
});
|
26
tests/data/freddy-2013-12-04.opvault/default/band_4.js
Normal file
26
tests/data/freddy-2013-12-04.opvault/default/band_4.js
Normal file
@ -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"
|
||||
}
|
||||
});
|
14
tests/data/freddy-2013-12-04.opvault/default/band_5.js
Normal file
14
tests/data/freddy-2013-12-04.opvault/default/band_5.js
Normal file
@ -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"
|
||||
}
|
||||
});
|
13
tests/data/freddy-2013-12-04.opvault/default/band_6.js
Normal file
13
tests/data/freddy-2013-12-04.opvault/default/band_6.js
Normal file
@ -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"
|
||||
}
|
||||
});
|
13
tests/data/freddy-2013-12-04.opvault/default/band_7.js
Normal file
13
tests/data/freddy-2013-12-04.opvault/default/band_7.js
Normal file
@ -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"
|
||||
}
|
||||
});
|
13
tests/data/freddy-2013-12-04.opvault/default/band_8.js
Normal file
13
tests/data/freddy-2013-12-04.opvault/default/band_8.js
Normal file
@ -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"
|
||||
}
|
||||
});
|
1
tests/data/freddy-2013-12-04.opvault/default/band_A.js
Normal file
1
tests/data/freddy-2013-12-04.opvault/default/band_A.js
Normal file
@ -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}});
|
37
tests/data/freddy-2013-12-04.opvault/default/band_D.js
Normal file
37
tests/data/freddy-2013-12-04.opvault/default/band_D.js
Normal file
@ -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"
|
||||
}
|
||||
});
|
37
tests/data/freddy-2013-12-04.opvault/default/band_E.js
Normal file
37
tests/data/freddy-2013-12-04.opvault/default/band_E.js
Normal file
@ -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"
|
||||
}
|
||||
});
|
1
tests/data/freddy-2013-12-04.opvault/default/band_F.js
Normal file
1
tests/data/freddy-2013-12-04.opvault/default/band_F.js
Normal file
File diff suppressed because one or more lines are too long
24
tests/data/freddy-2013-12-04.opvault/default/folders.js
Normal file
24
tests/data/freddy-2013-12-04.opvault/default/folders.js
Normal file
@ -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"
|
||||
}
|
||||
});
|
1
tests/data/freddy-2013-12-04.opvault/default/profile.js
Normal file
1
tests/data/freddy-2013-12-04.opvault/default/profile.js
Normal file
@ -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};
|
Loading…
Reference in New Issue
Block a user