diff options
Diffstat (limited to 'windows/pageant.c')
-rw-r--r-- | windows/pageant.c | 1958 |
1 files changed, 1958 insertions, 0 deletions
diff --git a/windows/pageant.c b/windows/pageant.c new file mode 100644 index 00000000..d1368903 --- /dev/null +++ b/windows/pageant.c @@ -0,0 +1,1958 @@ +/* + * Pageant: the PuTTY Authentication Agent. + */ + +#include <stdio.h> +#include <stdlib.h> +#include <stddef.h> +#include <ctype.h> +#include <assert.h> +#include <tchar.h> + +#include "putty.h" +#include "ssh.h" +#include "misc.h" +#include "tree234.h" +#include "security-api.h" +#include "cryptoapi.h" +#include "pageant.h" +#include "licence.h" +#include "pageant-rc.h" + +#include <shellapi.h> + +#include <aclapi.h> +#ifdef DEBUG_IPC +#define _WIN32_WINNT 0x0500 /* for ConvertSidToStringSid */ +#include <sddl.h> +#endif + +#define WM_SYSTRAY (WM_APP + 6) +#define WM_SYSTRAY2 (WM_APP + 7) + +#define APPNAME "Pageant" + +/* Titles and class names for invisible windows. IPCWINTITLE and + * IPCCLASSNAME are critical to backwards compatibility: WM_COPYDATA + * based Pageant clients will call FindWindow with those parameters + * and expect to find the Pageant IPC receiver. */ +#define TRAYWINTITLE "Pageant" +#define TRAYCLASSNAME "PageantSysTray" +#define IPCWINTITLE "Pageant" +#define IPCCLASSNAME "Pageant" + +static HWND traywindow; +static HWND keylist; +static HWND aboutbox; +static HMENU systray_menu, session_menu; +static bool already_running; +static FingerprintType fptype = SSH_FPTYPE_DEFAULT; + +static char *putty_path; +static bool restrict_putty_acl = false; + +/* CWD for "add key" file requester. */ +static filereq *keypath = NULL; + +/* From MSDN: In the WM_SYSCOMMAND message, the four low-order bits of + * wParam are used by Windows, and should be masked off, so we shouldn't + * attempt to store information in them. Hence all these identifiers have + * the low 4 bits clear. Also, identifiers should < 0xF000. */ + +#define IDM_CLOSE 0x0010 +#define IDM_VIEWKEYS 0x0020 +#define IDM_ADDKEY 0x0030 +#define IDM_ADDKEY_ENCRYPTED 0x0040 +#define IDM_REMOVE_ALL 0x0050 +#define IDM_REENCRYPT_ALL 0x0060 +#define IDM_HELP 0x0070 +#define IDM_ABOUT 0x0080 +#define IDM_PUTTY 0x0090 +#define IDM_SESSIONS_BASE 0x1000 +#define IDM_SESSIONS_MAX 0x2000 +#define PUTTY_REGKEY "Software\\SimonTatham\\PuTTY\\Sessions" +#define PUTTY_DEFAULT "Default%20Settings" +static int initial_menuitems_count; + +/* + * Print a modal (Really Bad) message box and perform a fatal exit. + */ +void modalfatalbox(const char *fmt, ...) +{ + va_list ap; + char *buf; + + va_start(ap, fmt); + buf = dupvprintf(fmt, ap); + va_end(ap); + MessageBox(traywindow, buf, "Pageant Fatal Error", + MB_SYSTEMMODAL | MB_ICONERROR | MB_OK); + sfree(buf); + exit(1); +} + +struct PassphraseProcStruct { + bool modal; + const char *help_topic; + PageantClientDialogId *dlgid; + char *passphrase; + const char *comment; +}; + +/* + * Dialog-box function for the Licence box. + */ +static INT_PTR CALLBACK LicenceProc(HWND hwnd, UINT msg, + WPARAM wParam, LPARAM lParam) +{ + switch (msg) { + case WM_INITDIALOG: + SetDlgItemText(hwnd, IDC_LICENCE_TEXTBOX, LICENCE_TEXT("\r\n\r\n")); + return 1; + case WM_COMMAND: + switch (LOWORD(wParam)) { + case IDOK: + case IDCANCEL: + EndDialog(hwnd, 1); + return 0; + } + return 0; + case WM_CLOSE: + EndDialog(hwnd, 1); + return 0; + } + return 0; +} + +/* + * Dialog-box function for the About box. + */ +static INT_PTR CALLBACK AboutProc(HWND hwnd, UINT msg, + WPARAM wParam, LPARAM lParam) +{ + switch (msg) { + case WM_INITDIALOG: { + char *buildinfo_text = buildinfo("\r\n"); + char *text = dupprintf( + "Pageant\r\n\r\n%s\r\n\r\n%s\r\n\r\n%s", + ver, buildinfo_text, + "\251 " SHORT_COPYRIGHT_DETAILS ". All rights reserved."); + sfree(buildinfo_text); + SetDlgItemText(hwnd, IDC_ABOUT_TEXTBOX, text); + MakeDlgItemBorderless(hwnd, IDC_ABOUT_TEXTBOX); + sfree(text); + return 1; + } + case WM_COMMAND: + switch (LOWORD(wParam)) { + case IDOK: + case IDCANCEL: + aboutbox = NULL; + DestroyWindow(hwnd); + return 0; + case IDC_ABOUT_LICENCE: + EnableWindow(hwnd, 0); + DialogBox(hinst, MAKEINTRESOURCE(IDD_LICENCE), hwnd, LicenceProc); + EnableWindow(hwnd, 1); + SetActiveWindow(hwnd); + return 0; + case IDC_ABOUT_WEBSITE: + /* Load web browser */ + ShellExecute(hwnd, "open", + "https://www.chiark.greenend.org.uk/~sgtatham/putty/", + 0, 0, SW_SHOWDEFAULT); + return 0; + } + return 0; + case WM_CLOSE: + aboutbox = NULL; + DestroyWindow(hwnd); + return 0; + } + return 0; +} + +static HWND modal_passphrase_hwnd = NULL; +static HWND nonmodal_passphrase_hwnd = NULL; + +static void end_passphrase_dialog(HWND hwnd, INT_PTR result) +{ + struct PassphraseProcStruct *p = (struct PassphraseProcStruct *) + GetWindowLongPtr(hwnd, GWLP_USERDATA); + + if (p->modal) { + EndDialog(hwnd, result); + } else { + /* + * Destroy this passphrase dialog box before passing the + * results back to the main pageant.c, to avoid re-entrancy + * issues. + * + * If we successfully got a passphrase from the user, but it + * was _wrong_, then pageant_passphrase_request_success will + * respond by calling back - synchronously - to our + * ask_passphrase() implementation, which will expect the + * previous value of nonmodal_passphrase_hwnd to have already + * been cleaned up. + */ + SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR) NULL); + DestroyWindow(hwnd); + nonmodal_passphrase_hwnd = NULL; + + if (result) + pageant_passphrase_request_success( + p->dlgid, ptrlen_from_asciz(p->passphrase)); + else + pageant_passphrase_request_refused(p->dlgid); + + burnstr(p->passphrase); + sfree(p); + } +} + +/* + * Dialog-box function for the passphrase box. + */ +static INT_PTR CALLBACK PassphraseProc(HWND hwnd, UINT msg, + WPARAM wParam, LPARAM lParam) +{ + struct PassphraseProcStruct *p; + + if (msg == WM_INITDIALOG) { + p = (struct PassphraseProcStruct *) lParam; + SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR) p); + } else { + p = (struct PassphraseProcStruct *) + GetWindowLongPtr(hwnd, GWLP_USERDATA); + } + + switch (msg) { + case WM_INITDIALOG: { + if (p->modal) + modal_passphrase_hwnd = hwnd; + + /* + * Centre the window. + */ + RECT rs, rd; + HWND hw; + + hw = GetDesktopWindow(); + if (GetWindowRect(hw, &rs) && GetWindowRect(hwnd, &rd)) + MoveWindow(hwnd, + (rs.right + rs.left + rd.left - rd.right) / 2, + (rs.bottom + rs.top + rd.top - rd.bottom) / 2, + rd.right - rd.left, rd.bottom - rd.top, true); + + SetForegroundWindow(hwnd); + SetWindowPos(hwnd, HWND_TOP, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW); + if (!p->modal) + SetActiveWindow(hwnd); /* this won't have happened automatically */ + if (p->comment) + SetDlgItemText(hwnd, IDC_PASSPHRASE_FINGERPRINT, p->comment); + burnstr(p->passphrase); + p->passphrase = dupstr(""); + SetDlgItemText(hwnd, IDC_PASSPHRASE_EDITBOX, p->passphrase); + if (!p->help_topic || !has_help()) { + HWND item = GetDlgItem(hwnd, IDHELP); + if (item) + DestroyWindow(item); + } + return 0; + } + case WM_COMMAND: + switch (LOWORD(wParam)) { + case IDOK: + if (p->passphrase) + end_passphrase_dialog(hwnd, 1); + else + MessageBeep(0); + return 0; + case IDCANCEL: + end_passphrase_dialog(hwnd, 0); + return 0; + case IDHELP: + if (p->help_topic) + launch_help(hwnd, p->help_topic); + return 0; + case IDC_PASSPHRASE_EDITBOX: + if ((HIWORD(wParam) == EN_CHANGE) && p->passphrase) { + burnstr(p->passphrase); + p->passphrase = GetDlgItemText_alloc( + hwnd, IDC_PASSPHRASE_EDITBOX); + } + return 0; + } + return 0; + case WM_CLOSE: + end_passphrase_dialog(hwnd, 0); + return 0; + } + return 0; +} + +/* + * Warn about the obsolescent key file format. + */ +void old_keyfile_warning(void) +{ + static const char mbtitle[] = "PuTTY Key File Warning"; + static const char message[] = + "You are loading an SSH-2 private key which has an\n" + "old version of the file format. This means your key\n" + "file is not fully tamperproof. Future versions of\n" + "PuTTY may stop supporting this private key format,\n" + "so we recommend you convert your key to the new\n" + "format.\n" + "\n" + "You can perform this conversion by loading the key\n" + "into PuTTYgen and then saving it again."; + + MessageBox(NULL, message, mbtitle, MB_OK); +} + +struct keylist_update_ctx { + HDC hdc; + int algbitswidth, algwidth, bitswidth, hashwidth; + bool enable_remove_controls; + bool enable_reencrypt_controls; +}; + +struct keylist_display_data { + strbuf *alg, *bits, *hash, *comment, *info; +}; + +static void keylist_update_callback( + void *vctx, char **fingerprints, const char *comment, uint32_t ext_flags, + struct pageant_pubkey *key) +{ + struct keylist_update_ctx *ctx = (struct keylist_update_ctx *)vctx; + FingerprintType this_type = ssh2_pick_fingerprint(fingerprints, fptype); + ptrlen fingerprint = ptrlen_from_asciz(fingerprints[this_type]); + + struct keylist_display_data *disp = snew(struct keylist_display_data); + disp->alg = strbuf_new(); + disp->bits = strbuf_new(); + disp->hash = strbuf_new(); + disp->comment = strbuf_new(); + disp->info = strbuf_new(); + + /* There is at least one key, so the controls for removing keys + * should be enabled */ + ctx->enable_remove_controls = true; + + switch (key->ssh_version) { + case 1: { + /* + * Expect the fingerprint to contain two words: bit count and + * hash. + */ + put_dataz(disp->alg, "SSH-1"); + put_datapl(disp->bits, ptrlen_get_word(&fingerprint, " ")); + put_datapl(disp->hash, ptrlen_get_word(&fingerprint, " ")); + break; + } + + case 2: { + /* + * Expect the fingerprint to contain three words: algorithm + * name, bit count, hash. + */ + const ssh_keyalg *alg = pubkey_blob_to_alg( + ptrlen_from_strbuf(key->blob)); + + ptrlen keytype_word = ptrlen_get_word(&fingerprint, " "); + if (alg) { + /* Use our own human-legible algorithm names if available, + * because they fit better in the space. (Certificate key + * algorithm names in particular are terribly long.) */ + char *alg_desc = ssh_keyalg_desc(alg); + put_dataz(disp->alg, alg_desc); + sfree(alg_desc); + } else { + put_datapl(disp->alg, keytype_word); + } + + ptrlen bits_word = ptrlen_get_word(&fingerprint, " "); + if (alg && ssh_keyalg_variable_size(alg)) + put_datapl(disp->bits, bits_word); + + put_datapl(disp->hash, ptrlen_get_word(&fingerprint, " ")); + } + } + + put_dataz(disp->comment, comment); + + SIZE sz; + if (disp->bits->len) { + GetTextExtentPoint32(ctx->hdc, disp->alg->s, disp->alg->len, &sz); + if (ctx->algwidth < sz.cx) ctx->algwidth = sz.cx; + GetTextExtentPoint32(ctx->hdc, disp->bits->s, disp->bits->len, &sz); + if (ctx->bitswidth < sz.cx) ctx->bitswidth = sz.cx; + } else { + GetTextExtentPoint32(ctx->hdc, disp->alg->s, disp->alg->len, &sz); + if (ctx->algbitswidth < sz.cx) ctx->algbitswidth = sz.cx; + } + GetTextExtentPoint32(ctx->hdc, disp->hash->s, disp->hash->len, &sz); + if (ctx->hashwidth < sz.cx) ctx->hashwidth = sz.cx; + + if (ext_flags & LIST_EXTENDED_FLAG_HAS_NO_CLEARTEXT_KEY) { + put_fmt(disp->info, "(encrypted)"); + } else if (ext_flags & LIST_EXTENDED_FLAG_HAS_ENCRYPTED_KEY_FILE) { + put_fmt(disp->info, "(re-encryptable)"); + + /* At least one key can be re-encrypted */ + ctx->enable_reencrypt_controls = true; + } + + /* This list box is owner-drawn but doesn't have LBS_HASSTRINGS, + * so we can use LB_ADDSTRING to hand the list box our display + * info pointer */ + SendDlgItemMessage(keylist, IDC_KEYLIST_LISTBOX, + LB_ADDSTRING, 0, (LPARAM)disp); +} + +/* Column start positions for the list box, in pixels (not dialog units). */ +static int colpos_bits, colpos_hash, colpos_comment; + +/* + * Update the visible key list. + */ +void keylist_update(void) +{ + if (keylist) { + /* + * Clear the previous list box content and free their display + * structures. + */ + { + int nitems = SendDlgItemMessage(keylist, IDC_KEYLIST_LISTBOX, + LB_GETCOUNT, 0, 0); + for (int i = 0; i < nitems; i++) { + struct keylist_display_data *disp = + (struct keylist_display_data *)SendDlgItemMessage( + keylist, IDC_KEYLIST_LISTBOX, LB_GETITEMDATA, i, 0); + strbuf_free(disp->alg); + strbuf_free(disp->bits); + strbuf_free(disp->hash); + strbuf_free(disp->comment); + strbuf_free(disp->info); + sfree(disp); + } + } + SendDlgItemMessage(keylist, IDC_KEYLIST_LISTBOX, + LB_RESETCONTENT, 0, 0); + + char *errmsg; + struct keylist_update_ctx ctx[1]; + ctx->enable_remove_controls = false; + ctx->enable_reencrypt_controls = false; + ctx->algbitswidth = ctx->algwidth = 0; + ctx->bitswidth = ctx->hashwidth = 0; + ctx->hdc = GetDC(keylist); + SelectObject(ctx->hdc, (HFONT)SendMessage(keylist, WM_GETFONT, 0, 0)); + int status = pageant_enum_keys(keylist_update_callback, ctx, &errmsg); + + SIZE sz; + GetTextExtentPoint32(ctx->hdc, "MM", 2, &sz); + int gutter = sz.cx; + + DeleteDC(ctx->hdc); + colpos_hash = ctx->algwidth + ctx->bitswidth + 2*gutter; + if (colpos_hash < ctx->algbitswidth + gutter) + colpos_hash = ctx->algbitswidth + gutter; + colpos_bits = colpos_hash - ctx->bitswidth - gutter; + colpos_comment = colpos_hash + ctx->hashwidth + gutter; + assert(status == PAGEANT_ACTION_OK); + assert(!errmsg); + + SendDlgItemMessage(keylist, IDC_KEYLIST_LISTBOX, + LB_SETCURSEL, (WPARAM) - 1, 0); + + EnableWindow(GetDlgItem(keylist, IDC_KEYLIST_REMOVE), + ctx->enable_remove_controls); + EnableWindow(GetDlgItem(keylist, IDC_KEYLIST_REENCRYPT), + ctx->enable_reencrypt_controls); + } +} + +static void win_add_keyfile(Filename *filename, bool encrypted) +{ + char *err; + int ret; + + /* + * Try loading the key without a passphrase. (Or rather, without a + * _new_ passphrase; pageant_add_keyfile will take care of trying + * all the passphrases we've already stored.) + */ + ret = pageant_add_keyfile(filename, NULL, &err, encrypted); + if (ret == PAGEANT_ACTION_OK) { + goto done; + } else if (ret == PAGEANT_ACTION_FAILURE) { + goto error; + } + + /* + * OK, a passphrase is needed, and we've been given the key + * comment to use in the passphrase prompt. + */ + while (1) { + INT_PTR dlgret; + struct PassphraseProcStruct pps; + pps.modal = true; + pps.help_topic = NULL; /* this dialog has no help button */ + pps.dlgid = NULL; + pps.passphrase = NULL; + pps.comment = err; + dlgret = DialogBoxParam( + hinst, MAKEINTRESOURCE(IDD_LOAD_PASSPHRASE), + NULL, PassphraseProc, (LPARAM) &pps); + modal_passphrase_hwnd = NULL; + + if (!dlgret) { + burnstr(pps.passphrase); + goto done; /* operation cancelled */ + } + + sfree(err); + + assert(pps.passphrase != NULL); + + ret = pageant_add_keyfile(filename, pps.passphrase, &err, false); + burnstr(pps.passphrase); + + if (ret == PAGEANT_ACTION_OK) { + goto done; + } else if (ret == PAGEANT_ACTION_FAILURE) { + goto error; + } + } + + error: + message_box(traywindow, err, APPNAME, MB_OK | MB_ICONERROR, + HELPCTXID(errors_cantloadkey)); + done: + sfree(err); + return; +} + +/* + * Prompt for a key file to add, and add it. + */ +static void prompt_add_keyfile(bool encrypted) +{ + OPENFILENAME of; + char *filelist = snewn(8192, char); + + if (!keypath) keypath = filereq_new(); + memset(&of, 0, sizeof(of)); + of.hwndOwner = traywindow; + of.lpstrFilter = FILTER_KEY_FILES; + of.lpstrCustomFilter = NULL; + of.nFilterIndex = 1; + of.lpstrFile = filelist; + *filelist = '\0'; + of.nMaxFile = 8192; + of.lpstrFileTitle = NULL; + of.lpstrTitle = "Select Private Key File"; + of.Flags = OFN_ALLOWMULTISELECT | OFN_EXPLORER; + if (request_file(keypath, &of, true, false)) { + if(strlen(filelist) > of.nFileOffset) { + /* Only one filename returned? */ + Filename *fn = filename_from_str(filelist); + win_add_keyfile(fn, encrypted); + filename_free(fn); + } else { + /* we are returned a bunch of strings, end to + * end. first string is the directory, the + * rest the filenames. terminated with an + * empty string. + */ + char *dir = filelist; + char *filewalker = filelist + strlen(dir) + 1; + while (*filewalker != '\0') { + char *filename = dupcat(dir, "\\", filewalker); + Filename *fn = filename_from_str(filename); + win_add_keyfile(fn, encrypted); + filename_free(fn); + sfree(filename); + filewalker += strlen(filewalker) + 1; + } + } + + keylist_update(); + pageant_forget_passphrases(); + } + sfree(filelist); +} + +/* + * Dialog-box function for the key list box. + */ +static INT_PTR CALLBACK KeyListProc(HWND hwnd, UINT msg, + WPARAM wParam, LPARAM lParam) +{ + static const struct { + const char *name; + FingerprintType value; + } fptypes[] = { + {"SHA256", SSH_FPTYPE_SHA256}, + {"MD5", SSH_FPTYPE_MD5}, + {"SHA256 including certificate", SSH_FPTYPE_SHA256_CERT}, + {"MD5 including certificate", SSH_FPTYPE_MD5_CERT}, + }; + + switch (msg) { + case WM_INITDIALOG: { + /* + * Centre the window. + */ + RECT rs, rd; + HWND hw; + + hw = GetDesktopWindow(); + if (GetWindowRect(hw, &rs) && GetWindowRect(hwnd, &rd)) + MoveWindow(hwnd, + (rs.right + rs.left + rd.left - rd.right) / 2, + (rs.bottom + rs.top + rd.top - rd.bottom) / 2, + rd.right - rd.left, rd.bottom - rd.top, true); + + if (has_help()) + SetWindowLongPtr(hwnd, GWL_EXSTYLE, + GetWindowLongPtr(hwnd, GWL_EXSTYLE) | + WS_EX_CONTEXTHELP); + else { + HWND item = GetDlgItem(hwnd, IDC_KEYLIST_HELP); + if (item) + DestroyWindow(item); + } + + keylist = hwnd; + + int selection = 0; + for (size_t i = 0; i < lenof(fptypes); i++) { + SendDlgItemMessage(hwnd, IDC_KEYLIST_FPTYPE, CB_ADDSTRING, + 0, (LPARAM)fptypes[i].name); + if (fptype == fptypes[i].value) + selection = (int)i; + } + SendDlgItemMessage(hwnd, IDC_KEYLIST_FPTYPE, + CB_SETCURSEL, 0, selection); + + keylist_update(); + return 0; + } + case WM_MEASUREITEM: { + assert(wParam == IDC_KEYLIST_LISTBOX); + + MEASUREITEMSTRUCT *mi = (MEASUREITEMSTRUCT *)lParam; + + /* + * Our list box is owner-drawn, but we put normal text in it. + * So the line height is the same as it would normally be, + * which is 8 dialog units. + */ + RECT r; + r.left = r.right = r.top = 0; + r.bottom = 8; + MapDialogRect(hwnd, &r); + mi->itemHeight = r.bottom; + + return 0; + } + case WM_DRAWITEM: { + assert(wParam == IDC_KEYLIST_LISTBOX); + + DRAWITEMSTRUCT *di = (DRAWITEMSTRUCT *)lParam; + + if (di->itemAction == ODA_FOCUS) { + /* Just toggle the focus rectangle either on or off. This + * is an XOR-type function, so it's the same call in + * either case. */ + DrawFocusRect(di->hDC, &di->rcItem); + } else { + /* Draw the full text. */ + bool selected = (di->itemState & ODS_SELECTED); + COLORREF newfg = GetSysColor( + selected ? COLOR_HIGHLIGHTTEXT : COLOR_WINDOWTEXT); + COLORREF newbg = GetSysColor( + selected ? COLOR_HIGHLIGHT : COLOR_WINDOW); + COLORREF oldfg = SetTextColor(di->hDC, newfg); + COLORREF oldbg = SetBkColor(di->hDC, newbg); + + HFONT font = (HFONT)SendMessage(hwnd, WM_GETFONT, 0, 0); + HFONT oldfont = SelectObject(di->hDC, font); + + /* ExtTextOut("") is an easy way to just draw the + * background rectangle */ + ExtTextOut(di->hDC, di->rcItem.left, di->rcItem.top, + ETO_OPAQUE | ETO_CLIPPED, &di->rcItem, "", 0, NULL); + + struct keylist_display_data *disp = + (struct keylist_display_data *)di->itemData; + + RECT r; + + /* Apparently real list boxes start drawing at x=1, not x=0 */ + r.left = r.top = r.bottom = 0; + r.right = 1; + MapDialogRect(hwnd, &r); + ExtTextOut(di->hDC, di->rcItem.left + r.right, di->rcItem.top, + ETO_CLIPPED, &di->rcItem, disp->alg->s, + disp->alg->len, NULL); + + if (disp->bits->len) { + ExtTextOut(di->hDC, di->rcItem.left + r.right + colpos_bits, + di->rcItem.top, ETO_CLIPPED, &di->rcItem, + disp->bits->s, disp->bits->len, NULL); + } + + ExtTextOut(di->hDC, di->rcItem.left + r.right + colpos_hash, + di->rcItem.top, ETO_CLIPPED, &di->rcItem, + disp->hash->s, disp->hash->len, NULL); + + strbuf *sb = strbuf_new(); + put_datapl(sb, ptrlen_from_strbuf(disp->comment)); + if (disp->info->len) { + put_byte(sb, '\t'); + put_datapl(sb, ptrlen_from_strbuf(disp->info)); + } + + TabbedTextOut(di->hDC, di->rcItem.left + r.right + colpos_comment, + di->rcItem.top, sb->s, sb->len, 0, NULL, 0); + + strbuf_free(sb); + + SetTextColor(di->hDC, oldfg); + SetBkColor(di->hDC, oldbg); + SelectObject(di->hDC, oldfont); + + if (di->itemState & ODS_FOCUS) + DrawFocusRect(di->hDC, &di->rcItem); + } + return 0; + } + case WM_COMMAND: + switch (LOWORD(wParam)) { + case IDOK: + case IDCANCEL: + keylist = NULL; + DestroyWindow(hwnd); + return 0; + case IDC_KEYLIST_ADDKEY: + case IDC_KEYLIST_ADDKEY_ENC: + if (HIWORD(wParam) == BN_CLICKED || + HIWORD(wParam) == BN_DOUBLECLICKED) { + if (modal_passphrase_hwnd) { + MessageBeep(MB_ICONERROR); + SetForegroundWindow(modal_passphrase_hwnd); + break; + } + prompt_add_keyfile(LOWORD(wParam) == IDC_KEYLIST_ADDKEY_ENC); + } + return 0; + case IDC_KEYLIST_REMOVE: + case IDC_KEYLIST_REENCRYPT: + if (HIWORD(wParam) == BN_CLICKED || + HIWORD(wParam) == BN_DOUBLECLICKED) { + int i; + int rCount, sCount; + int *selectedArray; + + /* our counter within the array of selected items */ + int itemNum; + + /* get the number of items selected in the list */ + int numSelected = SendDlgItemMessage( + hwnd, IDC_KEYLIST_LISTBOX, LB_GETSELCOUNT, 0, 0); + + /* none selected? that was silly */ + if (numSelected == 0) { + MessageBeep(0); + break; + } + + /* get item indices in an array */ + selectedArray = snewn(numSelected, int); + SendDlgItemMessage(hwnd, IDC_KEYLIST_LISTBOX, LB_GETSELITEMS, + numSelected, (WPARAM)selectedArray); + + itemNum = numSelected - 1; + rCount = pageant_count_ssh1_keys(); + sCount = pageant_count_ssh2_keys(); + + /* go through the non-rsakeys until we've covered them all, + * and/or we're out of selected items to check. note that + * we go *backwards*, to avoid complications from deleting + * things hence altering the offset of subsequent items + */ + for (i = sCount - 1; (itemNum >= 0) && (i >= 0); i--) { + if (selectedArray[itemNum] == rCount + i) { + switch (LOWORD(wParam)) { + case IDC_KEYLIST_REMOVE: + pageant_delete_nth_ssh2_key(i); + break; + case IDC_KEYLIST_REENCRYPT: + pageant_reencrypt_nth_ssh2_key(i); + break; + } + itemNum--; + } + } + + /* do the same for the rsa keys */ + for (i = rCount - 1; (itemNum >= 0) && (i >= 0); i--) { + if(selectedArray[itemNum] == i) { + switch (LOWORD(wParam)) { + case IDC_KEYLIST_REMOVE: + pageant_delete_nth_ssh1_key(i); + break; + case IDC_KEYLIST_REENCRYPT: + /* SSH-1 keys can't be re-encrypted */ + break; + } + itemNum--; + } + } + + sfree(selectedArray); + keylist_update(); + } + return 0; + case IDC_KEYLIST_HELP: + if (HIWORD(wParam) == BN_CLICKED || + HIWORD(wParam) == BN_DOUBLECLICKED) { + launch_help(hwnd, WINHELP_CTX_pageant_general); + } + return 0; + case IDC_KEYLIST_FPTYPE: + if (HIWORD(wParam) == CBN_SELCHANGE) { + int selection = SendDlgItemMessage( + hwnd, IDC_KEYLIST_FPTYPE, CB_GETCURSEL, 0, 0); + if (selection >= 0 && (size_t)selection < lenof(fptypes)) { + fptype = fptypes[selection].value; + keylist_update(); + } + } + return 0; + } + return 0; + case WM_HELP: { + int id = ((LPHELPINFO)lParam)->iCtrlId; + const char *topic = NULL; + switch (id) { + case IDC_KEYLIST_LISTBOX: + case IDC_KEYLIST_FPTYPE: + case IDC_KEYLIST_FPTYPE_STATIC: + topic = WINHELP_CTX_pageant_keylist; break; + case IDC_KEYLIST_ADDKEY: topic = WINHELP_CTX_pageant_addkey; break; + case IDC_KEYLIST_REMOVE: topic = WINHELP_CTX_pageant_remkey; break; + case IDC_KEYLIST_ADDKEY_ENC: + case IDC_KEYLIST_REENCRYPT: + topic = WINHELP_CTX_pageant_deferred; break; + } + if (topic) { + launch_help(hwnd, topic); + } else { + MessageBeep(0); + } + break; + } + case WM_CLOSE: + keylist = NULL; + DestroyWindow(hwnd); + return 0; + } + return 0; +} + +/* Set up a system tray icon */ +static BOOL AddTrayIcon(HWND hwnd) +{ + BOOL res; + NOTIFYICONDATA tnid; + HICON hicon; + +#ifdef NIM_SETVERSION + tnid.uVersion = 0; + res = Shell_NotifyIcon(NIM_SETVERSION, &tnid); +#endif + + tnid.cbSize = sizeof(NOTIFYICONDATA); + tnid.hWnd = hwnd; + tnid.uID = 1; /* unique within this systray use */ + tnid.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP; + tnid.uCallbackMessage = WM_SYSTRAY; + tnid.hIcon = hicon = LoadIcon(hinst, MAKEINTRESOURCE(201)); + strcpy(tnid.szTip, "Pageant (PuTTY authentication agent)"); + + res = Shell_NotifyIcon(NIM_ADD, &tnid); + + if (hicon) DestroyIcon(hicon); + + return res; +} + +/* Update the saved-sessions menu. */ +static void update_sessions(void) +{ + int num_entries; + HKEY hkey; + TCHAR buf[MAX_PATH + 1]; + MENUITEMINFO mii; + strbuf *sb; + + int index_key, index_menu; + + if (!putty_path) + return; + + if(ERROR_SUCCESS != RegOpenKey(HKEY_CURRENT_USER, PUTTY_REGKEY, &hkey)) + return; + + for(num_entries = GetMenuItemCount(session_menu); + num_entries > initial_menuitems_count; + num_entries--) + RemoveMenu(session_menu, 0, MF_BYPOSITION); + + index_key = 0; + index_menu = 0; + + sb = strbuf_new(); + while(ERROR_SUCCESS == RegEnumKey(hkey, index_key, buf, MAX_PATH)) { + if(strcmp(buf, PUTTY_DEFAULT) != 0) { + strbuf_clear(sb); + unescape_registry_key(buf, sb); + + memset(&mii, 0, sizeof(mii)); + mii.cbSize = sizeof(mii); + mii.fMask = MIIM_TYPE | MIIM_STATE | MIIM_ID; + mii.fType = MFT_STRING; + mii.fState = MFS_ENABLED; + mii.wID = (index_menu * 16) + IDM_SESSIONS_BASE; + mii.dwTypeData = sb->s; + InsertMenuItem(session_menu, index_menu, true, &mii); + index_menu++; + } + index_key++; + } + strbuf_free(sb); + + RegCloseKey(hkey); + + if(index_menu == 0) { + mii.cbSize = sizeof(mii); + mii.fMask = MIIM_TYPE | MIIM_STATE; + mii.fType = MFT_STRING; + mii.fState = MFS_GRAYED; + mii.dwTypeData = _T("(No sessions)"); + InsertMenuItem(session_menu, index_menu, true, &mii); + } +} + +/* + * Versions of Pageant prior to 0.61 expected this SID on incoming + * communications. For backwards compatibility, and more particularly + * for compatibility with derived works of PuTTY still using the old + * Pageant client code, we accept it as an alternative to the one + * returned from get_user_sid(). + */ +PSID get_default_sid(void) +{ + HANDLE proc = NULL; + DWORD sidlen; + PSECURITY_DESCRIPTOR psd = NULL; + PSID sid = NULL, copy = NULL, ret = NULL; + + if ((proc = OpenProcess(MAXIMUM_ALLOWED, false, + GetCurrentProcessId())) == NULL) + goto cleanup; + + if (p_GetSecurityInfo(proc, SE_KERNEL_OBJECT, OWNER_SECURITY_INFORMATION, + &sid, NULL, NULL, NULL, &psd) != ERROR_SUCCESS) + goto cleanup; + + sidlen = GetLengthSid(sid); + + copy = (PSID)smalloc(sidlen); + + if (!CopySid(sidlen, copy, sid)) + goto cleanup; + + /* Success. Move sid into the return value slot, and null it out + * to stop the cleanup code freeing it. */ + ret = copy; + copy = NULL; + + cleanup: + if (proc != NULL) + CloseHandle(proc); + if (psd != NULL) + LocalFree(psd); + if (copy != NULL) + sfree(copy); + + return ret; +} + +struct WmCopydataTransaction { + char *length, *body; + size_t bodysize, bodylen; + HANDLE ev_msg_ready, ev_reply_ready; +} wmct; + +static struct PageantClient wmcpc; + +static void wm_copydata_got_msg(void *vctx) +{ + pageant_handle_msg(&wmcpc, NULL, make_ptrlen(wmct.body, wmct.bodylen)); +} + +static void wm_copydata_got_response( + PageantClient *pc, PageantClientRequestId *reqid, ptrlen response) +{ + if (response.len > wmct.bodysize) { + /* Output would overflow message buffer. Replace with a + * failure message. */ + static const unsigned char failure[] = { SSH_AGENT_FAILURE }; + response = make_ptrlen(failure, lenof(failure)); + assert(response.len <= wmct.bodysize); + } + + PUT_32BIT_MSB_FIRST(wmct.length, response.len); + memcpy(wmct.body, response.ptr, response.len); + + SetEvent(wmct.ev_reply_ready); +} + +static bool ask_passphrase_common(PageantClientDialogId *dlgid, + const char *comment) +{ + /* Pageant core should be serialising requests, so we never expect + * a passphrase prompt to exist already at this point */ + assert(!nonmodal_passphrase_hwnd); + + struct PassphraseProcStruct *pps = snew(struct PassphraseProcStruct); + pps->modal = false; + pps->help_topic = WINHELP_CTX_pageant_deferred; + pps->dlgid = dlgid; + pps->passphrase = NULL; + pps->comment = comment; + + nonmodal_passphrase_hwnd = CreateDialogParam( + hinst, MAKEINTRESOURCE(IDD_ONDEMAND_PASSPHRASE), + NULL, PassphraseProc, (LPARAM)pps); + + /* + * Try to put this passphrase prompt into the foreground. + * + * This will probably not succeed in giving it the actual keyboard + * focus, because Windows is quite opposed to applications being + * able to suddenly steal the focus on their own initiative. + * + * That makes sense in a lot of situations, as a defensive + * measure. If you were about to type a password or other secret + * data into the window you already had focused, and some + * malicious app stole the focus, it might manage to trick you + * into typing your secrets into _it_ instead. + * + * In this case it's possible to regard the same defensive measure + * as counterproductive, because the effect if we _do_ steal focus + * is that you type something into our passphrase prompt that + * isn't the passphrase, and we fail to decrypt the key, and no + * harm is done. Whereas the effect of the user wrongly _assuming_ + * the new passphrase prompt has the focus is much worse: now you + * type your highly secret passphrase into some other window you + * didn't mean to trust with that information - such as the + * agent-forwarded PuTTY in which you just ran an ssh command, + * which the _whole point_ was to avoid telling your passphrase to! + * + * On the other hand, I'm sure _every_ application author can come + * up with an argument for why they think _they_ should be allowed + * to steal the focus. Probably most of them include the claim + * that no harm is done if their application receives data + * intended for something else, and of course that's not always + * true! + * + * In any case, I don't know of anything I can do about it, or + * anything I _should_ do about it if I could. If anyone thinks + * they can improve on all this, patches are welcome. + */ + SetForegroundWindow(nonmodal_passphrase_hwnd); + + return true; +} + +static bool wm_copydata_ask_passphrase( + PageantClient *pc, PageantClientDialogId *dlgid, const char *comment) +{ + return ask_passphrase_common(dlgid, comment); +} + +static const PageantClientVtable wmcpc_vtable = { + .log = NULL, /* no logging in this client */ + .got_response = wm_copydata_got_response, + .ask_passphrase = wm_copydata_ask_passphrase, +}; + +static char *answer_filemapping_message(const char *mapname) +{ + HANDLE maphandle = INVALID_HANDLE_VALUE; + void *mapaddr = NULL; + char *err = NULL; + size_t mapsize; + unsigned msglen; + + PSID mapsid = NULL; + PSID expectedsid = NULL; + PSID expectedsid_bc = NULL; + PSECURITY_DESCRIPTOR psd = NULL; + + wmct.length = wmct.body = NULL; + +#ifdef DEBUG_IPC + debug("mapname = \"%s\"\n", mapname); +#endif + + maphandle = OpenFileMapping(FILE_MAP_ALL_ACCESS, false, mapname); + if (maphandle == NULL || maphandle == INVALID_HANDLE_VALUE) { + err = dupprintf("OpenFileMapping(\"%s\"): %s", + mapname, win_strerror(GetLastError())); + goto cleanup; + } + +#ifdef DEBUG_IPC + debug("maphandle = %p\n", maphandle); +#endif + + if (should_have_security()) { + DWORD retd; + + if ((expectedsid = get_user_sid()) == NULL) { + err = dupstr("unable to get user SID"); + goto cleanup; + } + + if ((expectedsid_bc = get_default_sid()) == NULL) { + err = dupstr("unable to get default SID"); + goto cleanup; + } + + if ((retd = p_GetSecurityInfo( + maphandle, SE_KERNEL_OBJECT, OWNER_SECURITY_INFORMATION, + &mapsid, NULL, NULL, NULL, &psd) != ERROR_SUCCESS)) { + err = dupprintf("unable to get owner of file mapping: " + "GetSecurityInfo returned: %s", + win_strerror(retd)); + goto cleanup; + } + +#ifdef DEBUG_IPC + { + LPTSTR ours, ours2, theirs; + ConvertSidToStringSid(mapsid, &theirs); + ConvertSidToStringSid(expectedsid, &ours); + ConvertSidToStringSid(expectedsid_bc, &ours2); + debug("got sids:\n oursnew=%s\n oursold=%s\n" + " theirs=%s\n", ours, ours2, theirs); + LocalFree(ours); + LocalFree(ours2); + LocalFree(theirs); + } +#endif + + if (!EqualSid(mapsid, expectedsid) && + !EqualSid(mapsid, expectedsid_bc)) { + err = dupstr("wrong owning SID of file mapping"); + goto cleanup; + } + } else { +#ifdef DEBUG_IPC + debug("security APIs not present\n"); +#endif + } + + mapaddr = MapViewOfFile(maphandle, FILE_MAP_WRITE, 0, 0, 0); + if (!mapaddr) { + err = dupprintf("unable to obtain view of file mapping: %s", + win_strerror(GetLastError())); + goto cleanup; + } + +#ifdef DEBUG_IPC + debug("mapped address = %p\n", mapaddr); +#endif + + { + MEMORY_BASIC_INFORMATION mbi; + size_t mbiSize = VirtualQuery(mapaddr, &mbi, sizeof(mbi)); + if (mbiSize == 0) { + err = dupprintf("unable to query view of file mapping: %s", + win_strerror(GetLastError())); + goto cleanup; + } + if (mbiSize < (offsetof(MEMORY_BASIC_INFORMATION, RegionSize) + + sizeof(mbi.RegionSize))) { + err = dupstr("VirtualQuery returned too little data to get " + "region size"); + goto cleanup; + } + + mapsize = mbi.RegionSize; + } +#ifdef DEBUG_IPC + debug("region size = %"SIZEu"\n", mapsize); +#endif + if (mapsize < 5) { + err = dupstr("mapping smaller than smallest possible request"); + goto cleanup; + } + + wmct.length = (char *)mapaddr; + msglen = GET_32BIT_MSB_FIRST(wmct.length); + +#ifdef DEBUG_IPC + debug("msg length=%08x, msg type=%02x\n", + msglen, (unsigned)((unsigned char *) mapaddr)[4]); +#endif + + wmct.body = wmct.length + 4; + wmct.bodysize = mapsize - 4; + + if (msglen > wmct.bodysize) { + /* Incoming length field is too large. Emit a failure response + * without even trying to handle the request. + * + * (We know this must fit, because we checked mapsize >= 5 + * above.) */ + PUT_32BIT_MSB_FIRST(wmct.length, 1); + *wmct.body = SSH_AGENT_FAILURE; + } else { + wmct.bodylen = msglen; + SetEvent(wmct.ev_msg_ready); + WaitForSingleObject(wmct.ev_reply_ready, INFINITE); + } + + cleanup: + /* expectedsid has the lifetime of the program, so we don't free it */ + sfree(expectedsid_bc); + if (psd) + LocalFree(psd); + if (mapaddr) + UnmapViewOfFile(mapaddr); + if (maphandle != NULL && maphandle != INVALID_HANDLE_VALUE) + CloseHandle(maphandle); + return err; +} + +static void create_keylist_window(void) +{ + if (keylist) + return; + + keylist = CreateDialog(hinst, MAKEINTRESOURCE(IDD_KEYLIST), + NULL, KeyListProc); + ShowWindow(keylist, SW_SHOWNORMAL); +} + +static LRESULT CALLBACK TrayWndProc(HWND hwnd, UINT message, + WPARAM wParam, LPARAM lParam) +{ + static bool menuinprogress; + static UINT msgTaskbarCreated = 0; + + switch (message) { + case WM_CREATE: + msgTaskbarCreated = RegisterWindowMessage(_T("TaskbarCreated")); + break; + default: + if (message==msgTaskbarCreated) { + /* + * Explorer has been restarted, so the tray icon will + * have been lost. + */ + AddTrayIcon(hwnd); + } + break; + + case WM_SYSTRAY: + if (lParam == WM_RBUTTONUP) { + POINT cursorpos; + GetCursorPos(&cursorpos); + PostMessage(hwnd, WM_SYSTRAY2, cursorpos.x, cursorpos.y); + } else if (lParam == WM_LBUTTONDBLCLK) { + /* Run the default menu item. */ + UINT menuitem = GetMenuDefaultItem(systray_menu, false, 0); + if (menuitem != -1) + PostMessage(hwnd, WM_COMMAND, menuitem, 0); + } + break; + case WM_SYSTRAY2: + if (!menuinprogress) { + menuinprogress = true; + update_sessions(); + SetForegroundWindow(hwnd); + TrackPopupMenu(systray_menu, + TPM_RIGHTALIGN | TPM_BOTTOMALIGN | + TPM_RIGHTBUTTON, + wParam, lParam, 0, hwnd, NULL); + menuinprogress = false; + } + break; + case WM_COMMAND: + case WM_SYSCOMMAND: { + unsigned command = wParam & ~0xF; /* low 4 bits reserved to Windows */ + switch (command) { + case IDM_PUTTY: { + TCHAR cmdline[10]; + cmdline[0] = '\0'; + if (restrict_putty_acl) + strcat(cmdline, "&R"); + + if((INT_PTR)ShellExecute(hwnd, NULL, putty_path, cmdline, + _T(""), SW_SHOW) <= 32) { + MessageBox(NULL, "Unable to execute PuTTY!", + "Error", MB_OK | MB_ICONERROR); + } + break; + } + case IDM_CLOSE: + if (modal_passphrase_hwnd) + SendMessage(modal_passphrase_hwnd, WM_CLOSE, 0, 0); + SendMessage(hwnd, WM_CLOSE, 0, 0); + break; + case IDM_VIEWKEYS: + create_keylist_window(); + /* + * Sometimes the window comes up minimised / hidden for + * no obvious reason. Prevent this. This also brings it + * to the front if it's already present (the user + * selected View Keys because they wanted to _see_ the + * thing). + */ + SetForegroundWindow(keylist); + SetWindowPos(keylist, HWND_TOP, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW); + break; + case IDM_ADDKEY: + case IDM_ADDKEY_ENCRYPTED: + if (modal_passphrase_hwnd) { + MessageBeep(MB_ICONERROR); + SetForegroundWindow(modal_passphrase_hwnd); + break; + } + prompt_add_keyfile(command == IDM_ADDKEY_ENCRYPTED); + break; + case IDM_REMOVE_ALL: + pageant_delete_all(); + keylist_update(); + break; + case IDM_REENCRYPT_ALL: + pageant_reencrypt_all(); + keylist_update(); + break; + case IDM_ABOUT: + if (!aboutbox) { + aboutbox = CreateDialog(hinst, MAKEINTRESOURCE(IDD_ABOUT), + NULL, AboutProc); + ShowWindow(aboutbox, SW_SHOWNORMAL); + /* + * Sometimes the window comes up minimised / hidden + * for no obvious reason. Prevent this. + */ + SetForegroundWindow(aboutbox); + SetWindowPos(aboutbox, HWND_TOP, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW); + } + break; + case IDM_HELP: + launch_help(hwnd, WINHELP_CTX_pageant_general); + break; + default: { + if(wParam >= IDM_SESSIONS_BASE && wParam <= IDM_SESSIONS_MAX) { + MENUITEMINFO mii; + TCHAR buf[MAX_PATH + 1]; + TCHAR param[MAX_PATH + 1]; + memset(&mii, 0, sizeof(mii)); + mii.cbSize = sizeof(mii); + mii.fMask = MIIM_TYPE; + mii.cch = MAX_PATH; + mii.dwTypeData = buf; + GetMenuItemInfo(session_menu, wParam, false, &mii); + param[0] = '\0'; + if (restrict_putty_acl) + strcat(param, "&R"); + strcat(param, "@"); + strcat(param, mii.dwTypeData); + if((INT_PTR)ShellExecute(hwnd, NULL, putty_path, param, + _T(""), SW_SHOW) <= 32) { + MessageBox(NULL, "Unable to execute PuTTY!", "Error", + MB_OK | MB_ICONERROR); + } + } + break; + } + } + break; + } + case WM_NETEVENT: + winselgui_response(wParam, lParam); + return 0; + case WM_DESTROY: + quit_help(hwnd); + PostQuitMessage(0); + return 0; + } + + return DefWindowProc(hwnd, message, wParam, lParam); +} + +static LRESULT CALLBACK wm_copydata_WndProc(HWND hwnd, UINT message, + WPARAM wParam, LPARAM lParam) +{ + switch (message) { + case WM_COPYDATA: { + COPYDATASTRUCT *cds; + char *mapname, *err; + + cds = (COPYDATASTRUCT *) lParam; + if (cds->dwData != AGENT_COPYDATA_ID) + return 0; /* not our message, mate */ + mapname = (char *) cds->lpData; + if (mapname[cds->cbData - 1] != '\0') + return 0; /* failure to be ASCIZ! */ + err = answer_filemapping_message(mapname); + if (err) { +#ifdef DEBUG_IPC + debug("IPC failed: %s\n", err); +#endif + sfree(err); + return 0; + } + return 1; + } + } + + return DefWindowProc(hwnd, message, wParam, lParam); +} + +static DWORD WINAPI wm_copydata_threadfunc(void *param) +{ + HINSTANCE inst = *(HINSTANCE *)param; + + HWND ipchwnd = CreateWindow(IPCCLASSNAME, IPCWINTITLE, + WS_OVERLAPPEDWINDOW | WS_VSCROLL, + CW_USEDEFAULT, CW_USEDEFAULT, + 100, 100, NULL, NULL, inst, NULL); + ShowWindow(ipchwnd, SW_HIDE); + + MSG msg; + while (GetMessage(&msg, NULL, 0, 0) == 1) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + return 0; +} + +/* + * Fork and Exec the command in cmdline. [DBW] + */ +void spawn_cmd(const char *cmdline, const char *args, int show) +{ + if (ShellExecute(NULL, _T("open"), cmdline, + args, NULL, show) <= (HINSTANCE) 32) { + char *msg; + msg = dupprintf("Failed to run \"%s\": %s", cmdline, + win_strerror(GetLastError())); + MessageBox(NULL, msg, APPNAME, MB_OK | MB_ICONEXCLAMATION); + sfree(msg); + } +} + +void noise_ultralight(NoiseSourceId id, unsigned long data) +{ + /* Pageant doesn't use random numbers, so we ignore this */ +} + +void cleanup_exit(int code) +{ + shutdown_help(); + exit(code); +} + +static bool winpgnt_listener_ask_passphrase( + PageantListenerClient *plc, PageantClientDialogId *dlgid, + const char *comment) +{ + return ask_passphrase_common(dlgid, comment); +} + +struct winpgnt_client { + PageantListenerClient plc; +}; +static const PageantListenerClientVtable winpgnt_vtable = { + .log = NULL, /* no logging */ + .ask_passphrase = winpgnt_listener_ask_passphrase, +}; + +static struct winpgnt_client wpc[1]; + +HINSTANCE hinst; + +static NORETURN void opt_error(const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + char *msg = dupvprintf(fmt, ap); + va_end(ap); + + MessageBox(NULL, msg, "Pageant command line error", MB_ICONERROR | MB_OK); + + exit(1); +} + +#ifdef LEGACY_WINDOWS +BOOL sw_PeekMessage(LPMSG msg, HWND hwnd, UINT min, UINT max, UINT remove) +{ + static bool unicode_unavailable = false; + if (!unicode_unavailable) { + BOOL ret = PeekMessageW(msg, hwnd, min, max, remove); + if (!ret && GetLastError() == ERROR_CALL_NOT_IMPLEMENTED) + unicode_unavailable = true; /* don't try again */ + else + return ret; + } + return PeekMessageA(msg, hwnd, min, max, remove); +} +#else +#define sw_PeekMessage PeekMessageW +#endif + +int WINAPI WinMain(HINSTANCE inst, HINSTANCE prev, LPSTR cmdline, int show) +{ + MSG msg; + const char *command = NULL; + const char *unixsocket = NULL; + bool show_keylist_on_startup = false; + int argc; + char **argv, **argstart; + const char *openssh_config_file = NULL; + + typedef struct CommandLineKey { + Filename *fn; + bool add_encrypted; + } CommandLineKey; + + CommandLineKey *clkeys = NULL; + size_t nclkeys = 0, clkeysize = 0; + + dll_hijacking_protection(); + + hinst = inst; + + if (should_have_security()) { + /* + * Attempt to get the security API we need. + */ + if (!got_advapi()) { + MessageBox(NULL, + "Unable to access security APIs. Pageant will\n" + "not run, in case it causes a security breach.", + "Pageant Fatal Error", MB_ICONERROR | MB_OK); + return 1; + } + } + + /* + * See if we can find our Help file. + */ + init_help(); + + /* + * Look for the PuTTY binary (we will enable the saved session + * submenu if we find it). + */ + { + char b[2048], *p, *q, *r; + FILE *fp; + GetModuleFileName(NULL, b, sizeof(b) - 16); + r = b; + p = strrchr(b, '\\'); + if (p && p >= r) r = p+1; + q = strrchr(b, ':'); + if (q && q >= r) r = q+1; + strcpy(r, "putty.exe"); + if ( (fp = fopen(b, "r")) != NULL) { + putty_path = dupstr(b); + fclose(fp); + } else + putty_path = NULL; + } + + /* + * Process the command line, handling anything that can be done + * immediately, but deferring adding keys until after we've + * started up the main agent. Details of keys to be added are + * stored in the 'clkeys' array. + */ + split_into_argv(cmdline, &argc, &argv, &argstart); + bool add_keys_encrypted = false; + AuxMatchOpt amo = aux_match_opt_init(argc, argv, 0, opt_error); + while (!aux_match_done(&amo)) { + char *val; + #define match_opt(...) aux_match_opt( \ + &amo, NULL, __VA_ARGS__, (const char *)NULL) + #define match_optval(...) aux_match_opt( \ + &amo, &val, __VA_ARGS__, (const char *)NULL) + + if (aux_match_arg(&amo, &val)) { + /* + * Non-option arguments are expected to be key files, and + * added to clkeys. + */ + sgrowarray(clkeys, clkeysize, nclkeys); + CommandLineKey *clkey = &clkeys[nclkeys++]; + clkey->fn = filename_from_str(val); + clkey->add_encrypted = add_keys_encrypted; + } else if (match_opt("-pgpfp")) { + pgp_fingerprints_msgbox(NULL); + return 1; + } else if (match_opt("-restrict-acl", "-restrict_acl", + "-restrictacl")) { + restrict_process_acl(); + } else if (match_opt("-restrict-putty-acl", "-restrict_putty_acl")) { + restrict_putty_acl = true; + } else if (match_opt("-no-decrypt", "-no_decrypt", + "-nodecrypt", "-encrypted")) { + add_keys_encrypted = true; + } else if (match_opt("-keylist")) { + show_keylist_on_startup = true; + } else if (match_optval("-openssh-config", "-openssh_config")) { + openssh_config_file = val; + } else if (match_optval("-unix")) { + unixsocket = val; + } else if (match_opt("-c")) { + /* + * If we see `-c', then the rest of the command line + * should be treated as a command to be spawned. + */ + if (amo.index < amo.argc) + command = argstart[amo.index]; + else + command = ""; + break; + } else { + opt_error("unrecognised option '%s'\n", amo.argv[amo.index]); + } + } + + /* + * Create and lock an interprocess mutex while we figure out + * whether we're going to be the Pageant server or a client. That + * way, two Pageant processes started up simultaneously will be + * able to agree on which one becomes the server without a race + * condition. + */ + HANDLE mutex; + { + char *err; + char *mutexname = agent_mutex_name(); + mutex = lock_interprocess_mutex(mutexname, &err); + sfree(mutexname); + if (!mutex) { + MessageBox(NULL, err, "Pageant Error", MB_ICONERROR | MB_OK); + return 1; + } + } + + /* + * Find out if Pageant is already running. + */ + already_running = agent_exists(); + + /* + * If it isn't, we're going to be the primary Pageant that stays + * running, so set up all the machinery to answer requests. + */ + if (!already_running) { + /* + * Set up the window class for the hidden window that receives + * all the messages to do with our presence in the system tray. + */ + + if (!prev) { + WNDCLASS wndclass; + + memset(&wndclass, 0, sizeof(wndclass)); + wndclass.lpfnWndProc = TrayWndProc; + wndclass.hInstance = inst; + wndclass.hIcon = LoadIcon(inst, MAKEINTRESOURCE(IDI_MAINICON)); + wndclass.lpszClassName = TRAYCLASSNAME; + + RegisterClass(&wndclass); + } + + keylist = NULL; + + traywindow = CreateWindow(TRAYCLASSNAME, TRAYWINTITLE, + WS_OVERLAPPEDWINDOW | WS_VSCROLL, + CW_USEDEFAULT, CW_USEDEFAULT, + 100, 100, NULL, NULL, inst, NULL); + winselgui_set_hwnd(traywindow); + + /* + * Initialise the cross-platform Pageant code. + */ + pageant_init(); + + /* + * Set up a named-pipe listener. + */ + wpc->plc.vt = &winpgnt_vtable; + wpc->plc.suppress_logging = true; + if (should_have_security()) { + Plug *pl_plug; + struct pageant_listen_state *pl = + pageant_listener_new(&pl_plug, &wpc->plc); + char *pipename = agent_named_pipe_name(); + Socket *sock = new_named_pipe_listener(pipename, pl_plug); + if (sk_socket_error(sock)) { + char *err = dupprintf("Unable to open named pipe at %s " + "for SSH agent:\n%s", pipename, + sk_socket_error(sock)); + MessageBox(NULL, err, "Pageant Error", MB_ICONERROR | MB_OK); + return 1; + } + pageant_listener_got_socket(pl, sock); + + /* + * If we've been asked to write out an OpenSSH config file + * pointing at the named pipe, do so. + */ + if (openssh_config_file) { + FILE *fp = fopen(openssh_config_file, "w"); + if (!fp) { + char *err = dupprintf("Unable to write OpenSSH config " + "file to %s", openssh_config_file); + MessageBox(NULL, err, "Pageant Error", + MB_ICONERROR | MB_OK); + return 1; + } + fprintf(fp, "IdentityAgent %s\n", pipename); + fclose(fp); + } + + sfree(pipename); + } + + /* + * Set up an AF_UNIX listener too, if we were asked to. + */ + if (unixsocket) { + sk_init(); + + /* FIXME: diagnose any error except file-not-found. Also, + * check the file type if possible? */ + remove(unixsocket); + + Plug *pl_plug; + struct pageant_listen_state *pl = + pageant_listener_new(&pl_plug, &wpc->plc); + Socket *sock = sk_newlistener_unix(unixsocket, pl_plug); + if (sk_socket_error(sock)) { + char *err = dupprintf("Unable to open AF_UNIX socket at %s " + "for SSH agent:\n%s", unixsocket, + sk_socket_error(sock)); + MessageBox(NULL, err, "Pageant Error", MB_ICONERROR | MB_OK); + return 1; + } + pageant_listener_got_socket(pl, sock); + } + + /* + * Set up the window class for the hidden window that receives + * the WM_COPYDATA message used by the old-style Pageant IPC + * system. + */ + if (!prev) { + WNDCLASS wndclass; + + memset(&wndclass, 0, sizeof(wndclass)); + wndclass.lpfnWndProc = wm_copydata_WndProc; + wndclass.hInstance = inst; + wndclass.lpszClassName = IPCCLASSNAME; + + RegisterClass(&wndclass); + } + + /* + * And launch the subthread which will open that hidden window and + * handle WM_COPYDATA messages on it. + */ + wmcpc.vt = &wmcpc_vtable; + wmcpc.suppress_logging = true; + pageant_register_client(&wmcpc); + DWORD wm_copydata_threadid; + wmct.ev_msg_ready = CreateEvent(NULL, false, false, NULL); + wmct.ev_reply_ready = CreateEvent(NULL, false, false, NULL); + HANDLE hThread = CreateThread(NULL, 0, wm_copydata_threadfunc, + &inst, 0, &wm_copydata_threadid); + if (hThread) + CloseHandle(hThread); /* we don't need the thread handle */ + add_handle_wait(wmct.ev_msg_ready, wm_copydata_got_msg, NULL); + } + + /* + * Now we're either a fully set up Pageant server, or we know one + * is running somewhere else. Either way, now it's safe to unlock + * the mutex. + */ + unlock_interprocess_mutex(mutex); + + /* + * Add any keys provided on the command line. + */ + for (size_t i = 0; i < nclkeys; i++) { + CommandLineKey *clkey = &clkeys[i]; + win_add_keyfile(clkey->fn, clkey->add_encrypted); + filename_free(clkey->fn); + } + sfree(clkeys); + /* And forget any passphrases we stashed during that loop. */ + pageant_forget_passphrases(); + + /* + * Now our keys are present, spawn a command, if we were asked to. + */ + if (command) { + char *args; + if (command[0] == '"') + args = strchr(++command, '"'); + else + args = strchr(command, ' '); + if (args) { + *args++ = 0; + while(*args && isspace(*args)) args++; + } + spawn_cmd(command, args, show); + } + + /* + * If Pageant was already running, we leave now. If we haven't + * even taken any auxiliary action (spawned a command or added + * keys), complain. + */ + if (already_running) { + if (!command && !nclkeys) { + MessageBox(NULL, "Pageant is already running", "Pageant Error", + MB_ICONERROR | MB_OK); + } + return 0; + } + + /* Set up a system tray icon */ + AddTrayIcon(traywindow); + + /* Accelerators used: nsvkxa */ + systray_menu = CreatePopupMenu(); + if (putty_path) { + session_menu = CreateMenu(); + AppendMenu(systray_menu, MF_ENABLED, IDM_PUTTY, "&New Session"); + AppendMenu(systray_menu, MF_POPUP | MF_ENABLED, + (UINT_PTR) session_menu, "&Saved Sessions"); + AppendMenu(systray_menu, MF_SEPARATOR, 0, 0); + } + AppendMenu(systray_menu, MF_ENABLED, IDM_VIEWKEYS, + "&View Keys"); + AppendMenu(systray_menu, MF_ENABLED, IDM_ADDKEY, "Add &Key"); + AppendMenu(systray_menu, MF_ENABLED, IDM_ADDKEY_ENCRYPTED, + "Add key (encrypted)"); + AppendMenu(systray_menu, MF_SEPARATOR, 0, 0); + AppendMenu(systray_menu, MF_ENABLED, IDM_REMOVE_ALL, + "Remove All Keys"); + AppendMenu(systray_menu, MF_ENABLED, IDM_REENCRYPT_ALL, + "Re-encrypt All Keys"); + AppendMenu(systray_menu, MF_SEPARATOR, 0, 0); + if (has_help()) + AppendMenu(systray_menu, MF_ENABLED, IDM_HELP, "&Help"); + AppendMenu(systray_menu, MF_ENABLED, IDM_ABOUT, "&About"); + AppendMenu(systray_menu, MF_SEPARATOR, 0, 0); + AppendMenu(systray_menu, MF_ENABLED, IDM_CLOSE, "E&xit"); + initial_menuitems_count = GetMenuItemCount(session_menu); + + /* Set the default menu item. */ + SetMenuDefaultItem(systray_menu, IDM_VIEWKEYS, false); + + ShowWindow(traywindow, SW_HIDE); + + /* Open the visible key list window, if we've been asked to. */ + if (show_keylist_on_startup) + create_keylist_window(); + + /* + * Main message loop. + */ + while (true) { + int n; + + HandleWaitList *hwl = get_handle_wait_list(); + + DWORD timeout = toplevel_callback_pending() ? 0 : INFINITE; + n = MsgWaitForMultipleObjects(hwl->nhandles, hwl->handles, false, + timeout, QS_ALLINPUT); + + if ((unsigned)(n - WAIT_OBJECT_0) < (unsigned)hwl->nhandles) + handle_wait_activate(hwl, n - WAIT_OBJECT_0); + handle_wait_list_free(hwl); + + while (sw_PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + if (msg.message == WM_QUIT) + goto finished; /* two-level break */ + + if (IsWindow(keylist) && IsDialogMessage(keylist, &msg)) + continue; + if (IsWindow(aboutbox) && IsDialogMessage(aboutbox, &msg)) + continue; + if (IsWindow(nonmodal_passphrase_hwnd) && + IsDialogMessage(nonmodal_passphrase_hwnd, &msg)) + continue; + + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + run_toplevel_callbacks(); + } + finished: + + /* Clean up the system tray icon */ + { + NOTIFYICONDATA tnid; + + tnid.cbSize = sizeof(NOTIFYICONDATA); + tnid.hWnd = traywindow; + tnid.uID = 1; + + Shell_NotifyIcon(NIM_DELETE, &tnid); + + DestroyMenu(systray_menu); + } + + if (keypath) filereq_free(keypath); + + if (openssh_config_file) { + /* + * Leave this file around, but empty it, so that it doesn't + * refer to a pipe we aren't listening on any more. + */ + FILE *fp = fopen(openssh_config_file, "w"); + if (fp) + fclose(fp); + } + + cleanup_exit(msg.wParam); + return msg.wParam; /* just in case optimiser complains */ +} |