quassel-credentials extract

This commit is contained in:
deeaitch committed 2026-09-29 18:45:19 -04:00
1 parent 436097a61b
commit e4c08b177b
3 files changed
+217

No files matched your search

+12
View File
@@ -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
+57
View File
@@ -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 `<service> IDENTIFY <password>`. 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.
+148
View File
@@ -0,0 +1,148 @@
#include <sqlite3.h>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string>
using Database = std::unique_ptr<sqlite3, decltype(&sqlite3_close)>;
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<char>(c);
}
}
}
return result + '"';
}
private:
sqlite3* db_;
std::unique_ptr<sqlite3_stmt, decltype(&sqlite3_finalize)>
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;
}