마스터 암호 기능 추가 (잠금 화면, 제한 모드, 암호 변경)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019WT2pzDE45iP4QMAAo7cyzmain
parent
29f799ca08
commit
9962c4e7dd
441
Program.cs
441
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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
|
||||
{
|
||||
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
|
|||
};
|
||||
}
|
||||
|
||||
/// <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>
|
||||
private static string SystemDir()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue