diff --git a/Sources/Crypto/Digests/BoringSSL/Digest_boring.swift b/Sources/Crypto/Digests/BoringSSL/Digest_boring.swift index 0ecc61c4..0551af55 100644 --- a/Sources/Crypto/Digests/BoringSSL/Digest_boring.swift +++ b/Sources/Crypto/Digests/BoringSSL/Digest_boring.swift @@ -173,10 +173,10 @@ private final class DigestContext { private var context: H.Context init() { - guard let contex = H.initialize() else { + guard let context = H.initialize() else { preconditionFailure("Unable to initialize digest state") } - self.context = contex + self.context = context } init(copying original: DigestContext) { diff --git a/Sources/CryptoExtras/CMakeLists.txt b/Sources/CryptoExtras/CMakeLists.txt index 22bc012a..e9a74327 100644 --- a/Sources/CryptoExtras/CMakeLists.txt +++ b/Sources/CryptoExtras/CMakeLists.txt @@ -33,6 +33,10 @@ add_library(CryptoExtras "ARC/ARCServer.swift" "ChaCha20CTR/BoringSSL/ChaCha20CTR_boring.swift" "ChaCha20CTR/ChaCha20CTR.swift" + "Digests/BoringSSL/BoringSSLSHA512256Context.swift" + "Digests/BoringSSL/BoringSSLSHA512256HashFunction.swift" + "Digests/SHA512256.swift" + "Digests/SHA512256Digest.swift" "EC/Curve25519+PEM.swift" "EC/ObjectIdentifier.swift" "EC/PKCS8DERRepresentation.swift" @@ -58,6 +62,7 @@ add_library(CryptoExtras "RSA/RSA.swift" "RSA/RSA_boring.swift" "Reexport.swift" + "Util/BoringSSL/Zeroization_boring.swift" "Util/BoringSSLHelpers.swift" "Util/CryptoKitErrors_boring.swift" "Util/Data+Extensions.swift" diff --git a/Sources/CryptoExtras/Digests/BoringSSL/BoringSSLSHA512256Context.swift b/Sources/CryptoExtras/Digests/BoringSSL/BoringSSLSHA512256Context.swift new file mode 100644 index 00000000..c091bc3f --- /dev/null +++ b/Sources/CryptoExtras/Digests/BoringSSL/BoringSSLSHA512256Context.swift @@ -0,0 +1,82 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2026 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@_implementationOnly import CCryptoBoringSSL +import Crypto + +#if canImport(Darwin) +import Darwin +#endif + +@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *) +final class BoringSSLSHA512256Context { + private var context: SHA512_CTX + + init() { + guard let context = BoringSSLSHA512256HashFunction.initialize() else { + preconditionFailure("Unable to initialize digest state") + } + self.context = context + } + + deinit { + withUnsafeMutablePointer(to: &self.context) { + $0.zeroize() + } + } + + init(copying original: BoringSSLSHA512256Context) { + self.context = original.context + } + + func update(bufferPointer data: UnsafeRawBufferPointer) { + guard BoringSSLSHA512256HashFunction.update(&self.context, data: data) else { + preconditionFailure("Unable to update digest state") + } + } + + func finalize() -> SHA512256Digest { + var copyContext = self.context + defer { + withUnsafeMutablePointer(to: ©Context) { + $0.zeroize() + } + } + return withUnsafeTemporaryAllocation(byteCount: BoringSSLSHA512256HashFunction.digestSize, alignment: 1) { + digestPointer in + defer { + digestPointer.zeroize() + } + + guard BoringSSLSHA512256HashFunction.finalize(©Context, digest: digestPointer) else { + preconditionFailure("Unable to finalize digest state") + } + // We force unwrap here because if the digest size is wrong it's an internal error. + return SHA512256Digest(bufferPointer: UnsafeRawBufferPointer(digestPointer))! + } + } +} + +extension UnsafeMutablePointer { + fileprivate func zeroize() { + let size = MemoryLayout.size(ofValue: Pointee.self) + memset_s(self, size, 0, size) + } +} + +extension UnsafeMutableRawBufferPointer { + fileprivate func zeroize() { + memset_s(self.baseAddress!, self.count, 0, self.count) + } +} diff --git a/Sources/CryptoExtras/Digests/BoringSSL/BoringSSLSHA512256HashFunction.swift b/Sources/CryptoExtras/Digests/BoringSSL/BoringSSLSHA512256HashFunction.swift new file mode 100644 index 00000000..d97a2369 --- /dev/null +++ b/Sources/CryptoExtras/Digests/BoringSSL/BoringSSLSHA512256HashFunction.swift @@ -0,0 +1,43 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2026 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +@_implementationOnly import CCryptoBoringSSL +import Crypto + +struct BoringSSLSHA512256HashFunction { + static var digestSize: Int { + Int(SHA256_DIGEST_LENGTH) + } + + static func initialize() -> SHA512_CTX? { + var context = SHA512_CTX() + guard CCryptoBoringSSL_SHA512_256_Init(&context) == 1 else { + return nil + } + return context + } + + static func update(_ context: inout SHA512_CTX, data: UnsafeRawBufferPointer) -> Bool { + let result = CCryptoBoringSSL_SHA512_256_Update(&context, data.baseAddress, data.count) + return result == 1 + } + + static func finalize(_ context: inout SHA512_CTX, digest: UnsafeMutableRawBufferPointer) -> Bool { + guard let baseAddress = digest.baseAddress, digest.count == Self.digestSize else { + return false + } + let result = CCryptoBoringSSL_SHA512_256_Final(baseAddress, &context) + return result == 1 + } +} diff --git a/Sources/CryptoExtras/Digests/SHA512256.swift b/Sources/CryptoExtras/Digests/SHA512256.swift new file mode 100644 index 00000000..86924d74 --- /dev/null +++ b/Sources/CryptoExtras/Digests/SHA512256.swift @@ -0,0 +1,88 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2026 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Crypto + +/// An implementation of Secure Hashing Algorithm 2 (SHA-2) hashing with a +/// 256-bit digest using the SHA-512/256 variant. +/// +/// The ``SHA512256`` hash implements the ``HashFunction`` protocol for the +/// specific case of SHA-512/256 hashing with a 256-bit digest +/// (``SHA512256Digest``). SHA-512/256 is a truncated variant of SHA-512 that +/// provides the same security level as SHA-256 but can be faster on 64-bit +/// platforms. +/// +/// You can compute the digest by calling the static ``hash(data:)`` method +/// once. Alternatively, if the data that you want to hash is too large to fit +/// in memory, you can compute the digest iteratively by creating a new hash +/// instance, calling the ``update(data:)`` method repeatedly with blocks of +/// data, and then calling the ``finalize()`` method to get the result. +@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *) +public struct SHA512256: HashFunction, @unchecked Sendable { + /// The number of bytes that represents the hash function's internal state. + public static var blockByteCount: Int { + 128 + } + + private var context = BoringSSLSHA512256Context() + + /// Creates a SHA512-256 hash function. + /// + /// Initialize a new hash function by calling this method if you want to + /// hash data iteratively, such as when you don't have a buffer large enough + /// to hold all the data at once. Provide data blocks to the hash function + /// using the ``update(data:)`` or ``update(bufferPointer:)`` method. After + /// providing all the data, call ``finalize()`` to get the digest. + /// + /// If your data fits into a single buffer, you can use the ``hash(data:)`` + /// method instead, to compute the digest in a single call. + public init() {} + + /// Incrementally updates the hash function with the contents of the buffer. + /// + /// Call this method one or more times to provide data to the hash function + /// in blocks. After providing the last block of data, call the + /// ``finalize()`` method to get the computed digest. Don't call the update + /// method again after finalizing the hash function. + /// + /// - Note: Typically, it's safer to use an instance of + /// , or some + /// other type that conforms to the + /// , + /// to hold your data. When possible, use the ``update(data:)`` method + /// instead. + /// + /// - Parameters: + /// - bufferPointer: A pointer to the next block of data for the ongoing + /// digest calculation. + public mutating func update(bufferPointer data: UnsafeRawBufferPointer) { + if !isKnownUniquelyReferenced(&self.context) { + self.context = BoringSSLSHA512256Context(copying: self.context) + } + self.context.update(bufferPointer: data) + } + + /// Finalizes the hash function and returns the computed digest. + /// + /// Call this method after you provide the hash function with all the data + /// to hash by making one or more calls to the ``update(data:)`` or + /// ``update(bufferPointer:)`` method. After finalizing the hash function, + /// discard it. To compute a new digest, create a new hash function with a + /// call to the ``init()`` method. + /// + /// - Returns: The computed digest of the data. + public func finalize() -> SHA512256Digest { + self.context.finalize() + } +} diff --git a/Sources/CryptoExtras/Digests/SHA512256Digest.swift b/Sources/CryptoExtras/Digests/SHA512256Digest.swift new file mode 100644 index 00000000..0a41c5d2 --- /dev/null +++ b/Sources/CryptoExtras/Digests/SHA512256Digest.swift @@ -0,0 +1,99 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2026 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/// The output of a Secure Hashing Algorithm 2 (SHA-2) hash with a 256-bit digest +/// using the SHA-512/256 variant. +@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *) +public struct SHA512256Digest: Digest { + let bytes: (UInt64, UInt64, UInt64, UInt64) + + /// The number of bytes in the digest. + public static var byteCount: Int { + 32 + } + + init?(bufferPointer: UnsafeRawBufferPointer) { + guard bufferPointer.count == 32 else { + return nil + } + + var bytes = (UInt64(0), UInt64(0), UInt64(0), UInt64(0)) + withUnsafeMutableBytes(of: &bytes) { targetPtr in + targetPtr.copyBytes(from: bufferPointer) + } + self.bytes = bytes + } + + /// Invokes the given closure with a buffer pointer covering the raw bytes of + /// the digest. + /// + /// - Parameters: + /// - body: A closure that takes a raw buffer pointer to the bytes of the digest + /// and returns the digest. + /// + /// - Returns: The digest, as returned from the body closure. + #if !hasFeature(Embedded) + public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws -> R) rethrows -> R { + try Swift.withUnsafeBytes(of: bytes) { + let boundsCheckedPtr = UnsafeRawBufferPointer( + start: $0.baseAddress, + count: Self.byteCount + ) + return try body(boundsCheckedPtr) + } + } + #else + public func withUnsafeBytes(_ body: (UnsafeRawBufferPointer) throws(E) -> R) throws(E) -> R { + try Swift.withUnsafeBytes(of: bytes) { ptr throws(E) -> R in + let boundsCheckedPtr = UnsafeRawBufferPointer( + start: ptr.baseAddress, + count: Self.byteCount + ) + return try body(boundsCheckedPtr) + } + } + #endif + + private func toArray() -> ArraySlice { + var array = [UInt8]() + array.appendByte(bytes.0) + array.appendByte(bytes.1) + array.appendByte(bytes.2) + array.appendByte(bytes.3) + return array.prefix(SHA512256Digest.byteCount) + } + + #if !hasFeature(Embedded) + /// A human-readable description of the digest. + public var description: String { + "SHA512-256 digest: \(toArray().hexString)" + } + #endif + + /// Hashes the essential components of the digest by feeding them into the + /// given hash function. + /// + /// This method is part of the digest’s conformance to Swift standard library’s + /// protocol, making + /// it possible to compare digests. Don’t confuse that hashing with the + /// cryptographically secure hashing that you use to create the digest in the + /// first place by, for example, calling ``SHA512256/hash(data:)``. + /// + /// - Parameters: + /// - hasher: The hash function to use when combining the components of + /// the digest. + public func hash(into hasher: inout Hasher) { + self.withUnsafeBytes { hasher.combine(bytes: $0) } + } +} diff --git a/Sources/CryptoExtras/Util/BoringSSL/Zeroization_boring.swift b/Sources/CryptoExtras/Util/BoringSSL/Zeroization_boring.swift new file mode 100644 index 00000000..1991f2f6 --- /dev/null +++ b/Sources/CryptoExtras/Util/BoringSSL/Zeroization_boring.swift @@ -0,0 +1,29 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2019 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +#if !canImport(Darwin) +@_implementationOnly import CCryptoBoringSSL + +@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *) +typealias errno_t = CInt + +// This is a Swift wrapper for the libc function that does not exist on Linux. We shim it via a call to OPENSSL_cleanse. +// We have the same syntax, but mostly ignore it. +@discardableResult +func memset_s(_ s: UnsafeMutableRawPointer!, _ smax: Int, _ byte: CInt, _ n: Int) -> errno_t { + assert(smax == n, "memset_s invariant not met") + assert(byte == 0, "memset_s used to not zero anything") + CCryptoBoringSSL_OPENSSL_cleanse(s, smax) + return 0 +} +#endif diff --git a/Tests/CryptoExtrasTests/SHA512256Tests.swift b/Tests/CryptoExtrasTests/SHA512256Tests.swift new file mode 100644 index 00000000..37ae51fd --- /dev/null +++ b/Tests/CryptoExtrasTests/SHA512256Tests.swift @@ -0,0 +1,97 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2026 Apple Inc. and the SwiftCrypto project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftCrypto project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Crypto +import CryptoExtras +import XCTest + +final class SHA512256DigestTests: XCTestCase { + func testHashFunction() throws { + let data = + "abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu" + .data(using: .ascii)! + + var hasher = SHA512256() + hasher.update(data: data) + let digest = hasher.finalize() + + let expected = try Array(hexString: "3928e184fb8690f840da3988121d31be65cb9d3ef83ee6146feac861e19b563a") + XCTAssertEqual(Array(digest), expected) + XCTAssertEqual(Array(SHA512256.hash(data: data)), expected) + + let (contiguousResult, discontiguousResult) = expected.asDataProtocols() + XCTAssert(digest == contiguousResult) + XCTAssert(digest == discontiguousResult) + XCTAssertFalse(digest == DispatchData.empty) + } + + func testNullHash() throws { + let digest = SHA512256.hash(data: Data()) + + let expected = try Array(hexString: "c672b8d1ef56ed28ab87c3622c5114069bdd3ad7b8f9737498d0c01ecef0967a") + XCTAssertEqual(Array(digest), expected) + } + + func testABC() throws { + let digest = SHA512256.hash(data: "abc".data(using: .ascii)!) + + let expected = try Array(hexString: "53048e2681941ef99b2e29b76b4c7dabe4c2d0c634fc6d46e0e2f13107e7af23") + XCTAssertEqual(Array(digest), expected) + } + + func testTwoBlockMessage() throws { + let data = "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopmopq".data(using: .ascii)! + let digest = SHA512256.hash(data: data) + + let expected = try Array(hexString: "35de8c8794b9ed4b463c257fe50e62b516e07976a2931f4f78f1cd69a456dccd") + XCTAssertEqual(Array(digest), expected) + } + + func testSingleByte() throws { + let digest = SHA512256.hash(data: Data([0x61])) + + let expected = try Array(hexString: "455e518824bc0601f9fb858ff5c37d417d67c2f8e0df2babe4808858aea830f8") + XCTAssertEqual(Array(digest), expected) + } + + func testRepeatedBytes() throws { + let data = Data(repeating: 0x61, count: 1_000_000) + let digest = SHA512256.hash(data: data) + + let expected = try Array(hexString: "9a59a052930187a97038cae692f30708aa6491923ef5194394dc68d56c74fb21") + XCTAssertEqual(Array(digest), expected) + } + + func testCopyOnWrite() { + var hasher = SHA512256() + hasher.update(data: [1, 2, 3, 4]) + + var copy = hasher + hasher.update(data: [5, 6, 7, 8]) + let digest = hasher.finalize() + + copy.update(data: [5, 6, 7, 8]) + let copyDigest = copy.finalize() + + XCTAssertEqual(digest, copyDigest) + } + + func testBlockSize() { + XCTAssertEqual(SHA512256.blockByteCount, 128) + } + + func testDigestByteCount() { + XCTAssertEqual(SHA512256Digest.byteCount, 32) + } +}