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()
{