1042 lines
40 KiB
C#
1042 lines
40 KiB
C#
using System;
|
|
using System.Drawing;
|
|
using System.IO;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Windows.Forms;
|
|
using Microsoft.Win32;
|
|
|
|
namespace WindowsKeyInfo
|
|
{
|
|
internal static class Program
|
|
{
|
|
[STAThread]
|
|
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);
|
|
|
|
// 처음 알림창: 입력칸 없이 "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>제한 화면에서 Ctrl+Shift+P로 여는 마스터 암호 입력 창 (틀리면 알림 후 다시 입력)</summary>
|
|
internal sealed class MasterPasswordDialog : Form
|
|
{
|
|
private readonly TextBox _box;
|
|
|
|
public MasterPasswordDialog()
|
|
{
|
|
Text = "Windows Key";
|
|
FormBorderStyle = FormBorderStyle.FixedDialog;
|
|
MaximizeBox = false;
|
|
MinimizeBox = false;
|
|
ShowInTaskbar = false;
|
|
StartPosition = FormStartPosition.CenterParent;
|
|
AutoScaleMode = AutoScaleMode.Dpi;
|
|
Font = new Font("맑은 고딕", 9.5f);
|
|
ClientSize = new Size(360, 130);
|
|
|
|
var lbl = new Label
|
|
{
|
|
Text = "마스터 암호를 입력하세요.",
|
|
Location = new Point(14, 14),
|
|
Size = new Size(332, 22)
|
|
};
|
|
_box = new TextBox
|
|
{
|
|
Location = new Point(14, 42),
|
|
Size = new Size(332, 28),
|
|
UseSystemPasswordChar = true
|
|
};
|
|
|
|
var btnOk = new Button { Text = "확인", Location = new Point(126, 84), Size = new Size(108, 32) };
|
|
btnOk.Click += (s, e) => TryAccept();
|
|
var btnCancel = new Button { Text = "취소", Location = new Point(240, 84), Size = new Size(106, 32), DialogResult = DialogResult.Cancel };
|
|
|
|
Controls.Add(lbl);
|
|
Controls.Add(_box);
|
|
Controls.Add(btnOk);
|
|
Controls.Add(btnCancel);
|
|
AcceptButton = btnOk;
|
|
CancelButton = btnCancel;
|
|
ActiveControl = _box;
|
|
}
|
|
|
|
private void TryAccept()
|
|
{
|
|
if (PasswordStore.Verify(_box.Text))
|
|
{
|
|
DialogResult = DialogResult.OK;
|
|
Close();
|
|
return;
|
|
}
|
|
|
|
MessageBox.Show(this, "암호가 틀렸습니다.", Text, MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
|
_box.Clear();
|
|
_box.Focus();
|
|
}
|
|
}
|
|
|
|
/// <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();
|
|
}
|
|
}
|
|
|
|
/// <summary>레지스트리에서 윈도우 키 정보를 읽어오는 클래스 (원본 VBScript 로직 이식)</summary>
|
|
internal static class KeyReader
|
|
{
|
|
private const string CurrentVersionPath = @"SOFTWARE\Microsoft\Windows NT\CurrentVersion";
|
|
private const string ComputerNamePath = @"SYSTEM\CurrentControlSet\Control\ComputerName\ComputerName";
|
|
private const string NotFound = "없음";
|
|
|
|
public sealed class KeyInfo
|
|
{
|
|
public string ComputerName;
|
|
public string ProductName;
|
|
public string ProductId;
|
|
public string InstalledKey;
|
|
public string BackupKey;
|
|
}
|
|
|
|
public static KeyInfo Read()
|
|
{
|
|
var info = new KeyInfo();
|
|
|
|
// 64비트 윈도우에서는 반드시 64비트 레지스트리 뷰로 읽어야 DigitalProductId가 정상적으로 보임
|
|
RegistryView view = Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Default;
|
|
|
|
using (RegistryKey hklm = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, view))
|
|
{
|
|
using (RegistryKey cv = hklm.OpenSubKey(CurrentVersionPath))
|
|
{
|
|
info.ProductName = GetString(cv, "ProductName");
|
|
info.ProductId = GetString(cv, "ProductId");
|
|
|
|
byte[] digitalId = cv == null ? null : cv.GetValue("DigitalProductId") as byte[];
|
|
info.InstalledKey = ConvertToKey(digitalId);
|
|
}
|
|
|
|
using (RegistryKey spp = hklm.OpenSubKey(CurrentVersionPath + @"\SoftwareProtectionPlatform"))
|
|
{
|
|
info.BackupKey = GetString(spp, "BackupProductKeyDefault");
|
|
}
|
|
|
|
using (RegistryKey cn = hklm.OpenSubKey(ComputerNamePath))
|
|
{
|
|
string name = GetString(cn, "ComputerName");
|
|
info.ComputerName = name == NotFound ? Environment.MachineName : name;
|
|
}
|
|
}
|
|
|
|
return info;
|
|
}
|
|
|
|
private static string GetString(RegistryKey key, string valueName)
|
|
{
|
|
if (key == null) return NotFound;
|
|
object value = key.GetValue(valueName);
|
|
string text = value == null ? null : value.ToString();
|
|
return string.IsNullOrWhiteSpace(text) ? NotFound : text;
|
|
}
|
|
|
|
/// <summary>DigitalProductId(바이너리) → 25자리 제품 키 변환</summary>
|
|
public static string ConvertToKey(byte[] digitalId)
|
|
{
|
|
const int keyOffset = 52;
|
|
const string maps = "BCDFGHJKMPQRTVWXY2346789";
|
|
|
|
if (digitalId == null || digitalId.Length < keyOffset + 15) return NotFound;
|
|
|
|
byte[] key = (byte[])digitalId.Clone(); // 원본 배열 보존
|
|
|
|
// Windows 8 이상 형식인지 확인
|
|
int isWin8 = (key[66] / 6) & 1;
|
|
key[66] = (byte)((key[66] & 0xF7) | ((isWin8 & 2) * 4));
|
|
|
|
string keyOutput = string.Empty;
|
|
int last = 0;
|
|
|
|
for (int i = 24; i >= 0; i--)
|
|
{
|
|
int current = 0;
|
|
for (int j = 14; j >= 0; j--)
|
|
{
|
|
current = current * 256;
|
|
current = key[j + keyOffset] + current;
|
|
key[j + keyOffset] = (byte)(current / 24);
|
|
current = current % 24;
|
|
}
|
|
keyOutput = maps[current] + keyOutput;
|
|
last = current;
|
|
}
|
|
|
|
// Windows 8 이상 키는 'N' 문자 위치를 보정
|
|
if (isWin8 == 1)
|
|
{
|
|
string keypart1 = keyOutput.Substring(1, last);
|
|
string keypart2 = keyOutput.Substring(last + 1);
|
|
keyOutput = keypart1 + "N" + keypart2;
|
|
}
|
|
|
|
return keyOutput.Substring(0, 5) + "-" +
|
|
keyOutput.Substring(5, 5) + "-" +
|
|
keyOutput.Substring(10, 5) + "-" +
|
|
keyOutput.Substring(15, 5) + "-" +
|
|
keyOutput.Substring(20, 5);
|
|
}
|
|
}
|
|
|
|
/// <summary>메인 창 (WinForms). 제한 모드면 키 정보 4줄 + 복사/파일로 저장/닫기만 표시,
|
|
/// 제한 모드에서 Ctrl+Shift+P → 마스터 암호 입력 → 맞으면 그 자리에서 전체 화면으로 전환</summary>
|
|
internal sealed class MainForm : Form
|
|
{
|
|
private const Keys UnlockShortcut = Keys.Control | Keys.Shift | Keys.P;
|
|
|
|
private readonly KeyReader.KeyInfo _info;
|
|
private readonly TextBox _txtProductName;
|
|
private readonly TextBox _txtProductId;
|
|
private readonly TextBox _txtInstalledKey;
|
|
private readonly TextBox _txtBackupKey;
|
|
|
|
// 전체 모드에서만 보이는 컨트롤
|
|
private readonly Button _btnChangePw;
|
|
private readonly FlowLayoutPanel _phone;
|
|
private readonly FlowLayoutPanel _links;
|
|
private bool _fullMode;
|
|
|
|
public MainForm(bool fullMode)
|
|
{
|
|
Text = "Windows 제품 키 백업";
|
|
FormBorderStyle = FormBorderStyle.FixedDialog;
|
|
MaximizeBox = false;
|
|
StartPosition = FormStartPosition.CenterScreen;
|
|
AutoScaleMode = AutoScaleMode.Dpi;
|
|
Font = new Font("맑은 고딕", 9.5f);
|
|
ClientSize = new Size(560, 344);
|
|
|
|
var table = new TableLayoutPanel
|
|
{
|
|
Dock = DockStyle.Top,
|
|
Height = 180,
|
|
Padding = new Padding(14, 14, 14, 0),
|
|
ColumnCount = 2,
|
|
RowCount = 4
|
|
};
|
|
table.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 130));
|
|
table.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
|
|
for (int r = 0; r < 4; r++) table.RowStyles.Add(new RowStyle(SizeType.Percent, 25));
|
|
|
|
_txtProductName = AddRow(table, 0, "제품 이름", false);
|
|
_txtProductId = AddRow(table, 1, "제품 ID", false);
|
|
_txtInstalledKey = AddRow(table, 2, "설치된 키", true);
|
|
_txtBackupKey = AddRow(table, 3, "백업 키", true);
|
|
|
|
var buttons = new FlowLayoutPanel
|
|
{
|
|
Dock = DockStyle.Bottom,
|
|
FlowDirection = FlowDirection.RightToLeft,
|
|
Height = 56,
|
|
Padding = new Padding(10, 10, 10, 10)
|
|
};
|
|
|
|
var btnClose = MakeButton("닫기");
|
|
btnClose.Click += (s, e) => Close();
|
|
var btnSave = MakeButton("파일로 저장");
|
|
btnSave.Click += (s, e) => SaveToFile();
|
|
var btnCopy = MakeButton("복사");
|
|
btnCopy.Click += (s, e) => CopyToClipboard();
|
|
|
|
// RightToLeft 배치라 먼저 추가한 버튼이 오른쪽 → 화면 순서: [암호 변경] [복사] [파일로 저장] [닫기]
|
|
buttons.Controls.Add(btnClose);
|
|
buttons.Controls.Add(btnSave);
|
|
buttons.Controls.Add(btnCopy);
|
|
|
|
_btnChangePw = MakeButton("암호 변경");
|
|
_btnChangePw.Click += (s, e) => ChangePassword();
|
|
buttons.Controls.Add(_btnChangePw);
|
|
|
|
// 화면 하단 바로가기 버튼 (기본 브라우저로 열기)
|
|
_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 창이 막혀 있어도 동작)
|
|
_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);
|
|
CancelButton = btnClose;
|
|
|
|
try
|
|
{
|
|
_info = KeyReader.Read();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show("레지스트리에서 정보를 읽지 못했습니다.\n\n" + ex.Message,
|
|
Text, MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
_info = new KeyReader.KeyInfo
|
|
{
|
|
ComputerName = Environment.MachineName,
|
|
ProductName = "없음",
|
|
ProductId = "없음",
|
|
InstalledKey = "없음",
|
|
BackupKey = "없음"
|
|
};
|
|
}
|
|
|
|
_txtProductName.Text = _info.ProductName;
|
|
_txtProductId.Text = _info.ProductId;
|
|
_txtInstalledKey.Text = _info.InstalledKey;
|
|
_txtBackupKey.Text = _info.BackupKey;
|
|
|
|
// 처음 표시 모드 (DPI 배율 적용 전이므로 기준 크기로 지정)
|
|
_fullMode = fullMode;
|
|
_btnChangePw.Visible = fullMode;
|
|
_phone.Visible = fullMode;
|
|
_links.Visible = fullMode;
|
|
ClientSize = new Size(560, fullMode ? 344 : 240);
|
|
}
|
|
|
|
/// <summary>제한 모드 → 전체 모드 전환 (실행 중, DPI 배율이 적용된 실제 크기 기준으로 창 높이 증가)</summary>
|
|
private void SwitchToFullMode()
|
|
{
|
|
if (_fullMode) return;
|
|
_fullMode = true;
|
|
|
|
float scale = _phone.Height / 44f; // 현재 DPI 배율
|
|
int extra = _phone.Height + _links.Height + (int)Math.Round(10 * scale); // 344 - 240 = 104 = 44 + 50 + 10
|
|
|
|
SuspendLayout();
|
|
_btnChangePw.Visible = true;
|
|
_phone.Visible = true;
|
|
_links.Visible = true;
|
|
ClientSize = new Size(ClientSize.Width, ClientSize.Height + extra);
|
|
ResumeLayout(true);
|
|
}
|
|
|
|
/// <summary>제한 모드에서 Ctrl+Shift+P → 마스터 암호 입력 창</summary>
|
|
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
|
|
{
|
|
if (keyData == UnlockShortcut && !_fullMode)
|
|
{
|
|
using (var dlg = new MasterPasswordDialog())
|
|
{
|
|
if (dlg.ShowDialog(this) == DialogResult.OK) SwitchToFullMode();
|
|
}
|
|
return true;
|
|
}
|
|
return base.ProcessCmdKey(ref msg, keyData);
|
|
}
|
|
|
|
private static TextBox AddRow(TableLayoutPanel table, int row, string caption, bool monospace)
|
|
{
|
|
var label = new Label
|
|
{
|
|
Text = caption,
|
|
Dock = DockStyle.Fill,
|
|
TextAlign = ContentAlignment.MiddleLeft
|
|
};
|
|
var box = new TextBox
|
|
{
|
|
ReadOnly = true,
|
|
Dock = DockStyle.Fill,
|
|
Anchor = AnchorStyles.Left | AnchorStyles.Right,
|
|
BackColor = SystemColors.Window
|
|
};
|
|
if (monospace) box.Font = new Font("Consolas", 11f, FontStyle.Bold);
|
|
|
|
table.Controls.Add(label, 0, row);
|
|
table.Controls.Add(box, 1, row);
|
|
return box;
|
|
}
|
|
|
|
private static Button MakeButton(string text)
|
|
{
|
|
return new Button
|
|
{
|
|
Text = text,
|
|
Width = 110,
|
|
Height = 32,
|
|
Margin = new Padding(6, 0, 0, 0)
|
|
};
|
|
}
|
|
|
|
private Button MakeLinkButton(string text, string url)
|
|
{
|
|
var btn = new Button
|
|
{
|
|
Text = text,
|
|
Width = 130,
|
|
Height = 32,
|
|
Margin = new Padding(0, 0, 8, 0),
|
|
Cursor = Cursors.Hand
|
|
};
|
|
btn.Click += (s, e) => OpenUrl(url);
|
|
return btn;
|
|
}
|
|
|
|
private void OpenUrl(string url)
|
|
{
|
|
try
|
|
{
|
|
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(url) { UseShellExecute = true });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show("브라우저를 열지 못했습니다.\n\n" + url + "\n\n" + ex.Message, Text,
|
|
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
}
|
|
}
|
|
|
|
private static Button MakeActionButton(string text)
|
|
{
|
|
return new Button
|
|
{
|
|
Text = text,
|
|
Width = 140,
|
|
Height = 32,
|
|
Margin = new Padding(0, 0, 8, 0),
|
|
Cursor = Cursors.Hand
|
|
};
|
|
}
|
|
|
|
/// <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()
|
|
{
|
|
string windir = Environment.GetFolderPath(Environment.SpecialFolder.Windows);
|
|
string sysnative = Path.Combine(windir, "Sysnative");
|
|
if (!Environment.Is64BitProcess && Directory.Exists(sysnative)) return sysnative;
|
|
return Path.Combine(windir, "System32");
|
|
}
|
|
|
|
/// <summary>① 설치 ID 보기: cscript slmgr.vbs /dti 실행 후 결과 표시</summary>
|
|
private void ShowInstallationId()
|
|
{
|
|
string sys = SystemDir();
|
|
string cscript = Path.Combine(sys, "cscript.exe");
|
|
string slmgr = Path.Combine(sys, "slmgr.vbs");
|
|
string output;
|
|
|
|
Cursor = Cursors.WaitCursor;
|
|
try
|
|
{
|
|
var psi = new System.Diagnostics.ProcessStartInfo(cscript, "//nologo \"" + slmgr + "\" /dti")
|
|
{
|
|
UseShellExecute = false,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
CreateNoWindow = true,
|
|
StandardOutputEncoding = Encoding.GetEncoding(
|
|
System.Globalization.CultureInfo.CurrentCulture.TextInfo.OEMCodePage)
|
|
};
|
|
using (var p = System.Diagnostics.Process.Start(psi))
|
|
{
|
|
output = p.StandardOutput.ReadToEnd() + p.StandardError.ReadToEnd();
|
|
p.WaitForExit(60000);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Cursor = Cursors.Default;
|
|
MessageBox.Show("설치 ID를 가져오지 못했습니다.\n\n" + ex.Message, Text,
|
|
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
return;
|
|
}
|
|
Cursor = Cursors.Default;
|
|
|
|
// 출력에서 가장 긴 숫자열(설치 ID, 보통 63자리)을 추출
|
|
string iid = null;
|
|
foreach (System.Text.RegularExpressions.Match m in
|
|
System.Text.RegularExpressions.Regex.Matches(output, @"\d{40,}"))
|
|
{
|
|
if (iid == null || m.Value.Length > iid.Length) iid = m.Value;
|
|
}
|
|
|
|
if (iid == null)
|
|
{
|
|
MessageBox.Show("설치 ID를 찾지 못했습니다. 윈도우가 보낸 메시지:\n\n" + output.Trim(), Text,
|
|
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
|
return;
|
|
}
|
|
|
|
using (var dlg = new InstallationIdDialog(iid))
|
|
{
|
|
dlg.ShowDialog(this);
|
|
}
|
|
}
|
|
|
|
/// <summary>② 확인 ID 입력: 관리자 권한으로 slmgr.vbs /atp 확인ID 실행</summary>
|
|
private void EnterConfirmationId()
|
|
{
|
|
string cid;
|
|
using (var dlg = new ConfirmationIdDialog())
|
|
{
|
|
if (dlg.ShowDialog(this) != DialogResult.OK) return;
|
|
cid = dlg.ConfirmationId;
|
|
}
|
|
|
|
string sys = SystemDir();
|
|
string wscript = Path.Combine(sys, "wscript.exe");
|
|
string slmgr = Path.Combine(sys, "slmgr.vbs");
|
|
|
|
try
|
|
{
|
|
// wscript로 실행하면 slmgr가 결과(성공/오류)를 자체 메시지 창으로 보여 줌
|
|
var psi = new System.Diagnostics.ProcessStartInfo(wscript, "\"" + slmgr + "\" /atp " + cid)
|
|
{
|
|
UseShellExecute = true,
|
|
Verb = "runas" // 관리자 권한 요청 (UAC 창)
|
|
};
|
|
System.Diagnostics.Process.Start(psi);
|
|
}
|
|
catch (System.ComponentModel.Win32Exception ex) when (ex.NativeErrorCode == 1223)
|
|
{
|
|
MessageBox.Show("관리자 권한 요청이 취소되었습니다.\n확인 ID 입력에는 관리자 권한이 필요합니다.", Text,
|
|
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show("확인 ID 입력을 실행하지 못했습니다.\n\n" + ex.Message, Text,
|
|
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
}
|
|
}
|
|
|
|
/// <summary>원본 스크립트와 같은 형식의 텍스트</summary>
|
|
private string BuildText()
|
|
{
|
|
var sb = new StringBuilder();
|
|
sb.AppendLine("Product Name: " + _info.ProductName);
|
|
sb.AppendLine("Product ID: " + _info.ProductId);
|
|
sb.AppendLine("Orginal Installed Key: " + _info.InstalledKey);
|
|
sb.AppendLine("Backup Installed Key: " + _info.BackupKey);
|
|
return sb.ToString();
|
|
}
|
|
|
|
private void CopyToClipboard()
|
|
{
|
|
try
|
|
{
|
|
Clipboard.SetText(BuildText());
|
|
MessageBox.Show("키 정보를 클립보드에 복사했습니다.", Text,
|
|
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show("클립보드 복사에 실패했습니다.\n\n" + ex.Message, Text,
|
|
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
}
|
|
}
|
|
|
|
/// <summary>exe가 있는 폴더에 WindowsKeyInfo_컴퓨터이름_사용자이름.txt 로 저장</summary>
|
|
private void SaveToFile()
|
|
{
|
|
string computer = (_info.ComputerName ?? Environment.MachineName).Replace(" ", "");
|
|
string user = Environment.UserName;
|
|
string fileName = "WindowsKeyInfo_" + computer + "_" + user + ".txt";
|
|
string folder = AppDomain.CurrentDomain.BaseDirectory;
|
|
string fullPath = Path.Combine(folder, fileName);
|
|
|
|
try
|
|
{
|
|
File.WriteAllText(fullPath, BuildText(), new UTF8Encoding(true));
|
|
MessageBox.Show("저장했습니다.\n\n" + fullPath, Text,
|
|
MessageBoxButtons.OK, MessageBoxIcon.Information);
|
|
}
|
|
catch (UnauthorizedAccessException)
|
|
{
|
|
MessageBox.Show("이 폴더에 쓸 권한이 없습니다.\n\n" + folder +
|
|
"\n\nexe를 바탕화면이나 문서 폴더 등 쓰기 가능한 곳으로 옮긴 뒤 다시 시도해 주세요.",
|
|
Text, MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show("파일 저장에 실패했습니다.\n\n" + ex.Message, Text,
|
|
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>설치 ID 표시 창 (7자리씩 9묶음)</summary>
|
|
internal sealed class InstallationIdDialog : Form
|
|
{
|
|
public InstallationIdDialog(string iid)
|
|
{
|
|
Text = "설치 ID (Installation ID)";
|
|
FormBorderStyle = FormBorderStyle.FixedDialog;
|
|
MaximizeBox = false;
|
|
MinimizeBox = false;
|
|
ShowInTaskbar = false;
|
|
StartPosition = FormStartPosition.CenterParent;
|
|
AutoScaleMode = AutoScaleMode.Dpi;
|
|
Font = new Font("맑은 고딕", 9.5f);
|
|
ClientSize = new Size(620, 190);
|
|
|
|
var sbGrouped = new StringBuilder();
|
|
for (int i = 0; i < iid.Length; i += 7)
|
|
{
|
|
if (i > 0) sbGrouped.Append(' ');
|
|
sbGrouped.Append(iid.Substring(i, Math.Min(7, iid.Length - i)));
|
|
}
|
|
string grouped = sbGrouped.ToString();
|
|
|
|
var lbl = new Label
|
|
{
|
|
Text = "아래 설치 ID를 전화 자동 응답에 입력하거나 확인 ID 발급처에 전달하세요.\n" +
|
|
"받은 확인 ID는 메인 창의 [② 확인 ID 입력]에 넣으면 됩니다.",
|
|
Location = new Point(14, 12),
|
|
Size = new Size(592, 42)
|
|
};
|
|
var box = new TextBox
|
|
{
|
|
Text = grouped,
|
|
ReadOnly = true,
|
|
Multiline = true,
|
|
Location = new Point(14, 60),
|
|
Size = new Size(592, 64),
|
|
Font = new Font("Consolas", 12f, FontStyle.Bold),
|
|
BackColor = SystemColors.Window
|
|
};
|
|
|
|
var btnCopy = new Button { Text = "복사 (숫자만)", Location = new Point(366, 140), Size = new Size(120, 32) };
|
|
btnCopy.Click += (s, e) =>
|
|
{
|
|
try
|
|
{
|
|
Clipboard.SetText(iid);
|
|
MessageBox.Show("설치 ID를 클립보드에 복사했습니다.", Text, MessageBoxButtons.OK, MessageBoxIcon.Information);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show("복사에 실패했습니다.\n\n" + ex.Message, Text, MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
}
|
|
};
|
|
var btnClose = new Button { Text = "닫기", Location = new Point(494, 140), Size = new Size(112, 32), DialogResult = DialogResult.OK };
|
|
|
|
Controls.Add(lbl);
|
|
Controls.Add(box);
|
|
Controls.Add(btnCopy);
|
|
Controls.Add(btnClose);
|
|
AcceptButton = btnClose;
|
|
CancelButton = btnClose;
|
|
}
|
|
}
|
|
|
|
/// <summary>확인 ID 입력 창 (48자리 숫자, 하이픈/공백 허용)</summary>
|
|
internal sealed class ConfirmationIdDialog : Form
|
|
{
|
|
private readonly TextBox _box;
|
|
public string ConfirmationId { get; private set; }
|
|
|
|
public ConfirmationIdDialog()
|
|
{
|
|
Text = "확인 ID (Confirmation ID) 입력";
|
|
FormBorderStyle = FormBorderStyle.FixedDialog;
|
|
MaximizeBox = false;
|
|
MinimizeBox = false;
|
|
ShowInTaskbar = false;
|
|
StartPosition = FormStartPosition.CenterParent;
|
|
AutoScaleMode = AutoScaleMode.Dpi;
|
|
Font = new Font("맑은 고딕", 9.5f);
|
|
ClientSize = new Size(620, 170);
|
|
|
|
var lbl = new Label
|
|
{
|
|
Text = "받은 확인 ID 48자리를 입력하세요. 하이픈(-)이나 공백은 넣어도 자동으로 제거됩니다.\n" +
|
|
"[인증]을 누르면 관리자 권한 확인 창이 뜨고, 결과는 윈도우 메시지 창으로 표시됩니다.",
|
|
Location = new Point(14, 12),
|
|
Size = new Size(592, 42)
|
|
};
|
|
_box = new TextBox
|
|
{
|
|
Location = new Point(14, 62),
|
|
Size = new Size(592, 30),
|
|
Font = new Font("Consolas", 12f, FontStyle.Bold)
|
|
};
|
|
|
|
var btnOk = new Button { Text = "인증", Location = new Point(366, 120), Size = new Size(120, 32) };
|
|
btnOk.Click += (s, e) => TryAccept();
|
|
var btnCancel = new Button { Text = "취소", Location = new Point(494, 120), Size = new Size(112, 32), DialogResult = DialogResult.Cancel };
|
|
|
|
Controls.Add(lbl);
|
|
Controls.Add(_box);
|
|
Controls.Add(btnOk);
|
|
Controls.Add(btnCancel);
|
|
AcceptButton = btnOk;
|
|
CancelButton = btnCancel;
|
|
}
|
|
|
|
private void TryAccept()
|
|
{
|
|
var digits = new StringBuilder();
|
|
foreach (char c in _box.Text)
|
|
{
|
|
if (c >= '0' && c <= '9') digits.Append(c);
|
|
}
|
|
|
|
if (digits.Length != 48)
|
|
{
|
|
MessageBox.Show("확인 ID는 숫자 48자리여야 합니다.\n현재 입력된 숫자: " + digits.Length + "자리", Text,
|
|
MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
|
_box.Focus();
|
|
return;
|
|
}
|
|
|
|
ConfirmationId = digits.ToString();
|
|
DialogResult = DialogResult.OK;
|
|
Close();
|
|
}
|
|
}
|
|
}
|