Skip to content

Commit e2dd5a5

Browse files
baardeLukasa
andauthored
Reduce the number of heap allocations when computing hash digests (apple#238)
* Remove array allocation when computing digest Instead of creating a temporary array, the digest bytes are stored in a temporary stack-allocated buffer. This reduces the number of heap allocations needed to hash a message from 7 to 6. * Store digest context inline This reduces the number of heap allocations needed to hash a message from 6 to 2. Before: 1. the DigestContext object is created 2. the EVP_MD_CTX struct is allocated by EVP_MD_CTX_new 3. md_data is allocated when initializing the context After: 1. the DigestContext object is created In both cases, the number is doubled because COW happens during finalize(). * Use temporary variable to finalize context This reduces the number of heap allocations needed to hash a message from 2 to 1. --------- Co-authored-by: Cory Benfield <[email protected]>
1 parent 4607247 commit e2dd5a5

File tree

1 file changed

+122
-70
lines changed

1 file changed

+122
-70
lines changed

Sources/Crypto/Digests/BoringSSL/Digest_boring.swift

Lines changed: 122 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -19,44 +19,128 @@
1919
protocol HashFunctionImplementationDetails: HashFunction where Digest: DigestPrivate {}
2020

2121
protocol BoringSSLBackedHashFunction: HashFunctionImplementationDetails {
22-
static var digestType: DigestContext.DigestType { get }
22+
associatedtype Context
23+
static var digestSize: Int { get }
24+
static func initialize() -> Context?
25+
static func update(_ context: inout Context, data: UnsafeRawBufferPointer) -> Bool
26+
static func finalize(_ context: inout Context, digest: UnsafeMutableRawBufferPointer) -> Bool
2327
}
2428

2529
extension Insecure.MD5: BoringSSLBackedHashFunction {
26-
static var digestType: DigestContext.DigestType {
27-
.md5
30+
static var digestSize: Int {
31+
Int(MD5_DIGEST_LENGTH)
32+
}
33+
34+
static func initialize() -> MD5_CTX? {
35+
var context = MD5_CTX()
36+
guard CCryptoBoringSSL_MD5_Init(&context) == 1 else {
37+
return nil
38+
}
39+
return context
40+
}
41+
42+
static func update(_ context: inout MD5_CTX, data: UnsafeRawBufferPointer) -> Bool {
43+
CCryptoBoringSSL_MD5_Update(&context, data.baseAddress, data.count) == 1
44+
}
45+
46+
static func finalize(_ context: inout MD5_CTX, digest: UnsafeMutableRawBufferPointer) -> Bool {
47+
CCryptoBoringSSL_MD5_Final(digest.baseAddress, &context) == 1
2848
}
2949
}
3050

3151
extension Insecure.SHA1: BoringSSLBackedHashFunction {
32-
static var digestType: DigestContext.DigestType {
33-
.sha1
52+
static var digestSize: Int {
53+
Int(SHA_DIGEST_LENGTH)
54+
}
55+
56+
static func initialize() -> SHA_CTX? {
57+
var context = SHA_CTX()
58+
guard CCryptoBoringSSL_SHA1_Init(&context) == 1 else {
59+
return nil
60+
}
61+
return context
62+
}
63+
64+
static func update(_ context: inout SHA_CTX, data: UnsafeRawBufferPointer) -> Bool {
65+
CCryptoBoringSSL_SHA1_Update(&context, data.baseAddress, data.count) == 1
66+
}
67+
68+
static func finalize(_ context: inout SHA_CTX, digest: UnsafeMutableRawBufferPointer) -> Bool {
69+
CCryptoBoringSSL_SHA1_Final(digest.baseAddress, &context) == 1
3470
}
3571
}
3672

3773
extension SHA256: BoringSSLBackedHashFunction {
38-
static var digestType: DigestContext.DigestType {
39-
.sha256
74+
static var digestSize: Int {
75+
Int(SHA256_DIGEST_LENGTH)
76+
}
77+
78+
static func initialize() -> SHA256_CTX? {
79+
var context = SHA256_CTX()
80+
guard CCryptoBoringSSL_SHA256_Init(&context) == 1 else {
81+
return nil
82+
}
83+
return context
84+
}
85+
86+
static func update(_ context: inout SHA256_CTX, data: UnsafeRawBufferPointer) -> Bool {
87+
CCryptoBoringSSL_SHA256_Update(&context, data.baseAddress, data.count) == 1
88+
}
89+
90+
static func finalize(_ context: inout SHA256_CTX, digest: UnsafeMutableRawBufferPointer) -> Bool {
91+
CCryptoBoringSSL_SHA256_Final(digest.baseAddress, &context) == 1
4092
}
4193
}
4294

4395
extension SHA384: BoringSSLBackedHashFunction {
44-
static var digestType: DigestContext.DigestType {
45-
.sha384
96+
static var digestSize: Int {
97+
Int(SHA384_DIGEST_LENGTH)
98+
}
99+
100+
static func initialize() -> SHA512_CTX? {
101+
var context = SHA512_CTX()
102+
guard CCryptoBoringSSL_SHA384_Init(&context) == 1 else {
103+
return nil
104+
}
105+
return context
106+
}
107+
108+
static func update(_ context: inout SHA512_CTX, data: UnsafeRawBufferPointer) -> Bool {
109+
CCryptoBoringSSL_SHA384_Update(&context, data.baseAddress, data.count) == 1
110+
}
111+
112+
static func finalize(_ context: inout SHA512_CTX, digest: UnsafeMutableRawBufferPointer) -> Bool {
113+
CCryptoBoringSSL_SHA384_Final(digest.baseAddress, &context) == 1
46114
}
47115
}
48116

49117
extension SHA512: BoringSSLBackedHashFunction {
50-
static var digestType: DigestContext.DigestType {
51-
.sha512
118+
static var digestSize: Int {
119+
Int(SHA512_DIGEST_LENGTH)
120+
}
121+
122+
static func initialize() -> SHA512_CTX? {
123+
var context = SHA512_CTX()
124+
guard CCryptoBoringSSL_SHA512_Init(&context) == 1 else {
125+
return nil
126+
}
127+
return context
128+
}
129+
130+
static func update(_ context: inout SHA512_CTX, data: UnsafeRawBufferPointer) -> Bool {
131+
CCryptoBoringSSL_SHA512_Update(&context, data.baseAddress, data.count) == 1
132+
}
133+
134+
static func finalize(_ context: inout SHA512_CTX, digest: UnsafeMutableRawBufferPointer) -> Bool {
135+
CCryptoBoringSSL_SHA512_Final(digest.baseAddress, &context) == 1
52136
}
53137
}
54138

55139
struct OpenSSLDigestImpl<H: BoringSSLBackedHashFunction> {
56-
private var context: DigestContext
140+
private var context: DigestContext<H>
57141

58142
init() {
59-
self.context = DigestContext(digest: H.digestType)
143+
self.context = DigestContext()
60144
}
61145

62146
internal mutating func update(data: UnsafeRawBufferPointer) {
@@ -67,81 +151,49 @@ struct OpenSSLDigestImpl<H: BoringSSLBackedHashFunction> {
67151
}
68152

69153
internal func finalize() -> H.Digest {
70-
// To have a non-destructive finalize operation we must allocate.
71-
let copyContext = DigestContext(copying: self.context)
72-
let digestBytes = copyContext.finalize()
73-
return digestBytes.withUnsafeBytes {
74-
// We force unwrap here because if the digest size is wrong it's an internal error.
75-
H.Digest(bufferPointer: $0)!
76-
}
154+
self.context.finalize()
77155
}
78156
}
79157

80-
class DigestContext {
81-
private var contextPointer: UnsafeMutablePointer<EVP_MD_CTX>
158+
fileprivate final class DigestContext<H: BoringSSLBackedHashFunction> {
159+
private var context: H.Context
82160

83-
init(digest: DigestType) {
84-
// We force unwrap because we cannot recover from allocation failure.
85-
self.contextPointer = CCryptoBoringSSL_EVP_MD_CTX_new()!
86-
guard CCryptoBoringSSL_EVP_DigestInit(self.contextPointer, digest.dispatchTable) != 0 else {
87-
// We can't do much but crash here.
88-
fatalError("Unable to initialize digest state: \(CCryptoBoringSSL_ERR_get_error())")
161+
init() {
162+
guard let contex = H.initialize() else {
163+
preconditionFailure("Unable to initialize digest state")
89164
}
165+
self.context = contex
90166
}
91167

92168
init(copying original: DigestContext) {
93-
// We force unwrap because we cannot recover from allocation failure.
94-
self.contextPointer = CCryptoBoringSSL_EVP_MD_CTX_new()!
95-
guard CCryptoBoringSSL_EVP_MD_CTX_copy(self.contextPointer, original.contextPointer) != 0 else {
96-
// We can't do much but crash here.
97-
fatalError("Unable to copy digest state: \(CCryptoBoringSSL_ERR_get_error())")
98-
}
169+
self.context = original.context
99170
}
100171

101172
func update(data: UnsafeRawBufferPointer) {
102-
guard let baseAddress = data.baseAddress else {
103-
return
173+
guard H.update(&self.context, data: data) else {
174+
preconditionFailure("Unable to update digest state")
104175
}
105-
106-
CCryptoBoringSSL_EVP_DigestUpdate(self.contextPointer, baseAddress, data.count)
107176
}
108177

109-
// This finalize function is _destructive_: do not call it if you want to reuse the object!
110-
func finalize() -> [UInt8] {
111-
let digestSize = CCryptoBoringSSL_EVP_MD_size(self.contextPointer.pointee.digest)
112-
var digestBytes = Array(repeating: UInt8(0), count: digestSize)
113-
var count = UInt32(digestSize)
114-
115-
digestBytes.withUnsafeMutableBufferPointer { digestPointer in
116-
assert(digestPointer.count == count)
117-
CCryptoBoringSSL_EVP_DigestFinal(self.contextPointer, digestPointer.baseAddress, &count)
178+
func finalize() -> H.Digest {
179+
var copyContext = self.context
180+
defer {
181+
withUnsafeMutablePointer(to: &copyContext) { $0.zeroize() }
182+
}
183+
return withUnsafeTemporaryAllocation(byteCount: H.digestSize, alignment: 1) { digestPointer in
184+
defer {
185+
digestPointer.zeroize()
186+
}
187+
guard H.finalize(&copyContext, digest: digestPointer) else {
188+
preconditionFailure("Unable to finalize digest state")
189+
}
190+
// We force unwrap here because if the digest size is wrong it's an internal error.
191+
return H.Digest(bufferPointer: UnsafeRawBufferPointer(digestPointer))!
118192
}
119-
120-
return digestBytes
121193
}
122194

123195
deinit {
124-
CCryptoBoringSSL_EVP_MD_CTX_free(self.contextPointer)
125-
}
126-
}
127-
128-
extension DigestContext {
129-
struct DigestType {
130-
var dispatchTable: OpaquePointer
131-
132-
private init(_ dispatchTable: OpaquePointer) {
133-
self.dispatchTable = dispatchTable
134-
}
135-
136-
static let md5 = DigestType(CCryptoBoringSSL_EVP_md5())
137-
138-
static let sha1 = DigestType(CCryptoBoringSSL_EVP_sha1())
139-
140-
static let sha256 = DigestType(CCryptoBoringSSL_EVP_sha256())
141-
142-
static let sha384 = DigestType(CCryptoBoringSSL_EVP_sha384())
143-
144-
static let sha512 = DigestType(CCryptoBoringSSL_EVP_sha512())
196+
withUnsafeMutablePointer(to: &self.context) { $0.zeroize() }
145197
}
146198
}
147199
#endif // CRYPTO_IN_SWIFTPM && !CRYPTO_IN_SWIFTPM_FORCE_BUILD_API

0 commit comments

Comments
 (0)