마스터 암호 기능 추가 (잠금 화면, 제한 모드, 암호 변경)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019WT2pzDE45iP4QMAAo7cyz
main
Claude 2026-10-21 06:34:34 +00:00
parent 29f799ca08
commit 9962c4e7dd
No known key found for this signature in database
1 changed files with 381 additions and 60 deletions

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Drawing; using System.Drawing;
using System.IO; using System.IO;
using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Windows.Forms; using System.Windows.Forms;
using Microsoft.Win32; using Microsoft.Win32;
@ -10,11 +11,297 @@ namespace WindowsKeyInfo
internal static class Program internal static class Program
{ {
[STAThread] [STAThread]
private static void Main() private static int Main(string[] args)
{ {
// 관리자 권한으로 다시 실행된 경우: 사용자 지정 암호(해시)만 레지스트리에 저장하고 종료
if (args.Length == 3 && args[0] == PasswordStore.SetPasswordArg)
{
return PasswordStore.SaveFromElevatedArgs(args[1], args[2]);
}
Application.EnableVisualStyles(); Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false); Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MainForm());
// 처음 알림창: 입력칸 없이 "Windows Key" + [확인]만 보이고, 키보드 입력은 숨은 칸으로 받음
bool fullMode;
using (var unlock = new UnlockDialog())
{
if (unlock.ShowDialog() != DialogResult.OK) return 0; // X / Esc → 종료
fullMode = PasswordStore.Verify(unlock.Password);
}
Application.Run(new MainForm(fullMode));
return 0;
}
}
/// <summary>마스터 암호 관리 (내장 암호 1개 + 사용자 지정 암호 1개, PBKDF2-SHA256 해시)</summary>
internal static class PasswordStore
{
public const string SetPasswordArg = "--set-password";
private const string RegistryPath = @"SOFTWARE\WindowsKeyInfo";
private const string SaltValue = "PasswordSalt";
private const string HashValue = "PasswordHash";
private const string IterationsValue = "PasswordIterations";
private const int Iterations = 100000;
private const int SaltSize = 16;
private const int HashSize = 32;
// 내장 마스터 암호 (변경 불가) — 원문 대신 솔트와 해시만 보관
private static readonly byte[] BuiltInSalt = Convert.FromBase64String("R2lR1GEm7i5aAldpu9RynQ==");
private static readonly byte[] BuiltInHash = Convert.FromBase64String("r5zE7lvhdNTDklWIrkDd47Vhqq8lVBt9So8aYMLGFnY=");
/// <summary>내장 암호 또는 사용자 지정 암호 중 하나와 일치하면 true</summary>
public static bool Verify(string password)
{
if (string.IsNullOrEmpty(password)) return false;
if (Matches(password, BuiltInSalt, BuiltInHash, Iterations)) return true;
try
{
using (RegistryKey key = OpenBase().OpenSubKey(RegistryPath))
{
if (key == null) return false;
var salt = key.GetValue(SaltValue) as byte[];
var hash = key.GetValue(HashValue) as byte[];
object iterObj = key.GetValue(IterationsValue);
int iter = iterObj is int ? (int)iterObj : Iterations;
if (salt == null || hash == null || iter <= 0) return false;
return Matches(password, salt, hash, iter);
}
}
catch
{
return false;
}
}
/// <summary>새 사용자 지정 암호 저장 (관리자 권한으로 자기 자신을 다시 실행, 해시만 전달)</summary>
public static void ChangeUserPassword(IWin32Window owner, string newPassword, string caption)
{
byte[] salt = new byte[SaltSize];
using (var rng = RandomNumberGenerator.Create()) rng.GetBytes(salt);
byte[] hash = Derive(newPassword, salt, Iterations);
string exe = System.Reflection.Assembly.GetExecutingAssembly().Location;
var psi = new System.Diagnostics.ProcessStartInfo(exe,
SetPasswordArg + " " + Convert.ToBase64String(salt) + " " + Convert.ToBase64String(hash))
{
UseShellExecute = true,
Verb = "runas" // 관리자 권한 요청 (UAC 창)
};
try
{
using (var p = System.Diagnostics.Process.Start(psi))
{
p.WaitForExit();
if (p.ExitCode == 0)
{
MessageBox.Show(owner, "암호를 변경했습니다.", caption,
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
else
{
MessageBox.Show(owner, "암호를 저장하지 못했습니다. (오류 코드: " + p.ExitCode + ")", caption,
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
catch (System.ComponentModel.Win32Exception ex) when (ex.NativeErrorCode == 1223)
{
MessageBox.Show(owner, "관리자 권한 요청이 취소되었습니다.\n암호 변경에는 관리자 권한이 필요합니다.", caption,
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
MessageBox.Show(owner, "암호 변경을 실행하지 못했습니다.\n\n" + ex.Message, caption,
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>관리자 권한 프로세스에서 호출: HKLM에 솔트/해시 기록. 성공 0, 실패 1/2</summary>
public static int SaveFromElevatedArgs(string saltB64, string hashB64)
{
byte[] salt, hash;
try
{
salt = Convert.FromBase64String(saltB64);
hash = Convert.FromBase64String(hashB64);
}
catch
{
return 2;
}
if (salt.Length != SaltSize || hash.Length != HashSize) return 2;
try
{
using (RegistryKey key = OpenBase().CreateSubKey(RegistryPath))
{
key.SetValue(SaltValue, salt, RegistryValueKind.Binary);
key.SetValue(HashValue, hash, RegistryValueKind.Binary);
key.SetValue(IterationsValue, Iterations, RegistryValueKind.DWord);
}
return 0;
}
catch
{
return 1;
}
}
private static RegistryKey OpenBase()
{
RegistryView view = Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Default;
return RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, view);
}
private static byte[] Derive(string password, byte[] salt, int iterations)
{
using (var kdf = new Rfc2898DeriveBytes(Encoding.UTF8.GetBytes(password), salt, iterations,
HashAlgorithmName.SHA256))
{
return kdf.GetBytes(HashSize);
}
}
private static bool Matches(string password, byte[] salt, byte[] expected, int iterations)
{
byte[] actual = Derive(password, salt, iterations);
if (actual.Length != expected.Length) return false;
int diff = 0;
for (int i = 0; i < actual.Length; i++) diff |= actual[i] ^ expected[i]; // 고정 시간 비교
return diff == 0;
}
}
/// <summary>처음 알림창: "Windows Key" + [확인]만 표시, 입력은 화면 밖 숨은 칸으로 받음</summary>
internal sealed class UnlockDialog : Form
{
private readonly TextBox _hidden;
public string Password { get { return _hidden.Text; } }
public UnlockDialog()
{
Text = "Windows Key";
FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = false;
MinimizeBox = false;
StartPosition = FormStartPosition.CenterScreen;
AutoScaleMode = AutoScaleMode.Dpi;
Font = new Font("맑은 고딕", 9.5f);
ClientSize = new Size(300, 120);
var lbl = new Label
{
Text = "Windows Key",
TextAlign = ContentAlignment.MiddleCenter,
Location = new Point(0, 18),
Size = new Size(300, 36),
Font = new Font("맑은 고딕", 12f, FontStyle.Bold)
};
var btnOk = new Button
{
Text = "확인",
Size = new Size(96, 32),
Location = new Point(102, 72),
DialogResult = DialogResult.OK,
TabStop = false
};
// 화면에 보이지 않는 입력칸 (창 밖 좌표에 배치)
_hidden = new TextBox
{
Location = new Point(-2000, -2000),
Size = new Size(10, 10),
UseSystemPasswordChar = true,
ShortcutsEnabled = false
};
Controls.Add(lbl);
Controls.Add(btnOk);
Controls.Add(_hidden);
AcceptButton = btnOk; // 엔터 → 확인
ActiveControl = _hidden;
Shown += (s, e) => _hidden.Focus();
// 확인 버튼을 마우스로 누른 뒤에도 입력이 다시 숨은 칸으로 가도록
Activated += (s, e) => _hidden.Focus();
}
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
if (keyData == Keys.Escape)
{
DialogResult = DialogResult.Cancel; // Esc → 종료
Close();
return true;
}
return base.ProcessCmdKey(ref msg, keyData);
}
}
/// <summary>암호 변경 창 (새 암호 한 번 입력)</summary>
internal sealed class ChangePasswordDialog : Form
{
private readonly TextBox _box;
public string NewPassword { get; private set; }
public ChangePasswordDialog()
{
Text = "암호 변경";
FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = false;
MinimizeBox = false;
ShowInTaskbar = false;
StartPosition = FormStartPosition.CenterParent;
AutoScaleMode = AutoScaleMode.Dpi;
Font = new Font("맑은 고딕", 9.5f);
ClientSize = new Size(400, 150);
var lbl = new Label
{
Text = "새 사용자 지정 암호를 입력하세요.\n[저장]을 누르면 관리자 권한 확인 창이 뜹니다. (내장 암호는 변경되지 않습니다)",
Location = new Point(14, 12),
Size = new Size(372, 42)
};
_box = new TextBox
{
Location = new Point(14, 62),
Size = new Size(372, 28),
UseSystemPasswordChar = true
};
var btnOk = new Button { Text = "저장", Location = new Point(166, 104), Size = new Size(108, 32) };
btnOk.Click += (s, e) => TryAccept();
var btnCancel = new Button { Text = "취소", Location = new Point(280, 104), Size = new Size(106, 32), DialogResult = DialogResult.Cancel };
Controls.Add(lbl);
Controls.Add(_box);
Controls.Add(btnOk);
Controls.Add(btnCancel);
AcceptButton = btnOk;
CancelButton = btnCancel;
}
private void TryAccept()
{
if (string.IsNullOrEmpty(_box.Text))
{
MessageBox.Show(this, "새 암호를 입력하세요.", Text, MessageBoxButtons.OK, MessageBoxIcon.Warning);
_box.Focus();
return;
}
NewPassword = _box.Text;
DialogResult = DialogResult.OK;
Close();
} }
} }
@ -122,7 +409,7 @@ namespace WindowsKeyInfo
} }
} }
/// <summary>메인 창 (WinForms)</summary> /// <summary>메인 창 (WinForms). fullMode=false면 키 정보 4줄 + 복사/파일로 저장/닫기만 표시</summary>
internal sealed class MainForm : Form internal sealed class MainForm : Form
{ {
private readonly KeyReader.KeyInfo _info; private readonly KeyReader.KeyInfo _info;
@ -131,7 +418,7 @@ namespace WindowsKeyInfo
private readonly TextBox _txtInstalledKey; private readonly TextBox _txtInstalledKey;
private readonly TextBox _txtBackupKey; private readonly TextBox _txtBackupKey;
public MainForm() public MainForm(bool fullMode)
{ {
Text = "Windows 제품 키 백업"; Text = "Windows 제품 키 백업";
FormBorderStyle = FormBorderStyle.FixedDialog; FormBorderStyle = FormBorderStyle.FixedDialog;
@ -139,7 +426,7 @@ namespace WindowsKeyInfo
StartPosition = FormStartPosition.CenterScreen; StartPosition = FormStartPosition.CenterScreen;
AutoScaleMode = AutoScaleMode.Dpi; AutoScaleMode = AutoScaleMode.Dpi;
Font = new Font("맑은 고딕", 9.5f); Font = new Font("맑은 고딕", 9.5f);
ClientSize = new Size(560, 344); ClientSize = fullMode ? new Size(560, 344) : new Size(560, 240);
var table = new TableLayoutPanel var table = new TableLayoutPanel
{ {
@ -173,69 +460,82 @@ namespace WindowsKeyInfo
var btnCopy = MakeButton("복사"); var btnCopy = MakeButton("복사");
btnCopy.Click += (s, e) => CopyToClipboard(); btnCopy.Click += (s, e) => CopyToClipboard();
// RightToLeft 배치라 먼저 추가한 버튼이 오른쪽 → 화면 순서: [암호 변경] [복사] [파일로 저장] [닫기]
buttons.Controls.Add(btnClose); buttons.Controls.Add(btnClose);
buttons.Controls.Add(btnSave); buttons.Controls.Add(btnSave);
buttons.Controls.Add(btnCopy); buttons.Controls.Add(btnCopy);
// 화면 하단 바로가기 버튼 (기본 브라우저로 열기) if (fullMode)
var links = new FlowLayoutPanel
{ {
Dock = DockStyle.Bottom, var btnChangePw = MakeButton("암호 변경");
FlowDirection = FlowDirection.LeftToRight, btnChangePw.Click += (s, e) => ChangePassword();
Height = 50, buttons.Controls.Add(btnChangePw);
Padding = new Padding(14, 4, 10, 10) }
};
var lblLinks = new Label
{
Text = "바로가기:",
AutoSize = true,
Margin = new Padding(0, 9, 6, 0)
};
var btnGetCidMe = MakeLinkButton("GETCID.ME", "https://getcid.me/");
var btnGetCidInfo = MakeLinkButton("GETCID.INFO", "https://getcid.info/");
var btnGetCidUs = MakeLinkButton("GETCID.US", "https://getcid.us/");
links.Controls.Add(lblLinks);
links.Controls.Add(btnGetCidMe);
links.Controls.Add(btnGetCidInfo);
links.Controls.Add(btnGetCidUs);
// 전화 인증 (slmgr 명령어 방식 — slui 4 창이 막혀 있어도 동작)
var phone = new FlowLayoutPanel
{
Dock = DockStyle.Bottom,
FlowDirection = FlowDirection.LeftToRight,
Height = 44,
Padding = new Padding(14, 4, 10, 0)
};
var lblPhone = new Label
{
Text = "전화 인증:",
AutoSize = true,
Margin = new Padding(0, 9, 6, 0)
};
var btnShowIid = MakeActionButton("① 설치 ID 보기");
btnShowIid.Click += (s, e) => ShowInstallationId();
var btnEnterCid = MakeActionButton("② 확인 ID 입력");
btnEnterCid.Click += (s, e) => EnterConfirmationId();
phone.Controls.Add(lblPhone);
phone.Controls.Add(btnShowIid);
phone.Controls.Add(btnEnterCid);
var tip = new ToolTip();
tip.SetToolTip(btnShowIid, "설치 ID(Installation ID)를 확인합니다 (slmgr /dti)");
tip.SetToolTip(btnEnterCid, "받은 확인 ID(Confirmation ID)를 입력해 인증합니다 (slmgr /atp, 관리자 권한 필요)");
tip.SetToolTip(btnGetCidMe, "https://getcid.me/");
tip.SetToolTip(btnGetCidInfo, "https://getcid.info/");
tip.SetToolTip(btnGetCidUs, "https://getcid.us/");
// Dock=Bottom 은 나중에 추가한 것이 더 아래에 배치됨
Controls.Add(table); Controls.Add(table);
Controls.Add(buttons); Controls.Add(buttons);
Controls.Add(phone);
Controls.Add(links); if (fullMode)
{
// 화면 하단 바로가기 버튼 (기본 브라우저로 열기)
var links = new FlowLayoutPanel
{
Dock = DockStyle.Bottom,
FlowDirection = FlowDirection.LeftToRight,
Height = 50,
Padding = new Padding(14, 4, 10, 10)
};
var lblLinks = new Label
{
Text = "바로가기:",
AutoSize = true,
Margin = new Padding(0, 9, 6, 0)
};
var btnGetCidMe = MakeLinkButton("GETCID.ME", "https://getcid.me/");
var btnGetCidInfo = MakeLinkButton("GETCID.INFO", "https://getcid.info/");
var btnGetCidUs = MakeLinkButton("GETCID.US", "https://getcid.us/");
links.Controls.Add(lblLinks);
links.Controls.Add(btnGetCidMe);
links.Controls.Add(btnGetCidInfo);
links.Controls.Add(btnGetCidUs);
// 전화 인증 (slmgr 명령어 방식 — slui 4 창이 막혀 있어도 동작)
var phone = new FlowLayoutPanel
{
Dock = DockStyle.Bottom,
FlowDirection = FlowDirection.LeftToRight,
Height = 44,
Padding = new Padding(14, 4, 10, 0)
};
var lblPhone = new Label
{
Text = "전화 인증:",
AutoSize = true,
Margin = new Padding(0, 9, 6, 0)
};
var btnShowIid = MakeActionButton("① 설치 ID 보기");
btnShowIid.Click += (s, e) => ShowInstallationId();
var btnEnterCid = MakeActionButton("② 확인 ID 입력");
btnEnterCid.Click += (s, e) => EnterConfirmationId();
phone.Controls.Add(lblPhone);
phone.Controls.Add(btnShowIid);
phone.Controls.Add(btnEnterCid);
var tip = new ToolTip();
tip.SetToolTip(btnShowIid, "설치 ID(Installation ID)를 확인합니다 (slmgr /dti)");
tip.SetToolTip(btnEnterCid, "받은 확인 ID(Confirmation ID)를 입력해 인증합니다 (slmgr /atp, 관리자 권한 필요)");
tip.SetToolTip(btnGetCidMe, "https://getcid.me/");
tip.SetToolTip(btnGetCidInfo, "https://getcid.info/");
tip.SetToolTip(btnGetCidUs, "https://getcid.us/");
// Dock=Bottom 은 나중에 추가한 것이 더 아래에 배치됨
Controls.Add(phone);
Controls.Add(links);
}
CancelButton = btnClose; CancelButton = btnClose;
try try
@ -334,6 +634,27 @@ namespace WindowsKeyInfo
}; };
} }
/// <summary>암호 변경: 새 암호 입력 → 관리자 권한으로 HKLM에 해시 저장</summary>
private void ChangePassword()
{
string newPassword;
using (var dlg = new ChangePasswordDialog())
{
if (dlg.ShowDialog(this) != DialogResult.OK) return;
newPassword = dlg.NewPassword;
}
Cursor = Cursors.WaitCursor;
try
{
PasswordStore.ChangeUserPassword(this, newPassword, Text);
}
finally
{
Cursor = Cursors.Default;
}
}
/// <summary>System32 경로 (32비트 프로세스면 Sysnative 사용)</summary> /// <summary>System32 경로 (32비트 프로세스면 Sysnative 사용)</summary>
private static string SystemDir() private static string SystemDir()
{ {