마스터 암호 기능 추가 (잠금 화면, 제한 모드, 암호 변경)
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;
|
||||||
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()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue