Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,8 @@ else()
endif()

add_subdirectory(external/glm)
# Newer system GLM versions require this for gtx/ experimental extensions
add_compile_definitions(GLM_ENABLE_EXPERIMENTAL)
add_subdirectory(external/immer)

# Extract git branch and revision
Expand Down Expand Up @@ -437,7 +439,9 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wno-extra-semi-stmt) # enabled only for src/ directory

# require explicit template parameters when deduction guides are missing
add_compile_options(-Werror=ctad-maybe-unsupported)
# NOTE: Downgraded from -Werror to -Wno because Qt 6.10.x MOC-generated
# code uses CTAD without deduction guides (in qtmochelpers.h).
add_compile_options(-Wno-ctad-maybe-unsupported)

# always errors
add_compile_options(-Werror=cast-qual) # always a mistake unless you added the qualifier yourself.
Expand Down
5 changes: 3 additions & 2 deletions external/immer/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ else()
message(STATUS "Could not find a local immer; downloading to local source tree")
include(ExternalProject)
ExternalProject_Add(immer
URL "https://github.com/arximboldi/immer/archive/v0.8.1.tar.gz"
URL_HASH SHA1=3ab24d01bc6952f5fc0258020271db74f9d30585
GIT_REPOSITORY "https://github.com/arximboldi/immer.git"
GIT_TAG v0.8.1
GIT_SHALLOW ON

SOURCE_DIR "${CMAKE_CURRENT_BINARY_DIR}/immer"
UPDATE_COMMAND ""
Expand Down
2 changes: 1 addition & 1 deletion src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1164,7 +1164,7 @@ if(APPLE)
add_custom_command(
TARGET mmapper
POST_BUILD
COMMAND ${MACDEPLOYQT_APP} ${CMAKE_CURRENT_BINARY_DIR}/mmapper.app -libpath ${QTKEYCHAIN_LIBRARY_DIR} -verbose=2
COMMAND ${MACDEPLOYQT_APP} ${CMAKE_CURRENT_BINARY_DIR}/mmapper.app "-libpath=${QTKEYCHAIN_LIBRARY_DIR}" -verbose=2
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
COMMENT "Deploying the Qt Framework onto the bundle"
VERBATIM
Expand Down
11 changes: 10 additions & 1 deletion src/clock/mumeclock.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,16 @@ class NODISCARD QME final

public:
NODISCARD static int keyToValue(const QString &key) { return g_qme.keyToValue(key.toUtf8()); }
NODISCARD static QString valueToKey(const int value) { return g_qme.valueToKey(value); }
NODISCARD static QString valueToKey(const int value)
{
// Qt 6.10+ changed QMetaEnum::valueToKey() to take quint64.
// Qt 6.5 takes int. Compile-time dispatch avoids warnings on both.
#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
return g_qme.valueToKey(static_cast<quint64>(value));
#else
return g_qme.valueToKey(value);
#endif
}
NODISCARD static int keyCount() { return g_qme.keyCount(); }
};

Expand Down
206 changes: 202 additions & 4 deletions src/configuration/PasswordConfig.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,177 @@

#include "../global/macros.h"

#ifndef MMAPPER_NO_QTKEYCHAIN
#ifdef Q_OS_WASM
#include <cstdlib>
#include <emscripten.h>

// clang-format off
// (JS inside EM_JS macros is not valid C++)

// Encrypt and store a password (AES-GCM-256) in IndexedDB.
// The raw key is stored alongside the ciphertext because Firefox cannot
// store non-extractable CryptoKey objects in IndexedDB (DataCloneError).
//
// Async (non-blocking): returns immediately; the JS promise chain runs
// in the background so the Qt event loop is not stalled.
EM_JS(void, wasm_store_password, (const char *key, const char *password), {
var keyStr = UTF8ToString(key);
var passwordStr = UTF8ToString(password);

if (!Module._passwordSaveQueue) Module._passwordSaveQueue = Promise.resolve();
Module._passwordSaveQueue = Module._passwordSaveQueue.then(async function() {
try {
var cryptoKey = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"]
);

var iv = crypto.getRandomValues(new Uint8Array(12));
var encoded = new TextEncoder().encode(passwordStr);
var ciphertext = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv: iv },
cryptoKey,
encoded
);

var rawKey = new Uint8Array(await crypto.subtle.exportKey("raw", cryptoKey));

var db = await new Promise(function(resolve, reject) {
var req = indexedDB.open("mmapper-credentials", 1);
req.onupgradeneeded = function() {
var db2 = req.result;
if (!db2.objectStoreNames.contains("passwords")) {
db2.createObjectStore("passwords");
}
};
req.onsuccess = function() { resolve(req.result); };
req.onerror = function() { reject(req.error); };
});

await new Promise(function(resolve, reject) {
var tx = db.transaction("passwords", "readwrite");
var store = tx.objectStore("passwords");
store.put({ rawKey: rawKey, iv: iv, ciphertext: new Uint8Array(ciphertext) }, keyStr);
tx.oncomplete = function() { resolve(); };
tx.onerror = function() { reject(tx.error); };
});

db.close();
} catch (e) {
console.error("wasm_store_password error:", e);
}
});
});

// Read and decrypt a password from IndexedDB.
// Returns a malloc'd UTF-8 string on success (may be empty if no password is stored),
// or NULL on error. Caller must free() the returned pointer.
EM_ASYNC_JS(char *, wasm_read_password, (const char *key), {
try {
const keyStr = UTF8ToString(key);

const db = await new Promise((resolve, reject) => {
const req = indexedDB.open("mmapper-credentials", 1);
req.onupgradeneeded = () => {
const db = req.result;
if (!db.objectStoreNames.contains("passwords")) {
db.createObjectStore("passwords");
}
};
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});

const record = await new Promise((resolve, reject) => {
const tx = db.transaction("passwords", "readonly");
const store = tx.objectStore("passwords");
const getReq = store.get(keyStr);
getReq.onsuccess = () => resolve(getReq.result);
getReq.onerror = () => reject(getReq.error);
});

db.close();

if (!record) {
var emptyPtr = _malloc(1);
HEAPU8[emptyPtr] = 0;
return emptyPtr; // not found — empty string, not an error
}

// Import raw key bytes back into a CryptoKey for decryption.
const cryptoKey = await crypto.subtle.importKey(
"raw",
record.rawKey,
{ name: "AES-GCM" },
false, // non-extractable for decryption use
["decrypt"]
);

const decrypted = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv: record.iv },
cryptoKey,
record.ciphertext
);

const password = new TextDecoder().decode(decrypted);
const len = lengthBytesUTF8(password) + 1;
const ptr = _malloc(len);
stringToUTF8(password, ptr, len);
return ptr;
} catch (e) {
console.error("wasm_read_password error:", e);
return 0;
}
});

// Delete a password entry from IndexedDB.
// Async (non-blocking): shares the same promise chain as wasm_store_password
// to prevent a delete from racing with an in-flight store.
EM_JS(void, wasm_delete_password, (const char *key), {
var keyStr = UTF8ToString(key);

if (!Module._passwordSaveQueue) Module._passwordSaveQueue = Promise.resolve();
Module._passwordSaveQueue = Module._passwordSaveQueue.then(async function() {
try {
var db = await new Promise(function(resolve, reject) {
var req = indexedDB.open("mmapper-credentials", 1);
req.onupgradeneeded = function() {
var db2 = req.result;
if (!db2.objectStoreNames.contains("passwords")) {
db2.createObjectStore("passwords");
}
};
req.onsuccess = function() { resolve(req.result); };
req.onerror = function() { reject(req.error); };
});

await new Promise(function(resolve, reject) {
var tx = db.transaction("passwords", "readwrite");
var store = tx.objectStore("passwords");
store["delete"](keyStr);
tx.oncomplete = function() { resolve(); };
tx.onerror = function() { reject(tx.error); };
});

db.close();
} catch (e) {
console.error("wasm_delete_password error:", e);
}
});
});
// clang-format on

static const char *const WASM_PASSWORD_KEY = "password";

#elif !defined(MMAPPER_NO_QTKEYCHAIN)
static const QLatin1String PASSWORD_KEY("password");
static const QLatin1String APP_NAME("org.mume.mmapper");
#endif

PasswordConfig::PasswordConfig(QObject *const parent)
: QObject(parent)
#ifndef MMAPPER_NO_QTKEYCHAIN
#if !defined(Q_OS_WASM) && !defined(MMAPPER_NO_QTKEYCHAIN)
, m_readJob(APP_NAME)
, m_writeJob(APP_NAME)
{
Expand Down Expand Up @@ -40,7 +203,11 @@ PasswordConfig::PasswordConfig(QObject *const parent)

void PasswordConfig::setPassword(const QString &password)
{
#ifndef MMAPPER_NO_QTKEYCHAIN
#ifdef Q_OS_WASM
// Async (non-blocking): errors are logged to the browser console.
const QByteArray utf8 = password.toUtf8();
wasm_store_password(WASM_PASSWORD_KEY, utf8.constData());
#elif !defined(MMAPPER_NO_QTKEYCHAIN)
m_writeJob.setKey(PASSWORD_KEY);
m_writeJob.setTextData(password);
m_writeJob.start();
Expand All @@ -52,10 +219,41 @@ void PasswordConfig::setPassword(const QString &password)

void PasswordConfig::getPassword()
{
#ifndef MMAPPER_NO_QTKEYCHAIN
#ifdef Q_OS_WASM
char *password = wasm_read_password(WASM_PASSWORD_KEY);
if (!password) {
emit sig_error("Failed to retrieve password from browser storage.");
} else if (password[0] == '\0') {
// Not found — no password stored yet. Silent, not an error.
free(password);
} else {
emit sig_incomingPassword(QString::fromUtf8(password));
free(password);
}
#elif !defined(MMAPPER_NO_QTKEYCHAIN)
m_readJob.setKey(PASSWORD_KEY);
m_readJob.start();
#else
emit sig_error("Password retrieval is not available.");
#endif
}

void PasswordConfig::deletePassword()
{
#ifdef Q_OS_WASM
wasm_delete_password(WASM_PASSWORD_KEY);
#elif !defined(MMAPPER_NO_QTKEYCHAIN)
// QtKeychain delete job (async, non-blocking)
auto *deleteJob = new QKeychain::DeletePasswordJob(APP_NAME);
deleteJob->setAutoDelete(true);
deleteJob->setKey(PASSWORD_KEY);
connect(deleteJob, &QKeychain::DeletePasswordJob::finished, [this, deleteJob]() {
if (deleteJob->error()) {
emit sig_error(deleteJob->errorString());
}
});
deleteJob->start();
#else
emit sig_error("Password deletion is not available.");
#endif
}
1 change: 1 addition & 0 deletions src/configuration/PasswordConfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class PasswordConfig final : public QObject

void setPassword(const QString &password);
void getPassword();
void deletePassword();

signals:
void sig_error(const QString &msg);
Expand Down
4 changes: 3 additions & 1 deletion src/configuration/configuration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -666,7 +666,9 @@ void Configuration::AccountSettings::read(const QSettings &conf)
{
accountName = conf.value(KEY_ACCOUNT_NAME, "").toString();
accountPassword = conf.value(KEY_ACCOUNT_PASSWORD, false).toBool();
rememberLogin = NO_QTKEYCHAIN ? false : conf.value(KEY_REMEMBER_LOGIN, false).toBool();
rememberLogin = (NO_QTKEYCHAIN && CURRENT_PLATFORM != PlatformEnum::Wasm)
? false
: conf.value(KEY_REMEMBER_LOGIN, false).toBool();
}

void Configuration::AutoLoadSettings::read(const QSettings &conf)
Expand Down
1 change: 1 addition & 0 deletions src/opengl/LineRendering.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <cassert>

#include <glm/glm.hpp>
#include <glm/gtc/epsilon.hpp>
#include <glm/gtx/norm.hpp>

namespace mmgl {
Expand Down
Loading
Loading