レジストリのトランザクション機能を使ってユーザー名とパスワードを同時に書き込む (Windows)
あまり知られていないと思いますが、レジストリはトランザクション処理が可能です。たとえば、レジストリに対してユーザー名とパスワードの書き込みを指示した後、コミットして両方を同時に確定したり、ロールバックして取り消したりできます。実際に試してみましょう。
動作確認環境
- Windows 11 Home 23H2
- Visual Studio Community 2022 (Visual C++)
トランザクション処理の書き方
トランザクション処理を行うには、まず、CreateTransaction 関数を呼び出してトランザクションオブジェクトを作成します。例を示します。
#include <windows.h>
#include <ktmw32.h>
#pragma comment(lib, "ktmw32.lib")
......
// トランザクションオブジェクトの作成
HANDLE hTransaction;
hTransaction = CreateTransaction(
NULL, // セキュリティ属性
0, // 予約
TRANSACTION_DO_NOT_PROMOTE, // オプション
0, // 予約
0, // 予約
INFINITE, // タイムアウト (ミリ秒)
L"MyTransaction"); // メモ
処理に成功すると、トランザクションハンドルがリターンします。失敗すると、INVALID_HANDLE_VALUE がリターンします。
続いて、RegOpenKeyTransacted 関数を呼び出して既存のレジストリキーをオープンするか、RegCreateKeyTransacted 関数を呼び出してレジストキーを新規作成(すでに存在する場合はオープン)します。これらの ~Transacted 関数は、引数にトランザクションハンドルを取ります。
RegCreateKeyTransacted 関数の呼び出し例を示します。
#include <windows.h>
#pragma comment(lib, "advapi32.lib")
......
LSTATUS lstatus;
HKEY hkeyYaya;
lstatus = RegCreateKeyTransacted(
HKEY_CURRENT_USER, // キー(入力)
"Software\\yaya", // サブキー(入力)
0, // 予約
NULL, // クラス
REG_OPTION_NON_VOLATILE, // オプション
KEY_ALL_ACCESS, // アクセス権
NULL, // セキュリティ属性
&hkeyYaya, // キー(出力)
NULL, // 新規作成・オープン識別
hTransaction, // トランザクションハンドル
NULL); // 予約
最初の 2 つの引数で、作成(またはオープン)するキーの名前「HKEY_CURRENT_USER\Software\yaya」を指定しています。後ろから 2 つ目の引数で、さきほど CreateTransaction 関数で作ったトランザクションハンドルを指定しています。
本関数によりキーの作成(またはオープン)に成功すると、引数 hkeyYaya に「トランザクション処理に対応したレジストリキーへのハンドル」がセットされ、ERROR_SUCCESS (= 0) がリターンします。失敗すると、ERROR_SUCCESS 以外の値、たとえば ERROR_INVALID_TRANSACTION (= 6700) がリターンします。
こうして得られた「トランザクション処理に対応したレジストリキーへのハンドル」を、従来からあるレジストリ操作関数(名前に ~Transacted が付かない関数)に指定して、各種レジストリ操作を行います。レジストリ操作関数の例を示します。
- キーの作成 : CreateKeyEx()
- キーの削除 : RegDeleteKeyEx()
- 値の作成 : RegSetValueEx()
- 値の削除 : RegDeleteValueEx()
- キーと値の作成 : RegSetKeyValue()
- 値の読み込み : RegQueryValueEx()
ここでは値の作成を行う RegSetValueEx 関数を使って、「yaya」キー配下にユーザー名とパスワードを書き込んでみましょう。
// ユーザー名の書き込み
char pcUsername[] = "MyUsername";
lstatus = RegSetValueEx(
hkeyYaya, // キー
"username", // 名前
0, // 予約
REG_SZ, // 型
pcUsername, // データ
sizeof(pcUsername)); // データのサイズ
// パスワードの書き込み
char pcPassword[] = "MyPassword";
lstatus = RegSetValueEx(
hkeyYaya, // キー
"password", // 名前
0, // 予約
REG_SZ, // 型
pcPassword, // データ
sizeof(pcPassword)); // データのサイズ
値の作成に成功すると ERROR_SUCCESS がリターンします。失敗すると、それ以外の値がリターンします。
レジストリの操作が終わったら、RegCloseKey 関数でレジストリキーへのハンドルをクローズします。
lstatus = RegCloseKey(hkeyYaya);
この関数も、処理に成功すると ERROR_SUCCESS が、失敗するとそれ以外の値がリターンします。
さて、ここまでの処理にすべて成功した場合、「yaya」キーの作成と「username」「password」値の書き込みを行ったことから、レジストリの内容は次のようになっているはずです。
ところがレジストリエディタを開いてみても、何もありません。
まだトランザクション処理が仕掛中であり、コミットを行っていないためです。
トランザクション処理をコミットするには、CommitTransaction 関数を呼び出します。
BOOL bRet;
bRet = CommitTransaction(hTransaction);
コミットに成功すると、一連のレジストリ操作が確定し、0 以外の値がリターンします。競合発生などで失敗すると、0 がリターンします。
コミット成功後にレジストリエディタを開いてみると、期待通り、「yaya」キーと「username」「password」値が一斉に書き込まれたことが確認できます。
何らかの理由でコミットではなくロールバックしたい場合は、RollbackTransaction 関数を呼び出します。
bRet = RollbackTransaction(hTransaction);
ロールバックに成功すると、0 以外の値がリターンします。引数不正などで失敗すると、0 がリターンします。ロールバック時は、当然、「yaya」キーも「username」「password」値も書き込まれません。
コミットした場合もロールバックした場合も、最後に CloseHandle 関数を呼んでトランザクションハンドルをクローズしておきましょう。
bRet = CloseHandle(hTransaction);
ハンドルのクローズに成功すると、0 以外の値がリターンします。引数不正などで失敗すると、0 がリターンします。
異常系 1. トランザクション処理を重ねて実行
このトランザクション処理を 2 つのプロセスから実行した場合の例を示します。
プロセス A でユーザー名とパスワードの書き込み(図中の「RegSetValueEx() x 2」)を指示した後、プロセス B でも同じ書き込みを指示すると、プロセス B で競合が検出され処理に失敗します。このときの GetLastError() 値は ERROR_TRANSACTIONAL_CONFLICT (= 6800) です。
一方のプロセス A は、そのままコミットに進むことができます。
異常系 2. トランザクション処理中にレジストリエディタでレジストリを操作
トランザクション処理の途中にレジストリエディタで値を書き換えた場合の例を示します。
プロセス A でユーザー名とパスワードの書き込み(図中の「RegSetValueEx() x 2」)を指示した後、レジストリエディタで同じキー配下に書き込みを行うと、プロセス A による仕掛中の書き込みは無視され、レジストリエディタによる書き込みが確定します。
その後、プロセス A でコミットしようとすると、このトランザクションは中断済みということで失敗します。このときの GetLastError() 値は ERROR_TRANSACTION_ALREADY_ABORTED (= 6704) です。
異常系 3. コミットもロールバックもせずにプロセスが終了
コミットもロールバックもせずにプロセスが終了した場合の例を示します。
ユーザー名とパスワードの書き込み指示した後、PC の電源が落ちるなどしてコミットすることなくプロセスが終了した場合、書き込みは確定しません。ロールバック時と同様の動作になります。
異常系 4. タイムアウト時間が経過
トランザクションオブジェクトを作成する CreateTransaction 関数には、タイムアウトを指定する引数があります。この記事の最初の例では無限待ちを意味する INFINITE (= -1) を指定しましたが、3 秒を意味する 3000 を指定した場合の例を示します。
// トランザクションオブジェクトの作成(タイムアウト 3 秒)
hTransaction = CreateTransaction(
NULL, // セキュリティ属性
0, // 予約
TRANSACTION_DO_NOT_PROMOTE, // オプション
0, // 予約
0, // 予約
3000, // タイムアウト (ミリ秒)
L"MyTransaction"); // メモ
トランザクションオブジェクト作成から 3 秒以上経過したタイミングでユーザー名とパスワードの書き込みを指示すると、タイムアウト発生ということで処理が失敗します。このときの GetLastError() 値は ERROR_TRANSACTION_NOT_ACTIVE (= 6071) です。
おわりに
レジストリのトランザクション処理を試してみました。中途半端にレジストリが書き換わることを防止できるため、信頼性の高いソフトウェアの開発に役立ちます。