mirror of
https://github.com/laosb/swift-minisign.git
synced 2025-04-30 18:01:08 +00:00
157 lines
5.7 KiB
Swift
157 lines
5.7 KiB
Swift
// SPDX-License-Identifier: MIT
|
|
// Based on https://github.com/slarew/swift-minisign
|
|
// Copyright 2021 Stephen Larew, 2025 Shibo Lyu
|
|
|
|
import BLAKE2
|
|
import Foundation
|
|
|
|
#if UseSwiftCrypto
|
|
import Crypto
|
|
#else
|
|
import CryptoKit
|
|
#endif
|
|
|
|
public enum SignatureAlgorithm: RawRepresentable {
|
|
case pureEdDSA
|
|
case hashedEdDSA
|
|
|
|
private static let dataEd = "Ed".data(using: .utf8)!
|
|
private static let dataED = "ED".data(using: .utf8)!
|
|
|
|
public init?(rawValue: Data) {
|
|
if rawValue == Self.dataEd {
|
|
self = .pureEdDSA
|
|
} else if rawValue == Self.dataED {
|
|
self = .hashedEdDSA
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
public var rawValue: Data {
|
|
switch self {
|
|
case .pureEdDSA: return Self.dataEd
|
|
case .hashedEdDSA: return Self.dataED
|
|
}
|
|
}
|
|
}
|
|
|
|
private let untrustedCommentHeader = "untrusted comment: ".data(using: .utf8)!
|
|
private let trustedCommentHeader = "trusted comment: ".data(using: .utf8)!
|
|
|
|
public struct PublicKey {
|
|
public let untrustedComment: String
|
|
public let signatureAlgorithm: SignatureAlgorithm
|
|
public let keyID: Data
|
|
public let publicKey: Curve25519.Signing.PublicKey
|
|
|
|
public init?<D>(text: D) where D: DataProtocol {
|
|
let lines = text.split(separator: UInt8(ascii: "\n"), maxSplits: 2, omittingEmptySubsequences: false)
|
|
guard lines.count == 2 || lines[2].isEmpty else { return nil }
|
|
guard lines[0].starts(with: untrustedCommentHeader) else { return nil }
|
|
guard
|
|
let untrustedComment = String(
|
|
data: Data(lines[0].suffix(from: untrustedCommentHeader.count as! D.Index)),
|
|
encoding: .utf8
|
|
)
|
|
else { return nil }
|
|
self.untrustedComment = untrustedComment
|
|
guard let decLine2 = Data(base64Encoded: Data(lines[1]), options: []) else { return nil }
|
|
guard decLine2.count == 42 else { return nil }
|
|
guard let sigAlgo = SignatureAlgorithm(rawValue: decLine2.prefix(2)),
|
|
sigAlgo == .pureEdDSA
|
|
else { return nil }
|
|
self.signatureAlgorithm = sigAlgo
|
|
self.keyID = decLine2[2..<10]
|
|
guard let publicKey = try? Curve25519.Signing.PublicKey(rawRepresentation: decLine2[10..<42]) else {
|
|
return nil
|
|
}
|
|
self.publicKey = publicKey
|
|
}
|
|
|
|
public func isValidSignature<D>(_ signature: Signature, for data: D) -> Bool where D: DataProtocol {
|
|
guard signature.keyID == keyID else { return false }
|
|
switch signature.signatureAlgorithm {
|
|
case .pureEdDSA:
|
|
guard publicKey.isValidSignature(signature.signature, for: data) else { return false }
|
|
case .hashedEdDSA:
|
|
let digest = try! BLAKE2b.hash(data: data) // Default parameters are guaranteed to be correct
|
|
guard publicKey.isValidSignature(signature.signature, for: digest) else { return false }
|
|
}
|
|
let globalData = signature.signature + signature.trustedCommentData
|
|
guard publicKey.isValidSignature(signature.globalSignature, for: globalData) else { return false }
|
|
return true
|
|
}
|
|
|
|
/// Verify the signature for a file using the hashed EdDSA algorithm.
|
|
///
|
|
/// This method reads the file in chunks to avoid loading the entire file into memory, but does so in a blocking manner.
|
|
/// It's recommended to use this method in a background thread or task.
|
|
public func isValidSignature(_ signature: Signature, forFileAt url: URL) throws -> Bool {
|
|
guard signature.signatureAlgorithm == .hashedEdDSA else { throw SignatureVerifyError.algorithmNotSupportedForFile }
|
|
var blake2b = try! BLAKE2b()
|
|
|
|
let fileHandle = try FileHandle(forReadingFrom: url)
|
|
|
|
let bufferSize = 4096
|
|
while true {
|
|
let data = fileHandle.readData(ofLength: bufferSize)
|
|
if data.isEmpty { break }
|
|
blake2b.update(data: data)
|
|
}
|
|
|
|
defer { try? fileHandle.close() }
|
|
|
|
let digest = blake2b.finalize()
|
|
guard publicKey.isValidSignature(signature.signature, for: digest) else { return false }
|
|
let globalData = signature.signature + signature.trustedCommentData
|
|
guard publicKey.isValidSignature(signature.globalSignature, for: globalData) else { return false }
|
|
return true
|
|
}
|
|
|
|
public enum SignatureVerifyError: Error {
|
|
/// For ``PublicKey/isValidSignature(_:forFileAt:)``, only ``SignatureAlgorithm/hashedEdDSA`` algorithm is supported.
|
|
case algorithmNotSupportedForFile
|
|
}
|
|
}
|
|
|
|
public struct Signature {
|
|
public let untrustedComment: String
|
|
public let signatureAlgorithm: SignatureAlgorithm
|
|
public let keyID: Data
|
|
public let signature: Data
|
|
public let trustedCommentData: Data
|
|
public let globalSignature: Data
|
|
|
|
public var trustedComment: String? {
|
|
return String(data: trustedCommentData, encoding: .utf8)
|
|
}
|
|
|
|
public init?<D>(text: D) where D: DataProtocol {
|
|
let lines = text.split(separator: UInt8(ascii: "\n"), maxSplits: 4, omittingEmptySubsequences: false)
|
|
guard lines.count == 4 || lines[4].isEmpty else { return nil }
|
|
|
|
guard lines[0].starts(with: untrustedCommentHeader) else { return nil }
|
|
guard
|
|
let untrustedComment = String(
|
|
data: Data(lines[0].suffix(from: untrustedCommentHeader.count as! D.Index)),
|
|
encoding: .utf8
|
|
)
|
|
else { return nil }
|
|
self.untrustedComment = untrustedComment
|
|
|
|
guard let decLine2 = Data(base64Encoded: Data(lines[1]), options: []) else { return nil }
|
|
guard decLine2.count == 74 else { return nil }
|
|
guard let sigAlgo = SignatureAlgorithm(rawValue: decLine2.prefix(2)) else { return nil }
|
|
self.signatureAlgorithm = sigAlgo
|
|
self.keyID = decLine2[2..<10]
|
|
self.signature = decLine2[10..<74]
|
|
|
|
guard lines[2].starts(with: trustedCommentHeader) else { return nil }
|
|
self.trustedCommentData = Data(lines[2]).suffix(from: trustedCommentHeader.count)
|
|
|
|
guard let decLine4 = Data(base64Encoded: Data(lines[3]), options: []) else { return nil }
|
|
guard decLine4.count == 64 else { return nil }
|
|
self.globalSignature = decLine4
|
|
}
|
|
}
|