diff --git a/security/quassel-credentials/Makefile b/security/quassel-credentials/Makefile new file mode 100644 index 0000000..223b9cd --- /dev/null +++ b/security/quassel-credentials/Makefile @@ -0,0 +1,12 @@ +CXX ?= g++ +CXXFLAGS ?= -O2 -std=c++17 -Wall -Wextra -Wpedantic +LDLIBS += -lsqlite3 + +.PHONY: all clean +all: restore-nickserv + +restore-nickserv: main.cpp + $(CXX) $(CPPFLAGS) $(CXXFLAGS) $(LDFLAGS) main.cpp $(LDLIBS) -o $@ + +clean: + rm -f restore-nickserv diff --git a/security/quassel-credentials/README.md b/security/quassel-credentials/README.md new file mode 100644 index 0000000..89709b1 --- /dev/null +++ b/security/quassel-credentials/README.md @@ -0,0 +1,57 @@ +# Recover NickServ credentials from a Quassel database + +A C++17 command-line utility for the SQLite schema used by Quassel 0.14.0. +It takes one argument: the path to the database. The database is opened with +`SQLITE_OPEN_READONLY`; the utility neither creates a missing database nor modifies stored data. + +## Build and run + +Dependencies on Debian/Ubuntu: `build-essential libsqlite3-dev`. + +From the `restore` directory: + +```sh +make +./restore-nickserv "$HOME/.config/quassel-irc.org/quassel-storage.sqlite" +``` + +Alternatively, build without make: + +```sh +g++ -std=c++17 -O2 -Wall -Wextra -Wpedantic main.cpp -lsqlite3 -o restore-nickserv +``` + +The database path may differ, for example if Quassel uses `--configdir`. +For a straightforward recovery, close Quassel before reading the database. +If working with a copy, copy the database after Quassel has shut down normally: +while SQLite is running, the latest data may be in the adjacent `-wal` file. + +## Output + +- All networks belonging to all core users, even when auto-identification is disabled. +- The NickServ service and saved password (`autoidentifyservice`, `autoidentifypassword`). +- The associated identity, real name, ident, and all configured nicknames in order. +- Server addresses, ports, and TLS settings. +- The SASL account and password: an alternative way to authenticate to an IRC account. +- Connect, attach, and detach commands, which may also contain an `IDENTIFY` command. + +Quassel does not store a separate NickServ account name in its auto-identification +settings: the code sends ` IDENTIFY `. Nicknames are therefore +listed separately; they are not guaranteed to match the registered account name. + +Strings are enclosed in quotes. `NULL` means no value is stored; `""` means an empty +string. Newlines, tabs, quotes, and backslashes are escaped as `\n`, `\t`, `\"`, +and `\\`; other control characters are escaped as `\xHH`. +Escaping is only used for display and is not part of the password. + +The output contains passwords in plain text. To save it to a file accessible only +to its owner: + +```sh +(umask 077; ./restore-nickserv /path/to/quassel-storage.sqlite > recovered.txt) +``` + +The utility extracts saved values but does not check whether they are still valid +on the IRC server. If a password was never saved, it cannot be recovered from this database. +Exit codes: `0` for success, `1` for a read/output error or an incompatible schema, +and `2` for an incorrect number of arguments. diff --git a/security/quassel-credentials/main.cpp b/security/quassel-credentials/main.cpp new file mode 100644 index 0000000..7fa9ad6 --- /dev/null +++ b/security/quassel-credentials/main.cpp @@ -0,0 +1,148 @@ +#include + +#include +#include +#include +#include + +using Database = std::unique_ptr; + +class Query { +public: + Query(sqlite3* db, const char* sql) : db_(db) { + sqlite3_stmt* raw = nullptr; + const int result = sqlite3_prepare_v2(db, sql, -1, &raw, nullptr); + statement_.reset(raw); + if (result != SQLITE_OK) + throw std::runtime_error(sqlite3_errmsg(db)); + } + + void bind(int index, sqlite3_int64 value) { + if (sqlite3_bind_int64(statement_.get(), index, value) != SQLITE_OK) + throw std::runtime_error(sqlite3_errmsg(db_)); + } + + bool next() { + const int result = sqlite3_step(statement_.get()); + if (result == SQLITE_ROW) return true; + if (result == SQLITE_DONE) return false; + throw std::runtime_error(sqlite3_errmsg(db_)); + } + + sqlite3_int64 number(int column) const { + return sqlite3_column_int64(statement_.get(), column); + } + + // Quote strings and escape control characters, including terminal escapes. + // NULL and an empty string remain distinguishable. + std::string value(int column) const { + if (sqlite3_column_type(statement_.get(), column) == SQLITE_NULL) + return "NULL"; + const auto* data = sqlite3_column_text(statement_.get(), column); + if (!data) throw std::runtime_error("Cannot read SQLite text value"); + const int size = sqlite3_column_bytes(statement_.get(), column); + const char* hex = "0123456789abcdef"; + std::string result = "\""; + for (int i = 0; i < size; ++i) { + const unsigned char c = data[i]; + switch (c) { + case '\\': result += "\\\\"; break; + case '"': result += "\\\""; break; + case '\n': result += "\\n"; break; + case '\r': result += "\\r"; break; + case '\t': result += "\\t"; break; + default: + if (c < 32 || c == 127) { + result += "\\x"; + result += hex[c >> 4]; + result += hex[c & 15]; + } else { + result += static_cast(c); + } + } + } + return result + '"'; + } + +private: + sqlite3* db_; + std::unique_ptr + statement_{nullptr, sqlite3_finalize}; +}; + +int main(int argc, char** argv) { + if (argc != 2 || std::string(argv[1]) == "--help") { + std::cerr << "Usage: " << argv[0] << " /path/to/quassel-storage.sqlite\n" + << "Read-only NickServ/SASL recovery for Quassel 0.14.0.\n" + << "Output includes saved passwords in clear text.\n"; + return argc == 2 ? 0 : 2; + } + + try { + sqlite3* raw = nullptr; + const int result = sqlite3_open_v2(argv[1], &raw, SQLITE_OPEN_READONLY, nullptr); + Database db(raw, sqlite3_close); + if (result != SQLITE_OK) + throw std::runtime_error(raw ? sqlite3_errmsg(raw) : "Cannot allocate SQLite connection"); + sqlite3_busy_timeout(db.get(), 3000); + + // Keep all queries in the same consistent read snapshot. + Query begin(db.get(), "BEGIN"); + begin.next(); + Query networks(db.get(), + "SELECT n.networkid, n.userid, u.username, n.networkname, n.identityid, " + "i.identityname, i.realname, i.ident, n.useautoidentify, " + "n.autoidentifyservice, n.autoidentifypassword, n.usesasl, " + "n.saslaccount, n.saslpassword, n.perform, n.attachperform, n.detachperform " + "FROM network n LEFT JOIN quasseluser u ON u.userid = n.userid " + "LEFT JOIN identity i ON i.identityid = n.identityid AND i.userid = n.userid " + "ORDER BY n.userid, n.networkid"); + + int count = 0; + while (networks.next()) { + ++count; + std::cout << "\nNetwork " << networks.number(0) << ": " << networks.value(3) + << "\n Core user: " << networks.value(2) + << " (id " << networks.number(1) << ")" + << "\n Identity: " << networks.value(5) + << " (id " << networks.number(4) << ")" + << "\n Real name: " << networks.value(6) + << "\n Ident: " << networks.value(7) << '\n'; + + Query nicks(db.get(), + "SELECT nick FROM identity_nick WHERE identityid = ? ORDER BY nickid"); + nicks.bind(1, networks.number(4)); + while (nicks.next()) std::cout << " Nick: " << nicks.value(0) << '\n'; + + Query servers(db.get(), + "SELECT hostname, port, ssl FROM ircserver " + "WHERE networkid = ? AND userid = ? ORDER BY serverid"); + servers.bind(1, networks.number(0)); + servers.bind(2, networks.number(1)); + while (servers.next()) { + std::cout << " Server: " << servers.value(0) << ':' << servers.number(1) + << " (TLS " << (servers.number(2) ? "yes" : "no") << ")\n"; + } + + std::cout << " NickServ auto-identify enabled: " << (networks.number(8) ? "yes" : "no") + << "\n NickServ service: " << networks.value(9) + << "\n NickServ password: " << networks.value(10) + << "\n SASL enabled: " << (networks.number(11) ? "yes" : "no") + << "\n SASL account: " << networks.value(12) + << "\n SASL password: " << networks.value(13) + << "\n Connect commands (perform): " << networks.value(14) + << "\n Attach commands: " << networks.value(15) + << "\n Detach commands: " << networks.value(16) << '\n'; + } + Query commit(db.get(), "COMMIT"); + commit.next(); + std::cout << "\nNetworks found: " << count << '\n'; + std::cout.flush(); + if (!std::cout) throw std::runtime_error("Cannot write output"); + } catch (const std::exception& error) { + std::cerr << "Error: " << error.what() + << "\nExpected a Quassel 0.14.0 SQLite database.\n"; + return 1; + } + return 0; +}