quassel-credentials extract
This commit is contained in:
1 parent
436097a61b
commit
e4c08b177b
3 files changed
+217
No files matched your search
@@ -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
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user