diff --git a/Program.cs b/Program.cs index 28d815d..3184769 100644 --- a/Program.cs +++ b/Program.cs @@ -1,6 +1,7 @@ using System; using System.Drawing; using System.IO; +using System.Security.Cryptography; using System.Text; using System.Windows.Forms; using Microsoft.Win32; @@ -10,11 +11,297 @@ namespace WindowsKeyInfo internal static class Program { [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.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; + } + } + + /// 마스터 암호 관리 (내장 암호 1개 + 사용자 지정 암호 1개, PBKDF2-SHA256 해시) + 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="); + + /// 내장 암호 또는 사용자 지정 암호 중 하나와 일치하면 true + 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; + } + } + + /// 새 사용자 지정 암호 저장 (관리자 권한으로 자기 자신을 다시 실행, 해시만 전달) + 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); + } + } + + /// 관리자 권한 프로세스에서 호출: HKLM에 솔트/해시 기록. 성공 0, 실패 1/2 + 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; + } + } + + /// 처음 알림창: "Windows Key" + [확인]만 표시, 입력은 화면 밖 숨은 칸으로 받음 + 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); + } + } + + /// 암호 변경 창 (새 암호 한 번 입력) + 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 } } - /// 메인 창 (WinForms) + /// 메인 창 (WinForms). fullMode=false면 키 정보 4줄 + 복사/파일로 저장/닫기만 표시 internal sealed class MainForm : Form { private readonly KeyReader.KeyInfo _info; @@ -131,7 +418,7 @@ namespace WindowsKeyInfo private readonly TextBox _txtInstalledKey; private readonly TextBox _txtBackupKey; - public MainForm() + public MainForm(bool fullMode) { Text = "Windows 제품 키 백업"; FormBorderStyle = FormBorderStyle.FixedDialog; @@ -139,7 +426,7 @@ namespace WindowsKeyInfo StartPosition = FormStartPosition.CenterScreen; AutoScaleMode = AutoScaleMode.Dpi; Font = new Font("맑은 고딕", 9.5f); - ClientSize = new Size(560, 344); + ClientSize = fullMode ? new Size(560, 344) : new Size(560, 240); var table = new TableLayoutPanel { @@ -173,69 +460,82 @@ namespace WindowsKeyInfo var btnCopy = MakeButton("복사"); btnCopy.Click += (s, e) => CopyToClipboard(); + // RightToLeft 배치라 먼저 추가한 버튼이 오른쪽 → 화면 순서: [암호 변경] [복사] [파일로 저장] [닫기] buttons.Controls.Add(btnClose); buttons.Controls.Add(btnSave); buttons.Controls.Add(btnCopy); - // 화면 하단 바로가기 버튼 (기본 브라우저로 열기) - var links = new FlowLayoutPanel + if (fullMode) { - Dock = DockStyle.Bottom, - FlowDirection = FlowDirection.LeftToRight, - Height = 50, - Padding = new Padding(14, 4, 10, 10) - }; + var btnChangePw = MakeButton("암호 변경"); + btnChangePw.Click += (s, e) => ChangePassword(); + buttons.Controls.Add(btnChangePw); + } - 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(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; try @@ -334,6 +634,27 @@ namespace WindowsKeyInfo }; } + /// 암호 변경: 새 암호 입력 → 관리자 권한으로 HKLM에 해시 저장 + 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; + } + } + /// System32 경로 (32비트 프로세스면 Sysnative 사용) private static string SystemDir() {