最終更新日時:

SQLiteを使ってパスワード管理ソフトを作ってみる

はじめに

 昨今は何かしらの形式でパスワード管理をすることも多いだろう。かくいう私もパスワード管理ソフトを使っているが、なんとなく命を握られている感じがして気に入らない。というのは建前で、SQLiteをC++から直接利用してみたかったため、SQLiteを用いてC++でパスワード管理ソフトを作ってみる。
 今回作成をしたソースコードは以下で公開している。

SQLiteのC++ラッパーの実装

SQLiteの導入

 SQLite自体はC言語で実装がされているため、C++から利用する分には非常に容易である。今回は以下のダウンロードページにあるバージョン3.45.1のソースをビルドして利用をしている。

 現バージョン時点では、sqlite3.cをビルドしてリンク&sqlite3.hのインクルードをするだけで簡単に利用ができる。SQLiteのAPIを利用する際は主にドキュメントを参照することになるが、利用をする際は特に以下のドキュメントが参考になる。

ラッパーの構造

 ここでのラッパーは、単純なSQLiteのC言語のインターフェースをC++によるオブジェクト指向なインターフェースに変換して利用しやすくするだけとして、高度な抽象化は行わないものとする。今回主に作成したクラスは以下の3つの機能である。なお、ここでのviewというのはC++のRangeにおけるviewを指す。

  • SQLiteConnection/SQLite:SQLiteとのコネクションを管理するクラス

  • SQLiteStmtControl/SQLiteStmt:プリペアドステートメントを管理するクラスであり、インスタンスはSQLiteConnection/SQLiteから生成される

  • SQLiteIterator/SQLiteView:SQLの実行結果に関するviewとイテレータであり、インスタンスはSQLiteStmtControl/SQLiteStmtから生成される

 上記は単純ではあるが、各クラスから生成されるインスタンスの生存期間を考慮すると、単純にインスタンスが破棄されるときに、例えばSQLの実行途中にSQLiteとのコネクションを破棄するという操作が行われてしまいエラーが発生するということも考えられる。そのため、これらのインスタンスの生存期間も考慮しながら適宜コネクション等を延長するようにする必要がある。

ラッパーの実装

 ここからは具体的にラッパーを実装する。

SQLiteConnection/SQLite

 まずは、実際にSQLiteとのコネクションを管理するSQLiteConnectionを示す。SQLiteConnectionで用いているSQLiteのAPIは以下のとおりである。

  • sqlite3_open:SQLiteとのコネクションを確立する

  • sqlite3_close:SQLiteとのコネクションを破棄する(実行中のSQLが存在したりするとエラー)

/// <summary>
/// SQLiteのコネクションを管理するクラス
/// </summary>
struct SQLiteConnection {
	/// <summary>
	/// SQLiteとのコネクションのハンドラ
	/// </summary>
	sqlite3* conn = nullptr;

	~SQLiteConnection();

	/// <summary>
	/// SQLiteとのコネクションを確立する
	/// </summary>
	/// <param name="path">データベースへのパス</param>
	void connect(const std::filesystem::path& path);

	/// <summary>
	/// SQLiteとのコネクションを切断する
	/// </summary>
	void disconnect();
};

SQLiteConnection::~SQLiteConnection() {
    try {
        this->disconnect();
    }
    catch (const std::runtime_error& e) {
        std::cerr << e.what() << std::endl;
    }
}

void SQLiteConnection::connect(const std::filesystem::path& path) {
    this->disconnect();

    if (sqlite3_open(
        // パスはUTF8である必要がある
        reinterpret_cast<const char*>(path.u8string().data()),
        &this->conn
    ) != SQLITE_OK) {
        this->disconnect();
        throw std::runtime_error("SQLiteとの接続の確立に失敗");
    }
}

void SQLiteConnection::disconnect() {
    if (this->conn != nullptr) {
        if (sqlite3_close(this->conn) != SQLITE_OK) {
            this->conn = nullptr;
            throw std::runtime_error("SQLiteとの接続の切断に失敗");
        }
        this->conn = nullptr;
    }
}
C++

 SQLiteConnection自体は次に示すSQLiteで管理されるものであり、ユーザが直接生成するべきではない(生成する場合は自己責任である)。SQLiteはSQLiteのコネクションおよびSQL文の実行のためのクラスであり、後述するプリペアドステートメントを扱うクラスSQLiteStmtもこれから生成される。SQLiteで用いているSQLiteのAPIは以下のとおりである。

  • sqlite3_exec:プリペアドステートメントを利用してSQL文を実行する操作に関して、1つの関数で実現するラッパー

  • sqlite3_free:SQLite内部で確保されたメモリの開放

  • sqlite3_prepare_v2:プリペアドステートメントを生成

/// <summary>
/// SQLiteに関する操作の起点となるクラス
/// </summary>
class SQLite {
	/// <summary>
	/// SQLiteとのコネクションのハンドラ
	/// </summary>
	std::shared_ptr<SQLiteConnection> _conn;

public:
	SQLite(const std::filesystem::path& path);

	/// <summary>
	/// trueならSQLiteとのコネクションが存在する
	/// </summary>
	operator bool() const { return this->_conn->conn != nullptr; }

	/// <summary>
	/// SQLを実行する
	/// </summary>
	/// <param name="sql">実行するSQL</param>
	void exec(const std::u8string& sql);

	/// <summary>
	/// プリペアドステートメントを作成する
	/// </summary>
	/// <param name="sql">実行するSQL</param>
	[[nodiscard]] SQLiteStmt prepare(const std::u8string& sql);
};

SQLite::SQLite(const std::filesystem::path& path) : _conn(new SQLiteConnection) {
    this->_conn->connect(path);
}

void SQLite::exec(const std::u8string& sql) {
    char* errMsg = nullptr;
    // SQLを実行
    if (sqlite3_exec(
        this->_conn->conn,
        std::bit_cast<const char*>(sql.data()),
        nullptr,
        nullptr,
        &errMsg
    ) != SQLITE_OK) {
        std::string errMsg2 = std::string("SQL error: ") + errMsg;
        sqlite3_free(errMsg);
        throw std::runtime_error(errMsg2);
    }
}

SQLiteStmt SQLite::prepare(const std::u8string& sql) {
    sqlite3_stmt* stmt = nullptr;
    // プリペアドステートメントを作成
    if (sqlite3_prepare_v2(
        this->_conn->conn,
        std::bit_cast<const char*>(sql.data()),
        -1,
        &stmt,
        nullptr) != SQLITE_OK) {
        throw std::logic_error(std::string("SQL error: ") + sqlite3_errmsg(this->_conn->conn));
    }
    return SQLiteStmt(std::shared_ptr<SQLiteStmtControl>(new SQLiteStmtControl(this->_conn, *stmt, 0)));
}
C++

SQLiteStmtControl/SQLiteStmt

 次に、プリペアドステートメントを扱うSQLiteStmtSQLiteStmtControlを示す。SQLiteStmtは単一のSQL文から構成される文字列(単一のSQL文ではない)に対して生成されるプリペアドステートメントであり、本質的に一意である必要がある。そのため、SQLiteStmtはコピーを禁止する必要がある。SQLiteStmtControlSQLiteに対するSQLiteConnectionと似たような役割を持つものであり、プリペアドステートメントを破棄するタイミングを制御する。SQLが実行中等によりプリペアドステートメントの意図しない破棄が発生しないようにするものであるが、破棄するタイミングがSQLの実行の完了や中断が絡むこと、プリペアドステートメントの使いまわしを考慮すると、スマートポインタのデストラクタにより管理するのはあまり意味がないし、考慮する事項が増えるだけで面倒である。そのため、適宜ビットフラグを用いて管理をする。SQLiteStmtおよびSQLiteStmtControlで用いているSQLiteのAPIは以下のとおりである。

/// <summary>
/// バインドを行う型(今回は利用するやつだけ定義する)
/// </summary>
template <class T>
concept bind_value = std::disjunction_v<
	std::is_same<T, std::u8string_view>,
	std::is_same<T, std::u8string>,
	std::is_same<T, std::chrono::utc_seconds>,
	std::is_same<T, nullptr_t>,
	std::is_same<T, std::vector<unsigned char>>
>;

/// <summary>
/// SQLiteでSQL文を実行するためのクラス
/// </summary>
class SQLiteStmt {
	/// <summary>
	/// sqlite3_stmtの制御のための変数
	/// </summary>
	std::shared_ptr<SQLiteStmtControl> _control;

public:
	SQLiteStmt() = delete;
	SQLiteStmt(std::shared_ptr<SQLiteStmtControl> control);
	~SQLiteStmt();

	void bind(int index, std::u8string_view data);
	void bind(int index, const std::u8string& data);
	void bind(int index, const std::chrono::utc_seconds& data);
	void bind(int index, nullptr_t);
	void bind(int index, const std::vector<unsigned char>& data);
	template <bind_value T>
	void bind(int index, const std::optional<T>& data) {
		if (data) {
			this->bind(index, data.value());
		}
		else {
			this->bind(index, nullptr);
		}
	}

	/// <summary>
	/// SQLの実行結果の取得のためのViewを生成する
	/// </summary>
	/// <returns>SQLの実行結果の取得のためのView</returns>
	[[nodiscard]] SQLiteView exec();

	SQLiteStmt(SQLiteStmt&& x) noexcept;
	SQLiteStmt& operator=(SQLiteStmt&& x) noexcept;

	// コピーによる構築を禁止する
	SQLiteStmt(const SQLiteStmt&) = delete;
	SQLiteStmt& operator=(const SQLiteStmt&) = delete;
};

SQLiteStmt::SQLiteStmt(std::shared_ptr<SQLiteStmtControl> control) : _control(control) {
    this->_control->keep(SQLiteStmtControl::ENABLE_SQLITE_STMT);
}

SQLiteStmt::~SQLiteStmt() {
    this->_control->dispose(SQLiteStmtControl::ENABLE_SQLITE_STMT);
}

void SQLiteStmt::bind(int index, std::u8string_view data) {
    sqlite3_bind_text(this->_control->stmt, index, std::bit_cast<const char*>(data.data()), static_cast<int>(data.size()), SQLITE_STATIC);
}

void SQLiteStmt::bind(int index, const std::u8string& data) {
    sqlite3_bind_text(this->_control->stmt, index, std::bit_cast<const char*>(data.data()), -1, SQLITE_STATIC);
}

void SQLiteStmt::bind(int index, const std::chrono::utc_seconds& data) {
    // 明示的にコピーをバインドする
    sqlite3_bind_text(this->_control->stmt, index, std::format("{:%Y-%m-%d %H:%M:%S}", data).c_str(), -1, SQLITE_TRANSIENT);
}

void SQLiteStmt::bind(int index, nullptr_t) {
    sqlite3_bind_null(this->_control->stmt, index);
}

void SQLiteStmt::bind(int index, const std::vector<unsigned char>& data) {
    sqlite3_bind_blob64(this->_control->stmt, index, data.data(), static_cast<sqlite3_uint64>(data.size()), SQLITE_STATIC);
}

SQLiteView SQLiteStmt::exec() {
    if ((this->_control->control & (SQLiteStmtControl::ENABLE_SQLITE_VIEW | SQLiteStmtControl::ENABLE_SQLITE_ITERATOR)) != 0) {
        throw std::logic_error("有効なSQLiteViewあるいはSQLiteIteratorが存在しているためSQLiteViewを生成することはできません");
    }

    // SQLの実行状態をリセットする
    if (sqlite3_reset(this->_control->stmt) != SQLITE_OK) {
        throw std::runtime_error(std::string("SQL error: ") + sqlite3_errmsg(sqlite3_db_handle(this->_control->stmt)));
    }
    return SQLiteView(this->_control);
}

SQLiteStmt::SQLiteStmt(SQLiteStmt&& x) noexcept {
    *this = std::move(x);
}

SQLiteStmt& SQLiteStmt::operator=(SQLiteStmt&& x) noexcept {
    this->_control = std::move(x._control);
    return *this;
}
C++
/// <summary>
/// SQLiteStmtに関するインスタンスを制御するクラス
/// </summary>
struct SQLiteStmtControl {
	/// <summary>
	/// SQLiteとのコネクションのハンドラ
	/// </summary>
	std::shared_ptr<SQLiteConnection> conn;
	/// <summary>
	/// 実行するSQLについてのステートメント
	/// </summary>
	sqlite3_stmt* stmt = nullptr;
	/// <summary>
	/// sqlite3_stmtの制御のための変数
	/// </summary>
	std::size_t control = 0;

	/// <summary>
	/// sqlite3_stmtを保持する
	/// </summary>
	void keep(std::size_t mask) noexcept;

	/// <summary>
	/// sqlite3_stmtを破棄する
	/// </summary>
	void dispose(std::size_t mask) noexcept;

	SQLiteStmtControl(std::shared_ptr<SQLiteConnection> conn, sqlite3_stmt& stmt, std::size_t control);
	~SQLiteStmtControl();

	SQLiteStmtControl(SQLiteStmtControl&& x) noexcept;
	SQLiteStmtControl& operator=(SQLiteStmtControl&&) noexcept;

	/// <summary>
	/// SQLiteStmtが有効であることを示すビットフラグ
	/// </summary>
	static constexpr std::size_t ENABLE_SQLITE_STMT = 0b1;
	/// <summary>
	/// SQLiteViewが有効であることを示すビットフラグ
	/// </summary>
	static constexpr std::size_t ENABLE_SQLITE_VIEW = 0b10;
	/// <summary>
	/// SQLiteIteratorが有効であることを示すビットフラグ
	/// </summary>
	static constexpr std::size_t ENABLE_SQLITE_ITERATOR = 0b100;
};

void SQLiteStmtControl::keep(std::size_t mask) noexcept {
    this->control |= mask;
}

void SQLiteStmtControl::dispose(std::size_t mask) noexcept {
    if (this->stmt != nullptr) {
        this->control &= ~mask;
        if (this->control == 0) {
            // 他で利用されていない場合でのみ開放(SQLITE_ERRORを返すとしてもエラーとは限らないため無視)
            sqlite3_finalize(this->stmt);
            this->stmt = nullptr;
        }
    }
}

SQLiteStmtControl::SQLiteStmtControl(std::shared_ptr<SQLiteConnection> conn, sqlite3_stmt& stmt, std::size_t control) : conn(conn), stmt(std::addressof(stmt)), control(control) {}

SQLiteStmtControl::~SQLiteStmtControl() {
    this->dispose(~0);
}

SQLiteStmtControl::SQLiteStmtControl(SQLiteStmtControl&& x) noexcept {
    *this = std::move(x);
}

SQLiteStmtControl& SQLiteStmtControl::operator=(SQLiteStmtControl&& x) noexcept {
    this->dispose(~0);
    this->conn = std::move(x.conn);
    this->stmt = x.stmt;
    this->control = x.control;
    x.stmt = nullptr;
    x.control = 0;
    return *this;
}
C++

SQLiteIterator/SQLiteView

 次に、SQLの実行結果を走査するためのSQLiteViewを示す。SQLiteViewについても本質的にSQLiteStmtと近い性質を持つため、コピーは禁止する必要がある。また、SQLiteViewSQLiteStmtからのみ生成できるようにコンストラクタをprivateなメンバとして実装し、SQLiteStmtをフレンドクラスとして加える。SQLiteViewで用いているSQLiteのAPIは以下のとおりである。

/// <summary>
/// SQLiteでSQL文を実行結果をイテレートするためのView
/// </summary>
class SQLiteView : public std::ranges::view_interface<SQLiteView> {
    /// <summary>
    /// sqlite3_stmtの制御のための変数
    /// </summary>
    std::shared_ptr<SQLiteStmtControl> _control;
    /// <summary>
    /// beginが既に呼び出されたことがあるかを示すフラグ
    /// </summary>
    mutable bool _beginCalled = false;

    friend SQLiteStmt;
    SQLiteView(std::shared_ptr<SQLiteStmtControl> control);
public:
    SQLiteView() = delete;
    ~SQLiteView();

    [[nodiscard]] SQLiteIterator begin() const;
    [[nodiscard]] SQLiteViewSentinel end() const;

    SQLiteView(SQLiteView&& x) noexcept;
    SQLiteView& operator=(SQLiteView&& x) noexcept;

    // コピーによる構築を禁止する
    SQLiteView(const SQLiteView&) = delete;
    SQLiteView& operator=(const SQLiteView&) = delete;
};

SQLiteView::SQLiteView(std::shared_ptr<SQLiteStmtControl> control) : _control(control) {
	this->_control->keep(SQLiteStmtControl::ENABLE_SQLITE_VIEW);
}

SQLiteView::~SQLiteView() {
	this->_control->dispose(SQLiteStmtControl::ENABLE_SQLITE_VIEW);
}

SQLiteIterator SQLiteView::begin() const {
	if (this->_beginCalled) {
		throw std::logic_error("2回以上beginを呼び出すことは不正です");
	}

	// SQLの1行目の取得を試みる
	int prevStep = sqlite3_step(this->_control->stmt);
	if (prevStep != SQLITE_ROW && prevStep != SQLITE_DONE) {
		throw std::runtime_error(std::string("SQL error: ") + sqlite3_errmsg(sqlite3_db_handle(this->_control->stmt)));
	}
	this->_beginCalled = true;
	return SQLiteIterator(this->_control, prevStep);
}

SQLiteViewSentinel SQLiteView::end() const {
	return SQLiteViewSentinel();
}

SQLiteView::SQLiteView(SQLiteView&& x) noexcept {
	*this = std::move(x);
}

SQLiteView& SQLiteView::operator=(SQLiteView&& x) noexcept {
	this->_control = std::move(x._control);
	return *this;
}
C++

 最後に、SQLiteViewによる走査のための部品であるSQLiteIteratorを示す。SQLiteIteratorSQLiteViewの部品であるため、コピーは禁止する必要があり、SQLiteViewからのみ生成できるようにする必要がある。また、SQLiteIteratorは単一のクラスとして実現されるのではなく、番兵を示すSQLiteViewSentinelとSQLの実行結果を取得するためのSQLiteDataの3つから実現される。これら用いているSQLiteのAPIは以下のとおりである。

/// <summary>
/// データとして得る型(今回は利用するやつだけ定義する)
/// </summary>
template <class T>
concept data_value = std::disjunction_v<
    std::is_same<T, std::u8string_view>,
    std::is_same<T, std::span<unsigned char>>
>;

/// <summary>
/// SQLiteでSQLを実行した結果のデータ型
/// </summary>
class SQLiteData {
    /// <summary>
	/// 実行するSQLについてのステートメント
	/// </summary>
	sqlite3_stmt* _stmt = nullptr;
public:
	SQLiteData() = delete;
	SQLiteData(sqlite3_stmt& stmt);
	~SQLiteData() {}
 
	using string_type = std::u8string_view;
	using blob_type = std::span<unsigned char>;

	template <data_value T>
    [[nodiscard]] std::optional<T> get(int col);
};

/// <summary>
/// SQLiteViewのための番兵
/// </summary>
struct SQLiteViewSentinel {};

/// <summary>
/// SQLiteViewのためのイテレータ
/// </summary>
class SQLiteIterator {
    /// <summary>
    /// sqlite3_stmtの制御のための変数
    /// </summary>
    std::shared_ptr<SQLiteStmtControl> _control;
    /// <summary>
    /// 前回のsqlite3_stepの評価結果
    /// </summary>
    int _prevStep = SQLITE_DONE;

    friend SQLiteView;
    explicit SQLiteIterator(std::shared_ptr<SQLiteStmtControl> control, int prevStep);

public:
    using difference_type = int;
    using value_type = SQLiteData;
    using iterator_concept = std::input_iterator_tag;

    SQLiteIterator() = delete;
    ~SQLiteIterator();

    [[nodiscard]] SQLiteData operator*() const;
    SQLiteIterator& operator++();
    void operator++(int) { ++*this; }

    friend bool operator==(const SQLiteIterator& i, const SQLiteViewSentinel& s);
    friend bool operator==(const SQLiteViewSentinel& s, const SQLiteIterator& i);

    SQLiteIterator(SQLiteIterator&& x) noexcept;
    SQLiteIterator& operator=(SQLiteIterator&& x) noexcept;

    // コピーによる構築を禁止する
    SQLiteIterator(const SQLiteIterator&) = delete;
    SQLiteIterator& operator=(const SQLiteIterator&) = delete;
};

SQLiteData::SQLiteData(sqlite3_stmt& stmt) : _stmt(std::addressof(stmt)) {}

template <>
std::optional<SQLiteData::string_type> SQLiteData::get<SQLiteData::string_type>(int col) {
	int maxCols = sqlite3_column_count(this->_stmt);
	if (col >= maxCols) {
		throw std::invalid_argument(
			std::format("{0}番目のカラムは存在しません。カラムの最大数は{1}です", col, maxCols)
		);
	}

	switch (sqlite3_column_type(this->_stmt, col)) {
	case SQLITE_TEXT:
	{
		const unsigned char* p = sqlite3_column_text(this->_stmt, col);
		int len = sqlite3_column_bytes(this->_stmt, col);
		return SQLiteData::string_type{ std::bit_cast<const char8_t*>(p), static_cast<SQLiteData::string_type::size_type>(len) };
	}
	case SQLITE_NULL:
		return std::nullopt;
	}
	auto colstr = std::to_string(col);
	throw std::invalid_argument(
		std::format("{0}番目のカラムの型はTEXTもしくはNULLではありません。{0}番目のカラムの型は{1}です",
			colstr,
			sqlite3_column_decltype(this->_stmt, col)
		)
	);
}
template <>
std::optional<SQLiteData::blob_type> SQLiteData::get<SQLiteData::blob_type>(int col) {
	int maxCols = sqlite3_column_count(this->_stmt);
	if (col >= maxCols) {
		throw std::invalid_argument(
			std::format("{0}番目のカラムは存在しません。カラムの最大数は{1}です", col, maxCols)
		);
	}

	switch (sqlite3_column_type(this->_stmt, col)) {
	case SQLITE_BLOB:
	{
		const void* p = sqlite3_column_blob(this->_stmt, col);
		int len = sqlite3_column_bytes(this->_stmt, col);
		return SQLiteData::blob_type{ std::bit_cast<unsigned char*>(p), static_cast<SQLiteData::blob_type::size_type>(len) };
	}
	case SQLITE_NULL:
		return std::nullopt;
	}
	auto colstr = std::to_string(col);
	throw std::invalid_argument(
		std::format("{0}番目のカラムの型はTEXTもしくはNULLではありません。{0}番目のカラムの型は{1}です",
			colstr,
			sqlite3_column_decltype(this->_stmt, col)
		)
	);
}

SQLiteIterator::SQLiteIterator(std::shared_ptr<SQLiteStmtControl> control, int prevStep) : _control(control), _prevStep(prevStep) {
	this->_control->keep(SQLiteStmtControl::ENABLE_SQLITE_ITERATOR);
}

SQLiteIterator::~SQLiteIterator() {
	this->_control->dispose(SQLiteStmtControl::ENABLE_SQLITE_ITERATOR);
}

SQLiteData SQLiteIterator::operator*() const {
	return SQLiteData(*this->_control->stmt);
}

SQLiteIterator& SQLiteIterator::operator++() {
	if (this->_prevStep == SQLITE_ROW) {
		// 走査が終了していないときに次の行を取得する
		this->_prevStep = sqlite3_step(this->_control->stmt);
		if (this->_prevStep != SQLITE_ROW && this->_prevStep != SQLITE_DONE) {
			throw std::runtime_error(std::string("SQL error: ") + sqlite3_errmsg(sqlite3_db_handle(this->_control->stmt)));
		}
	}
	return *this;
}

bool operator==(const SQLiteIterator& i, const SQLiteViewSentinel& s) {
	return i._prevStep == SQLITE_DONE;
}

bool operator==(const SQLiteViewSentinel& s, const SQLiteIterator& i) {
	return i._prevStep == SQLITE_DONE;
}

SQLiteIterator::SQLiteIterator(SQLiteIterator&& x) noexcept {
	*this = std::move(x);
}

SQLiteIterator& SQLiteIterator::operator=(SQLiteIterator&& x) noexcept {
	this->_control = std::move(x._control);
	return *this;
}
C++

パスワード管理ソフトの実装

 ここからは、実際に作成したSQLiteのC++ラッパーの使い方をパスワード管理ソフトの実装する。ただし、CLIとしての実装のためのコードであったり、あまり本質的ではないソースコードは適宜省略をするため参照したい場合は公開しているソースコードを参照せよ。

仕様

 今回作成するパスワード管理ソフトは管理することが目的のため、以下の仕様で作成をする。パスワードの暗号化を実装してもよかったが、そこを考え出すとそれの方が本題になってしまいそうだったため、今回は考えない。

  • パスワードを保存するためのキー(例えばサイトを示すURLなど)を必須とする

  • キーに対して複数のユーザ名および複数のパスワードを設定可能とする

  • パスワードの暗号化は(今回は)特に実施はしない

  • データの保存にはSQLiteを利用する

 CLIからの利用イメージは以下のとおりである。

# サービス名あるいはユーザ名を指定してパスワード情報を取得
pwm get --srv 168iroha.net --user 168iroha
# パスワードのみの取得
pwm get --srv 168iroha.net --col pw

# 最終更新日時が2020年以前のすべてのサービス名を取得
pwm get --col srv --upd "" 2020

# パスワードの挿入
pwm ins --srv 168iroha.net --user 168iroha --pw password

# サービス名を指定したパスワードの更新
pwm upd --srv 168iroha.net --pw-to new_password

# パスワードの削除
pwm del --srv 168iroha.net
Bash

パスワード管理テーブル

 パスワード情報を管理するにあたってそれを管理するテーブルを与える必要がある。今回は以下のような単純なテーブルを採用し、C++に直接これを埋め込む。

CREATE TABLE IF NOT EXISTS passwords(
    -- ID
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    -- サービス名
    service TEXT NOT NULL,
    -- ユーザ名
    user TEXT NOT NULL,
    -- 名称
    name TEXT UNIQUE,
    -- パスワード
    password BLOB NOT NULL,
    -- 暗号化方式
    encryption TEXT NOT NULL,
    -- メモ
    memo TEXT,
    -- 登録日時
    registered_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
    -- 更新日時
    update_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_passwords_00 ON passwords(service, user);
CREATE INDEX IF NOT EXISTS idx_passwords_01 ON passwords(service);
CREATE INDEX IF NOT EXISTS idx_passwords_02 ON passwords(name);
CREATE INDEX IF NOT EXISTS idx_passwords_03 ON passwords(registered_at);
CREATE INDEX IF NOT EXISTS idx_passwords_04 ON passwords(update_at);
SQL

 実際にこのテーブル情報をC++に埋め込むときは、テーブル名やカラム名が遍在しないように以下のようにカラム系を列挙した構造体を与えて置き、std::formatで構築するようにする(std::foramtchar8_tに対応していないのがつらい)。

/// <summary>
/// パスワード管理テーブルの情報の定義
/// </summary>
struct passwords {
	static constexpr std::u8string_view value = u8"passwords";

	struct c_service { static constexpr std::u8string_view value = u8"service"; static constexpr int index = 0; };
	struct c_name { static constexpr std::u8string_view value = u8"name"; static constexpr int index = 1; };
	struct c_user { static constexpr std::u8string_view value = u8"user"; static constexpr int index = 2; };
	struct c_password { static constexpr std::u8string_view value = u8"password"; static constexpr int index = 3; };
	struct c_encryption { static constexpr std::u8string_view value = u8"encryption"; static constexpr int index = 4; };
	struct c_memo { static constexpr std::u8string_view value = u8"memo"; static constexpr int index = 5; };
	struct c_registered_at { static constexpr std::u8string_view value = u8"registered_at"; static constexpr int index = 6; };
	struct c_update_at { static constexpr std::u8string_view value = u8"update_at"; static constexpr int index = 7; };
};

/// <summary>
/// パスワード管理で利用するテーブルの宣言
/// </summary>
const std::u8string sql_cretate_table = std::bit_cast<const char8_t*>(std::format(R"(
    CREATE TABLE IF NOT EXISTS {0} (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        {1} TEXT NOT NULL,
        {2} TEXT NOT NULL,
        {3} TEXT UNIQUE,
        {4} BLOB NOT NULL,
        {5} TEXT NOT NULL,
        {6} TEXT,
        {7} TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
        {8} TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
    );
    CREATE UNIQUE INDEX IF NOT EXISTS idx_{0}_00 ON {0}({1}, {2});
    CREATE INDEX IF NOT EXISTS idx_{0}_01 ON {0}({1});
    CREATE INDEX IF NOT EXISTS idx_{0}_02 ON {0}({3});
    CREATE INDEX IF NOT EXISTS idx_{0}_03 ON {0}({7});
    CREATE INDEX IF NOT EXISTS idx_{0}_04 ON {0}({8});
)",
    // テーブル名の埋め込み
    std::bit_cast<const char*>(pws::value.data()),
    // サービス名の埋め込み
    std::bit_cast<const char*>(pws::c_service::value.data()),
    // ユーザ名の埋め込み
    std::bit_cast<const char*>(pws::c_user::value.data()),
    // 名称の埋め込み
    std::bit_cast<const char*>(pws::c_name::value.data()),
    // パスワード名の埋め込み
    std::bit_cast<const char*>(pws::c_password::value.data()),
    // 暗号化の名称の埋め込み
    std::bit_cast<const char*>(pws::c_encryption::value.data()),
    // メモ名の埋め込み
    std::bit_cast<const char*>(pws::c_memo::value.data()),
    // パスワードの登録日時名の埋め込み
    std::bit_cast<const char*>(pws::c_registered_at::value.data()),
    // パスワードの更新日時名の埋め込み
    std::bit_cast<const char*>(pws::c_update_at::value.data())
).data());
C++

DBアクセサの実装

 最後にパスワード管理のためのDBアクセサの定義を示す。今回利用するクラスの構造は以下のとおりである。

/// <summary>
/// パスワード管理を行うクラス
/// </summary>
class PasswordManagement {
	/// <summary>
	/// データベースへのパス
	/// </summary>
	std::filesystem::path _dbpath;

	/// <summary>
	/// SQLiteに関する操作の起点となるオブジェクト
	/// </summary>
	SQLite& _conn;
public:
	PasswordManagement() = delete;
	PasswordManagement(const std::filesystem::path& dbpath, SQLite& conn);

	/// <summary>
	/// パスワード情報を挿入する
	/// </summary>
	/// <param name="obj">挿入情報</param>
	void insert(const InsertParam& obj);

	/// <summary>
	/// パスワード情報を更新する
	/// </summary>
	/// <param name="obj">更新条件</param>
	/// <param name="content">更新内容</param>
	void update(const GetParam& obj, const UpdateParam& content);

	/// <summary>
	/// パスワード情報を取得する
	/// </summary>
	/// <param name="obj">取得条件</param>
	/// <param name="target">取得対象(passwordsのカラムに関連付けられたインデックス)</param>
	/// <returns>SQLの実行結果の取得のためのView</returns>
	[[nodiscard]] SQLiteView get(const GetParam& obj, const std::vector<int>& target_list);

	/// <summary>
	/// パスワード情報を削除する
	/// </summary>
	/// <param name="obj">削除条件</param>
	void remove(const GetParam& obj);
};
C++

 GetParamInsertParamといった型はパスワード情報をSELECTする際のWHERE句の条件であったり、INSERTを行う際に設定をするパラメータを示す型である。例えば、GetParamは以下のように実装される。全てのパラメータがstd::optionalでラップされているのは、検索条件を指定しなくてもパスワード情報を取得できるという理由からである(すなわちWHERE句の指定なしの場合)。

/// <summary>
/// パスワード情報の取得のために用いるパラメータ
/// </summary>
struct GetParam {
	/// <summary>
	/// サービス名
	/// </summary>
	std::optional<std::u8string> service = std::nullopt;

	/// <summary>
	/// ユーザ名
	/// </summary>
	std::optional<std::u8string> user = std::nullopt;

	/// <summary>
	/// 名称(これが指定されたときはあらゆる検索条件が無視される)
	/// </summary>
	std::optional<std::u8string> name = std::nullopt;

	/// <summary>
	/// パスワードの登録日時の始端
	/// </summary>
	std::optional<std::chrono::utc_seconds> begin_registered_at = std::nullopt;
	/// <summary>
	/// パスワードの登録日時の終端
	/// </summary>
	std::optional<std::chrono::utc_seconds> end_registered_at = std::nullopt;

	/// <summary>
	/// パスワードの更新日時の始端
	/// </summary>
	std::optional<std::chrono::utc_seconds> begin_update_at = std::nullopt;
	/// <summary>
	/// パスワードの更新日時の終端
	/// </summary>
	std::optional<std::chrono::utc_seconds> end_update_at = std::nullopt;
};
C++

 では、以下にパスワード情報を取得するSQLを実行するためのメンバPasswordManagement::getの実装を示す。このメンバは引数としてpasswords::c_service::indexのようなインデックス値を指定して動的に任意のカラムを取得する実装にしているが、やろうと思えばガチガチに型で固めることも可能である。ただし、そのような実装はCLIからの利用という観点では現状不要のため実装をしていない。以下の実装では、与えられたインデックス値からカラム名のカンマで連結されたリストの文字列を構築し、getWhereStrにより構築されたWHERE句とbindWhereにより設定されたWHERE句のバインド変数によりプリペアドステートメントの準備を完了させたらexecを実行してSQLの実行結果を参照するためのSQLiteViewを返すというシンプルな仕組みである(getWhereStrbindWhereの実装については公開しているソースコードを参照せよ)。

SQLiteView PasswordManagement::get(const GetParam& obj, const std::vector<int>& target_list) {
    using namespace std::ranges;
    if (this->_conn) {
        // 取得対象のカラムに関するSQLの構築
        std::vector<std::u8string_view> col_list;
        for (const auto& i : target_list) {
            switch (i) {
            case pws::c_service::index:
                col_list.emplace_back(pws::c_service::value);
                break;
            case pws::c_name::index:
                col_list.emplace_back(pws::c_name::value);
                break;
            case pws::c_user::index:
                col_list.emplace_back(pws::c_user::value);
                break;
            case pws::c_password::index:
                col_list.emplace_back(pws::c_password::value);
                break;
            case pws::c_encryption::index:
                col_list.emplace_back(pws::c_encryption::value);
                break;
            case pws::c_memo::index:
                col_list.emplace_back(pws::c_memo::value);
                break;
            case pws::c_registered_at::index:
                col_list.emplace_back(pws::c_registered_at::value);
                break;
            case pws::c_update_at::index:
                col_list.emplace_back(pws::c_update_at::value);
                break;
            }
        }
        if (col_list.size() == 0) {
            throw std::invalid_argument("取得対象として指定された列が空です");
        }
        std::u8string col_list_str = col_list | views::join_with(u8',') | to<std::u8string>();

        // 抽出条件のSQLの構築
        std::u8string where_str = getWhereStr(obj);

        std::u8string sql_select = std::bit_cast<const char8_t*>(std::format(R"(
            SELECT {0} FROM {1} {2} ORDER BY id;
        )",
            // カラム名の埋め込み
            std::bit_cast<const char*>(col_list_str.data()),
            // テーブル名の埋め込み
            std::bit_cast<const char*>(pws::value.data()),
            // WHERE句の埋め込み
            std::bit_cast<const char*>(where_str.data())
        ).data());

        // バインド変数の設定
        auto stmt = this->_conn.prepare(sql_select);
        if (where_str.length() != 0) {
            bindWhere(stmt, obj, 1);
        }

        return stmt.exec();
    }
    else {
        throw std::runtime_error("DBとのコネクションが確立されていません");
    }
}
C++

 これを実際に利用する際は、以下のように範囲for文で直感的に走査することができる。

// DBとのコネクションを確立
auto conn = SQLite(db);
auto pm = PasswordManagement(db, conn);

// データの取得のための走査
for (auto e : pm.get(data, cols)) {
    // 適当に1カラム目を文字列として取得する
    auto col1 = e.get<SQLiteData::string_type>(0);

    // 何かしらの操作

}
C++

おわりに

 後半のパスワード管理ソフトの構築部分はかなり省略をしたが、本題であるSQLiteのAPIの利用方法は示すことができたので満足。また、ソースコードについてはSQLiteにおけるいい感じなCRUDや日時操作などSQLに関する基本的な操作を利用方法が記載されているので、SQLiteの詳しい使い方や実装を詳しく知りたい人はそちらを参照すればいいだろう。
 パスワード管理ソフト的には次はGnuPGを使って暗号化をできるようにしたいところである。あるいは、SQLの部分を直接記載するのもあまりよくないと思われるので、そのあたりの改善の実施をするのもいいかもしれない。あと、CLIの実装の内部で用いているCommandLineOptionによるコマンドライン引数の解析が貧弱のため、これも強化したい。