【问题标题】:Check if SSH Private Key is Encrypted检查 SSH 私钥是否已加密
【发布时间】:2019-09-10 03:35:49
【问题描述】:

在 macOS 上使用ssh-keygen 生成的密钥对可以有不同的格式。

  • macOS 的SecKey API 可读的标准 PEM ASN.1 对象
  • 带有文本标题的 PEM
  • OpenSSH 密钥
  • OpenSSH 加密密钥

OpenSSH/BSD 使用这种非标准化格式here

现在我只需要能够检查一个私钥是否设置了密码,这样我就可以提示用户输入它,而不必处理不同密钥格式的复杂性。

在 macOS 上是否有一种通过 Swift 或 C API 快速检查密钥是否设置密码的方法?

【问题讨论】:

标签: c swift macos ssh


【解决方案1】:

未加密和加密私钥之间的区别在于密钥 blob 已加密。您需要先解密私钥 blob 数据,然后才能使用私钥 blob。所以加密后的私钥数据一旦被解码,就可以和未加密的私钥数据一样对待。

未加密的私钥 blob PEM 文件如下所示:

—–BEGIN PRIVATE KEY—–
{base64 private key blob)
—–END PRIVATE KEY—–

加密的 RSA 私钥 PEM 文件如下所示:

—–BEGIN RSA PRIVATE KEY—–
Proc-Type: 4,ENCRYPTED
DEK-Info: {encryption algorithm},{salt}
{base64 encrypted private key blob)
—–END RSA PRIVATE KEY—–

例如

—–BEGIN RSA PRIVATE KEY—–
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-256-CBC,AB8E2B5B2D989271273F6730B6F9C687
{base64 encrypted private key blob)
—–END RSA PRIVATE KEY—–

所以要解码你需要的私钥数据:

  1. 解析DEK-Info加密算法和盐(确认第一行的好主意是:“Proc-Type: 4,ENCRYPTED”)。
  2. 解码 base64 加密的私钥 blob。
  3. 根据盐和密码生成加密算法“密钥”和“IV”
  4. 解码加密的私钥 blob。

完成后,解密的私钥 blob 可以像未编码的私钥 blob 一样处理。

支持的加密算法数量相当多,因此您可能希望支持一组算法。例如“DES-EDE3-CBC”、“AES-xxx-CBC”等

要生成 IV,您需要将盐字符串转换为二进制。盐字符串是一个十六进制编码的字符串,因此使用十六进制字符串字符到字节转换器将每两个字符串字符转换为一个字节。

为了生成加密算法密钥,您需要密钥大小(例如,DES-EDE3-CBC 是 192 位,AES-256-CBC 是 256 位)。通过将 MD5 哈希结果附加到密钥的循环来构建密钥“位”,直到生成所需的所有密钥位。

MD5 HASH 循环生成将包括:

  1. 第一个 MD5 哈希:IV 和密码的前 8 个字节的 MD5 哈希
  2. 所有其他 MD5 哈希是最后一个 MD5 哈希结果和 IV 和密码短语的前 8 个字节的 MD5 哈希

有关密钥位生成的示例,请参见 EVP_BytesToKey 方法的 openssl 源代码。

现在可以使用上面的 IV 和 KEY 构建使用选定的加密算法对加密的私钥 blob 进行解码。

【讨论】:

  • 那不是真的。密钥不与那些“纯文本”PEM 标头一起存储。它们存储在 tools.ietf.org/html/rfc5208#page-4 中指定的 ASN.1 对象中。我只是想知道为什么 openssl 似乎无法阅读它。 PEM 没有 DEK-Info 字段。
  • 好吧,显然它不是 ASN1。看起来我的私钥 PEM 具有以下格式 cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.key?annotate=HEAD
  • 我描述的是openSSL加密的PEM私钥格式。你指的是openSSH私钥格式,它不是PEM。你确定它甚至是 ASN.1 格式吗?文件中没有这样说。
  • 看起来 openssh 公钥/私钥是一个我不知道的世界,它们在格式上确实与 openssl pem 文件不同。
  • 也许您应该将标题更改为“SSH-RSA”以更明确地说明您在谈论什么格式?
【解决方案2】:

我建议有两种方法。使用 readLine() 读取命令行输出并检查它是否要求输入密码,然后执行相应的操作。

import Foundation

func runCommand(cmd : String, args : String...) -> (output: [String], error: [String], exitCode: Int32) {

    var output : [String] = []
    var error : [String] = []

    let task = Process()
    task.launchPath = cmd
    task.arguments = args

    let outpipe = Pipe()
    task.standardOutput = outpipe
    let errpipe = Pipe()
    task.standardError = errpipe

    task.launch()

    let outdata = outpipe.fileHandleForReading.readDataToEndOfFile()
    if var string = String(data: outdata, encoding: .utf8) {
        string = string.trimmingCharacters(in: .newlines)
        output = string.components(separatedBy: "\n")
    }

    let errdata = errpipe.fileHandleForReading.readDataToEndOfFile()
    if var string = String(data: errdata, encoding: .utf8) {
        string = string.trimmingCharacters(in: .newlines)
        error = string.components(separatedBy: "\n")
    }

    task.waitUntilExit()
    let status = task.terminationStatus

    return (output, error, status)
}

//Sample usage

let (output, error, status) = runCommand(cmd: "/usr/local/bin/node", args: "--version")
print("program exited with status \(status)")
if output.count > 0 {
    print("program output:")
    print(output)
    //HERE YOU CAN CHECK IF PASSWORD REQUEST HAS BEEN MADE
}
if error.count > 0 {
    print("error output:")
    print(error)
}

如果有,示例代码将返回您安装的节点版本,但您可以使用它来检查系统是否已对 RSA 密钥进行密码提示。

另一种方法可能是使用第三方库,如 SwiftyRSABlueRSA,这可能有助于验证。

【讨论】:

  • 不能在沙盒应用中使用它
  • @ErikAigner 我试过上面的代码编译了一个简单的应用程序,事实上,它是基于标准终端命令行的,它允许执行我假设你已经做过的 shell 命令在您的应用程序中使用。但是你还没有说明。
  • 另外node 不是标准的系统二进制文件
  • @ErikAigner 这只是一个命令行执行和响应读取的示例。当我在一个节点项目上工作时,这是我想到的第一个二进制文件。试一试该命令并尝试读取受密码保护的 ssh 密钥,您应该会以程序输出的形式返回密码提示:.......祝您有美好的一天。
【解决方案3】:

我为两种最常见的格式实现了自己的 OpenSSH 检查

  • 首先,我正在检查用于 Linux 样式 SSH PEM 的 DEK-Info 的 PEM 标头
  • 对于 OpenSSH 样式的密钥,我使用下面的类手动解析格式
import Foundation

private let opensshMagic = "openssh-key-v1"

public class SSHPrivateKey {

    public struct OpenSSHKey {
        let cipherName: String
        let kdfName: String
        let kdfOptions: Data
        let numKeys: Int

        var isEncrypted: Bool {
            return cipherName != "none"
        }
    }

    public let data: Data

    public init(data: Data) {
        self.data = data
    }

    public func openSSHKey() -> OpenSSHKey? {
        // #define AUTH_MAGIC      "openssh-key-v1"
        //
        // byte[]  AUTH_MAGIC
        // string  ciphername
        // string  kdfname
        // string  kdfoptions
        // int     number of keys N
        // string  publickey1
        // string  publickey2
        // ...
        // string  publickeyN
        // string  encrypted, padded list of private keys

        guard let magic = opensshMagic.data(using: .utf8) else {
            return nil
        }

        if data.prefix(magic.count) != magic {
            return nil
        }

        var offset = magic.count + 1

        guard let cipherName = data.consumeString(offset: &offset),
            let kdfName = data.consumeString(offset: &offset) else {
                return nil
        }

        let kdfOptions = data.consumeBytes(offset: &offset)
        let numKeys = data.consumeUInt32(offset: &offset)

        return OpenSSHKey(cipherName: cipherName,
                          kdfName: kdfName,
                          kdfOptions: kdfOptions,
                          numKeys: Int(numKeys))
    }
}

private extension Data {

    func consumeBytes(offset: inout Int) -> Data {
        let n = Int(consumeUInt32(offset: &offset))
        let b = Data(self[offset..<offset + n])
        offset += n
        return b
    }

    func consumeString(offset: inout Int) -> String? {
        return consumeBytes(offset: &offset).utf8String
    }

    func consumeUInt8(offset: inout Int) -> UInt8 {
        let v = self[offset] & 0xFF
        offset += 1

        return v
    }

    func consumeUInt32(offset: inout Int) -> UInt32 {
        var i: UInt32 = 0

        i = (i << 8) | UInt32(consumeUInt8(offset: &offset))
        i = (i << 8) | UInt32(consumeUInt8(offset: &offset))
        i = (i << 8) | UInt32(consumeUInt8(offset: &offset))
        i = (i << 8) | UInt32(consumeUInt8(offset: &offset))

        return i
    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-09-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多