Mac OS下创建支持自动登录的.rdp文件及password 51:b字段生成方案
解决Mac上生成RDP文件密码字段及rdp://协议传密码的问题
一、生成RDP文件的password 51:b字段
Windows的CryptProtectData是系统特有的DPAPI加密,但RDP的密码字段遵循一套公开的加密规则,我们可以在Mac上手动实现这套规则来生成正确的51:b字段。
核心加密流程
RDP密码加密基于NTLM哈希、MD5和RC4,步骤如下:
- 将用户密码转换为UTF-16LE编码,计算其MD4哈希(即NTLM哈希)。
- 根据目标主机的名称/IP生成16字节的基础数据,计算其MD5哈希。
- 用步骤2的MD5哈希作为RC4密钥,加密步骤1的NTLM哈希,得到最终的RC4加密密钥。
- 用这个RC4密钥加密密码的UTF-16LE字节序列,将结果Base64编码后加上
51:b:前缀,就是RDP文件中的密码字段值。
Swift代码实现示例
下面是完整的Swift实现,包含IP和主机名的处理:
import CommonCrypto // 生成NTLM哈希(密码的MD4哈希,基于UTF-16LE) func ntlmHash(from password: String) -> Data? { guard let utf16leData = password.data(using: .utf16LittleEndian) else { return nil } var hash = [UInt8](repeating: 0, count: Int(CC_MD4_DIGEST_LENGTH)) utf16leData.withUnsafeBytes { bytes in CC_MD4(bytes.baseAddress, CC_LONG(utf16leData.count), &hash) } return Data(hash) } // 生成服务器标识的MD5哈希(支持主机名和IP) func serverMD5(from serverIdentifier: String) -> Data? { var serverData: Data // 判断是否为IP地址 let ipPattern = "^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$" if serverIdentifier.range(of: ipPattern, options: .regularExpression) != nil { // 解析IP为字节数组 let ipParts = serverIdentifier.split(separator: ".").compactMap { UInt8($0) } guard ipParts.count == 4 else { return nil } serverData = Data(ipParts) // 补0到16字节 while serverData.count < 16 { serverData.append(0) } } else { // 处理主机名:大写、截断为15字符、补0到16字节 let upperName = serverIdentifier.uppercased() let truncatedName = upperName.prefix(15) serverData = Data(truncatedName.utf8) while serverData.count < 16 { serverData.append(0) } } // 计算MD5哈希 var hash = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH)) serverData.withUnsafeBytes { bytes in CC_MD5(bytes.baseAddress, CC_LONG(serverData.count), &hash) } return Data(hash) } // RC4加密实现 struct RC4 { private var sBox: [UInt8] = Array(0...255) private var i: UInt8 = 0 private var j: UInt8 = 0 init(key: Data) { var j: UInt8 = 0 for i in 0...255 { j &+= sBox[i] &+ key[Int(i % UInt8(key.count))] sBox.swapAt(i, Int(j)) } } mutating func encrypt(data: Data) -> Data { var result = Data() for byte in data { i &+= 1 j &+= sBox[Int(i)] sBox.swapAt(Int(i), Int(j)) let keyByte = sBox[Int(sBox[Int(i)] &+ sBox[Int(j)])] result.append(byte ^ keyByte) } return result } } // 生成最终的RDP密码字段 func generateRdpPasswordField(password: String, serverIdentifier: String) -> String? { guard let ntHash = ntlmHash(from: password), let serverMd5 = serverMD5(from: serverIdentifier), ntHash.count == 16, serverMd5.count == 16 else { return nil } // 生成RC4加密密钥 var keyGenerator = RC4(key: serverMd5) let rc4Key = keyGenerator.encrypt(data: ntHash) // 加密密码的UTF-16LE数据 guard let passwordUtf16le = password.data(using: .utf16LittleEndian) else { return nil } var encryptor = RC4(key: rc4Key) let encryptedPassword = encryptor.encrypt(data: passwordUtf16le) // 生成最终字段 return "51:b:\(encryptedPassword.base64EncodedString())" }
使用时,调用generateRdpPasswordField(password: "yourPassword", serverIdentifier: "192.168.1.100")即可得到正确的密码字段值。
二、解决rdp://协议无法传递密码的问题
确实,macOS上的rdp://协议没有标准参数支持传递密码(出于安全和客户端实现限制),有两种替代方案:
方案1:生成.rdp文件并打开
将生成的密码字段写入.rdp文件,然后用命令行打开:
# 假设你已经生成了包含正确密码字段的myconnection.rdp open myconnection.rdp
Microsoft Remote Desktop客户端会自动读取文件中的密码并填充。
方案2:用AppleScript控制客户端直接连接
通过AppleScript可以直接调用Microsoft Remote Desktop客户端,传递用户名、密码和主机信息并发起连接,无需生成文件:
tell application "Microsoft Remote Desktop" set newConn to make new desktop connection with properties {¬ host:"192.168.1.100", ¬ username:"yourUsername", ¬ password:"yourPassword", ¬ name:"Auto-Connect"} connect newConn end tell
你也可以通过命令行执行这段脚本:
osascript -e 'tell application "Microsoft Remote Desktop" to connect (make new desktop connection with properties {host:"192.168.1.100", username:"yourUsername", password:"yourPassword"})'
注意:如果你的工具需要处理敏感密码,建议避免明文传递,不过AppleScript接口目前只接受明文密码,客户端会自行处理加密存储。
内容的提问来源于stack exchange,提问作者Stanislav




