如何在Windows控制台上读取UTF-8密码

2020-05-04 21:26:04

假设您正在编写一个命令行程序,提示用户输入密码或口令短语,而Windows是其中一个受支持的平台(即使是非常旧的版本)。该程序使用UTF-8作为其字符串表示,这是理所当然的,因此理想情况下,它会从用户那里接收编码为UTF-8的密码。在大多数平台上,这在很大程度上是自动的。然而,在Windows上寻找这个问题的正确答案是一个迷宫,所有的标志都通向死胡同。我最近穿过这个迷宫,找到了出口。

我知道这是可能的,因为我的passphrase2pgp工具使用了golang.org/x/crypto/ssh/Terminal包,这使它近乎完美。尽管他们6个月前还在修复细微的错误。

第一步是忽略你在网上找到的所有东西,因为它要么是错的,要么是在解决一个略有不同的问题。稍后我将讨论死胡同,并首先关注解决方案。最终,我希望在Windows上实现此功能:

//显示提示,然后读取以零结尾的UTF-8密码。//带终止符返回密码长度,错误时返回零。int read_password(char*buf,int len,const char*Prompt);

我选择int作为长度,而不是size_t,因为它是密码,甚至不应该接近int_max。

这种方法的一个很大的优点是它不依赖于标准输入和标准输出。其中一个或两个都可以重定向到其他地方,并且此功能仍然与用户的终端交互。Windows版本也将拥有同样的优势。

尽管有一些诱人的快捷方式不起作用,但Windows上的步骤基本上是相同的,只是名称不同。有两个实体和额外的步骤。我将忽略下面代码片段中的错误,但是完整的示例有完整的错误处理。

该程序使用CreateFileA()打开两个文件,而不是/dev/tty:conin$和CONOUT$。注:“A”代表ANSI,而“W”代表宽(Unicode)。这指的是文件名的编码,而不是文件内容的编码方式。由于需要写入权限才能更改控制台的模式,因此打开了conin$以供读取和写入。

Handle hi=CreateFileA(";Conin$";,GENERIC_READ|GENERIC_WRITE,0,0,OPEN_EXISTING,0,0);HANDLE ho=CreateFileA(";CONOUT$";,GENERIC_WRITE,0,0,OPEN_EXISTING,0,0);

要编写提示符,请在输出句柄上调用WriteConsoleA()。就其本身而言,它假定提示符是纯ASCII(即";密码:";),而不是UTF-8(即";对比度:";):

如果提示符可能包含UTF-8数据(可能是因为它显示的是username或不是英语),则您有两个选择:

将SetConsoleOutputCP()与CP_utf8(65001)一起使用。这是全局(到控制台)设置,完成后应恢复。

接下来,使用GetConsoleMode()和SetConsoleMode()禁用回显。控制台通常已经设置了ENABLE_PROCESSED_INPUT,它告诉控制台处理CTRL-C等,但我显式设置了它以防万一。我还设置了ENABLINE_LINE_INPUT,这样用户就可以使用退格键,这样就可以一次发送整行内容。

有报告称ENABLE_LINE_INPUT将读取限制为254字节,但我无法重现它。我的完整示例可以毫不费力地阅读大量密码。

这是你必须付钱给风笛手的地方。截至本文日期,Windows API还没有提供从控制台读取UTF-8输入的方法。现在就放弃这个希望吧。如果您在任何配置下使用“ANSI”函数来读取输入,它们将与通常的Windows一样默默地破坏您的输入。

因此,您必须使用UTF-16API ReadConsoleW(),然后自己编码。幸运的是,Win32提供了一个UTF-8编码器WideCharToMultiByte(),它甚至可以为所有喜欢在密码中放入一堆便便(U+1F4A9)的人处理代理项对:

WCHAR*wbuf=malloc((len-1+2)*sizeof(*wbuf));DWORD nREAD;ReadConsoleW(hi,wbuf,len-1+2,&;nREAD,0);wbuf[nREAD-2]=0;//截断";\r\n";int r=WideCharToMultiByte(CP_UTF8,0,wbuf。

分配中的+2用于稍后将被截断的CRLF线路末尾。错误处理版本检查输入是否确实以CRLF结尾。否则它会被截断(太长)。

最后,由于没有回显用户键入的换行符,因此打印一个换行符,恢复旧的控制台模式,关闭控制台句柄,并返回最终编码的长度:

错误检查版本不检查来自任何这些函数的错误,因为它们要么不会失败,要么在出现错误时没有任何合理的处理方法。

如果您环顾一下Win32API,您可能会注意到SetConsoleCP()。合理的人可能认为将“代码页”设置为UTF-8(CP_UTF8)可能会将控制台配置为以UTF-8编码输入。好消息是Windows将不再像以前那样破坏你的输入。坏消息是,它将受到不同的破坏。

您可能认为可以在连接到控制台的文件*上使用CRT函数_setmode()和_O_U8TEXT。这没有什么用处。(_setmode()的唯一用法是WITH_O_BINARY,用于在标准输入和输出上禁用braindead字符转换。)。对于CRT,您能做的最好的事情就是使用非标准函数读取相同类型的宽字符,然后转换为UTF-8。

CredUICmdLinePromptForCredentials()承诺既有足够的函数名,又有这个问题的预先打包的解决方案,它只在第一个函数上提供。这项功能似乎在一段时间前就失效了,微软没有人注意到这一点--可能是因为从来没有人使用过这项功能。我找不到可用的示例,也找不到在任何实际应用程序中使用的示例。当我试图使用它的时候,我得到了一个无稽之谈的错误代码,它从来没有起作用。这个函数有一个可以工作的GUI版本,对于某些情况,它是一个可行的替代方案,尽管我的情况不是这样。

在我最绝望的时候,我希望启用虚拟终端处理会是一个神奇的开关。在Windows10上,它神奇地启用了一些ANSI转义序列。文档没有以任何方式暗示它会起作用,我通过实验证实它不会起作用。可怜。

我花了很多时间寻找这些死胡同,直到最终使用上面的ReadConsoleW()解决了问题。我希望它能更自动化,但我很高兴我至少想出了一些解决方案。

对这篇文章有什么评论吗?通过发送电子邮件至~Skeeto/[email protected][邮件列表礼仪]开始我的公共收件箱中的讨论,或查看现有讨论。