diff --git a/Kingfisher.xcodeproj/project.pbxproj b/Kingfisher.xcodeproj/project.pbxproj index 687f82d5d..7e32571f3 100644 --- a/Kingfisher.xcodeproj/project.pbxproj +++ b/Kingfisher.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 07292245263B02F00089E810 /* KFAnimatedImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07292244263B02F00089E810 /* KFAnimatedImage.swift */; }; 22FDCE0E2700078B0044D11E /* CPListItem+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22FDCE0D2700078B0044D11E /* CPListItem+Kingfisher.swift */; }; + 3ADE9AF92A73CD69009A86CA /* String+SHA256.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ADE9AF82A73CD69009A86CA /* String+SHA256.swift */; }; 4B10480D216F157000300C61 /* ImageDataProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B10480C216F157000300C61 /* ImageDataProcessor.swift */; }; 4B46CC5F217449C600D90C4A /* MemoryStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B46CC5E217449C600D90C4A /* MemoryStorage.swift */; }; 4B46CC64217449E000D90C4A /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B46CC63217449E000D90C4A /* Storage.swift */; }; @@ -52,7 +53,6 @@ D12AB718215D2BB50013BA68 /* CacheSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6B7215D2BB50013BA68 /* CacheSerializer.swift */; }; D12AB71C215D2BB50013BA68 /* FormatIndicatedCacheSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6B8215D2BB50013BA68 /* FormatIndicatedCacheSerializer.swift */; }; D12AB724215D2BB50013BA68 /* Box.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6BB215D2BB50013BA68 /* Box.swift */; }; - D12AB728215D2BB50013BA68 /* String+MD5.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6BC215D2BB50013BA68 /* String+MD5.swift */; }; D12AB72C215D2BB50013BA68 /* Indicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6BE215D2BB50013BA68 /* Indicator.swift */; }; D12AB730215D2BB50013BA68 /* AnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D12AB6BF215D2BB50013BA68 /* AnimatedImageView.swift */; }; D12E0C4F1C47F23500AC98AD /* dancing-banana.gif in Resources */ = {isa = PBXBuildFile; fileRef = D12E0C441C47F23500AC98AD /* dancing-banana.gif */; }; @@ -135,6 +135,7 @@ 07292244263B02F00089E810 /* KFAnimatedImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KFAnimatedImage.swift; sourceTree = ""; }; 185218B51CC07F8300BD58DE /* NSButtonExtensionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSButtonExtensionTests.swift; sourceTree = ""; }; 22FDCE0D2700078B0044D11E /* CPListItem+Kingfisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CPListItem+Kingfisher.swift"; sourceTree = ""; }; + 3ADE9AF82A73CD69009A86CA /* String+SHA256.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+SHA256.swift"; sourceTree = ""; }; 4B10480C216F157000300C61 /* ImageDataProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDataProcessor.swift; sourceTree = ""; }; 4B164ACE1B8D554200768EC6 /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = System/Library/Frameworks/CFNetwork.framework; sourceTree = SDKROOT; }; 4B3E714D1B01FEB200F5AAED /* WatchKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WatchKit.framework; path = System/Library/Frameworks/WatchKit.framework; sourceTree = SDKROOT; }; @@ -182,7 +183,6 @@ D12AB6B7215D2BB50013BA68 /* CacheSerializer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CacheSerializer.swift; sourceTree = ""; }; D12AB6B8215D2BB50013BA68 /* FormatIndicatedCacheSerializer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FormatIndicatedCacheSerializer.swift; sourceTree = ""; }; D12AB6BB215D2BB50013BA68 /* Box.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Box.swift; sourceTree = ""; }; - D12AB6BC215D2BB50013BA68 /* String+MD5.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+MD5.swift"; sourceTree = ""; }; D12AB6BE215D2BB50013BA68 /* Indicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Indicator.swift; sourceTree = ""; }; D12AB6BF215D2BB50013BA68 /* AnimatedImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimatedImageView.swift; sourceTree = ""; }; D12E0C441C47F23500AC98AD /* dancing-banana.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = "dancing-banana.gif"; sourceTree = ""; }; @@ -418,12 +418,12 @@ children = ( D13646732165A1A100A33652 /* Result.swift */, D12AB6BB215D2BB50013BA68 /* Box.swift */, - D12AB6BC215D2BB50013BA68 /* String+MD5.swift */, D1A37BE7215D365A009B39B7 /* ExtensionHelpers.swift */, D1A37BF1215D3850009B39B7 /* SizeExtensions.swift */, D1839844216E333E003927D3 /* Delegate.swift */, 4B8351CB217084660081EED8 /* Runtime.swift */, D1BA781C2174D07800C69D7B /* CallbackQueue.swift */, + 3ADE9AF82A73CD69009A86CA /* String+SHA256.swift */, ); path = Utility; sourceTree = ""; @@ -811,7 +811,6 @@ D12AB6DC215D2BB50013BA68 /* ImageProcessor.swift in Sources */, D12AB6D4215D2BB50013BA68 /* Image.swift in Sources */, D1AEB09425890DE7008556DF /* ImageBinder.swift in Sources */, - D12AB728215D2BB50013BA68 /* String+MD5.swift in Sources */, 4B8E2917216F3F7F0095FAD1 /* ImageDownloaderDelegate.swift in Sources */, D1132C9725919F69003E528D /* KFOptionsSetter.swift in Sources */, D18B3222251852E100662F63 /* KF.swift in Sources */, @@ -852,6 +851,7 @@ 4BE688F822FD513700B11168 /* WKInterfaceImage+Kingfisher.swift in Sources */, D12AB724215D2BB50013BA68 /* Box.swift in Sources */, 4B8E291C216F40AA0095FAD1 /* AuthenticationChallengeResponsable.swift in Sources */, + 3ADE9AF92A73CD69009A86CA /* String+SHA256.swift in Sources */, D12AB710215D2BB50013BA68 /* KingfisherOptionsInfo.swift in Sources */, DCEB2842257E4BE100D7A610 /* TVMonogramView+Kingfisher.swift in Sources */, ); diff --git a/Sources/Cache/DiskStorage.swift b/Sources/Cache/DiskStorage.swift index 89853ceab..3ebbb959b 100644 --- a/Sources/Cache/DiskStorage.swift +++ b/Sources/Cache/DiskStorage.swift @@ -315,7 +315,7 @@ public enum DiskStorage { func cacheFileName(forKey key: String) -> String { if config.usesHashedFileName { - let hashedKey = key.kf.md5 + let hashedKey = key.kf.sha256 if let ext = config.pathExtension { return "\(hashedKey).\(ext)" } else if config.autoExtAfterHashedFileName, diff --git a/Sources/Utility/String+MD5.swift b/Sources/Utility/String+MD5.swift deleted file mode 100644 index 59586b5c1..000000000 --- a/Sources/Utility/String+MD5.swift +++ /dev/null @@ -1,278 +0,0 @@ -// -// String+MD5.swift -// Kingfisher -// -// Created by Wei Wang on 18/09/25. -// -// Copyright (c) 2019 Wei Wang -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -import Foundation -import CommonCrypto - -extension String: KingfisherCompatibleValue { } -extension KingfisherWrapper where Base == String { - var md5: String { - guard let data = base.data(using: .utf8) else { - return base - } - - let message = data.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) in - return [UInt8](bytes) - } - - let MD5Calculator = MD5(message) - let MD5Data = MD5Calculator.calculate() - - var MD5String = String() - for c in MD5Data { - MD5String += String(format: "%02x", c) - } - return MD5String - } - - var ext: String? { - var ext = "" - if let index = base.lastIndex(of: ".") { - let extRange = base.index(index, offsetBy: 1).. 0 ? String(firstSeg) : nil - } -} - -// array of bytes, little-endian representation -func arrayOfBytes(_ value: T, length: Int? = nil) -> [UInt8] { - let totalBytes = length ?? (MemoryLayout.size * 8) - - let valuePointer = UnsafeMutablePointer.allocate(capacity: 1) - valuePointer.pointee = value - - let bytes = valuePointer.withMemoryRebound(to: UInt8.self, capacity: totalBytes) { (bytesPointer) -> [UInt8] in - var bytes = [UInt8](repeating: 0, count: totalBytes) - for j in 0...size, totalBytes) { - bytes[totalBytes - 1 - j] = (bytesPointer + j).pointee - } - return bytes - } - - valuePointer.deinitialize(count: 1) - valuePointer.deallocate() - - return bytes -} - -extension Int { - // Array of bytes with optional padding (little-endian) - func bytes(_ totalBytes: Int = MemoryLayout.size) -> [UInt8] { - return arrayOfBytes(self, length: totalBytes) - } - -} - -protocol HashProtocol { - var message: [UInt8] { get } - // Common part for hash calculation. Prepare header data. - func prepare(_ len: Int) -> [UInt8] -} - -extension HashProtocol { - - func prepare(_ len: Int) -> [UInt8] { - var tmpMessage = message - - // Step 1. Append Padding Bits - tmpMessage.append(0x80) // append one bit (UInt8 with one bit) to message - - // append "0" bit until message length in bits ≡ 448 (mod 512) - var msgLength = tmpMessage.count - var counter = 0 - - while msgLength % len != (len - 8) { - counter += 1 - msgLength += 1 - } - - tmpMessage += [UInt8](repeating: 0, count: counter) - return tmpMessage - } -} - -func toUInt32Array(_ slice: ArraySlice) -> [UInt32] { - var result = [UInt32]() - result.reserveCapacity(16) - - for idx in stride(from: slice.startIndex, to: slice.endIndex, by: MemoryLayout.size) { - let d0 = UInt32(slice[idx.advanced(by: 3)]) << 24 - let d1 = UInt32(slice[idx.advanced(by: 2)]) << 16 - let d2 = UInt32(slice[idx.advanced(by: 1)]) << 8 - let d3 = UInt32(slice[idx]) - let val: UInt32 = d0 | d1 | d2 | d3 - - result.append(val) - } - return result -} - -struct BytesIterator: IteratorProtocol { - - let chunkSize: Int - let data: [UInt8] - - init(chunkSize: Int, data: [UInt8]) { - self.chunkSize = chunkSize - self.data = data - } - - var offset = 0 - - mutating func next() -> ArraySlice? { - let end = min(chunkSize, data.count - offset) - let result = data[offset.. 0 ? result : nil - } -} - -struct BytesSequence: Sequence { - let chunkSize: Int - let data: [UInt8] - - func makeIterator() -> BytesIterator { - return BytesIterator(chunkSize: chunkSize, data: data) - } -} - -func rotateLeft(_ value: UInt32, bits: UInt32) -> UInt32 { - return ((value << bits) & 0xFFFFFFFF) | (value >> (32 - bits)) -} - -class MD5: HashProtocol { - - let message: [UInt8] - - init (_ message: [UInt8]) { - self.message = message - } - - // specifies the per-round shift amounts - private let shifts: [UInt32] = [7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, - 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, - 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, - 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21] - - // binary integer part of the sines of integers (Radians) - private let sines: [UInt32] = [0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, - 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501, - 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, - 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, - 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, - 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8, - 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, - 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a, - 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, - 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, - 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x4881d05, - 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, - 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, - 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1, - 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, - 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391] - - private let hashes: [UInt32] = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476] - - func calculate() -> [UInt8] { - var tmpMessage = prepare(64) - tmpMessage.reserveCapacity(tmpMessage.count + 4) - - // hash values - var hh = hashes - - // Step 2. Append Length a 64-bit representation of lengthInBits - let lengthInBits = (message.count * 8) - let lengthBytes = lengthInBits.bytes(64 / 8) - tmpMessage += lengthBytes.reversed() - - // Process the message in successive 512-bit chunks: - let chunkSizeBytes = 512 / 8 // 64 - - for chunk in BytesSequence(chunkSize: chunkSizeBytes, data: tmpMessage) { - // break chunk into sixteen 32-bit words M[j], 0 ≤ j ≤ 15 - let M = toUInt32Array(chunk) - assert(M.count == 16, "Invalid array") - - // Initialize hash value for this chunk: - var A: UInt32 = hh[0] - var B: UInt32 = hh[1] - var C: UInt32 = hh[2] - var D: UInt32 = hh[3] - - var dTemp: UInt32 = 0 - - // Main loop - for j in 0 ..< sines.count { - var g = 0 - var F: UInt32 = 0 - - switch j { - case 0...15: - F = (B & C) | ((~B) & D) - g = j - case 16...31: - F = (D & B) | (~D & C) - g = (5 * j + 1) % 16 - case 32...47: - F = B ^ C ^ D - g = (3 * j + 5) % 16 - case 48...63: - F = C ^ (B | (~D)) - g = (7 * j) % 16 - default: - break - } - dTemp = D - D = C - C = B - B = B &+ rotateLeft((A &+ F &+ sines[j] &+ M[g]), bits: shifts[j]) - A = dTemp - } - - hh[0] = hh[0] &+ A - hh[1] = hh[1] &+ B - hh[2] = hh[2] &+ C - hh[3] = hh[3] &+ D - } - var result = [UInt8]() - result.reserveCapacity(hh.count / 4) - - hh.forEach { - let itemLE = $0.littleEndian - let r1 = UInt8(itemLE & 0xff) - let r2 = UInt8((itemLE >> 8) & 0xff) - let r3 = UInt8((itemLE >> 16) & 0xff) - let r4 = UInt8((itemLE >> 24) & 0xff) - result += [r1, r2, r3, r4] - } - return result - } -} diff --git a/Sources/Utility/String+SHA256.swift b/Sources/Utility/String+SHA256.swift new file mode 100644 index 000000000..8898df1de --- /dev/null +++ b/Sources/Utility/String+SHA256.swift @@ -0,0 +1,58 @@ +// +// String+SHA256.swift +// Kingfisher +// +// Created by kaimaschke on 28.07.23. +// +// Copyright (c) 2023 Wei Wang +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import CryptoKit +import CommonCrypto + +extension String: KingfisherCompatibleValue { } +extension KingfisherWrapper where Base == String { + var sha256: String { + guard let data = base.data(using: .utf8) else { return base } + if #available(iOS 13.0, tvOS 13.0, macOS 10.15, watchOS 6.0, macCatalyst 13.0, *) { + let hashed = SHA256.hash(data: data) + return hashed.compactMap { String(format: "%02x", $0) }.joined() + } else { + var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) + data.withUnsafeBytes { bytes in + _ = CC_SHA256(bytes.baseAddress, UInt32(data.count), &digest) + } + return digest.makeIterator().compactMap { String(format: "%02x", $0) }.joined() + } + } + + var ext: String? { + var ext = "" + if let index = base.lastIndex(of: ".") { + let extRange = base.index(index, offsetBy: 1).. 0 ? String(firstSeg) : nil + } +} diff --git a/Tests/KingfisherTests/DiskStorageTests.swift b/Tests/KingfisherTests/DiskStorageTests.swift index 21995f266..1a2e7eb38 100644 --- a/Tests/KingfisherTests/DiskStorageTests.swift +++ b/Tests/KingfisherTests/DiskStorageTests.swift @@ -185,8 +185,8 @@ class DiskStorageTests: XCTestCase { storage.config.usesHashedFileName = true let hashedFileName = storage.cacheFileName(forKey: key) XCTAssertNotEqual(hashedFileName, key) - // validation md5 hash of the key - XCTAssertEqual(hashedFileName, key.kf.md5) + // validation sha256 hash of the key + XCTAssertEqual(hashedFileName, key.kf.sha256) // fileName without hash storage.config.usesHashedFileName = false @@ -202,8 +202,8 @@ class DiskStorageTests: XCTestCase { storage.config.autoExtAfterHashedFileName = true let hashedFileName = storage.cacheFileName(forKey: key) XCTAssertNotEqual(hashedFileName, key) - // validation md5 hash of the key - XCTAssertEqual(hashedFileName, key.kf.md5 + ".gif") + // validation sha256 hash of the key + XCTAssertEqual(hashedFileName, key.kf.sha256 + ".gif") // fileName without hash storage.config.usesHashedFileName = false @@ -220,8 +220,8 @@ class DiskStorageTests: XCTestCase { storage.config.autoExtAfterHashedFileName = true let hashedFileName = storage.cacheFileName(forKey: key) XCTAssertNotEqual(hashedFileName, key) - // validation md5 hash of the key - XCTAssertEqual(hashedFileName, key.kf.md5 + ".jpeg") + // validation sha256 hash of the key + XCTAssertEqual(hashedFileName, key.kf.sha256 + ".jpeg") // fileName without hash storage.config.usesHashedFileName = false diff --git a/Tests/KingfisherTests/StringExtensionTests.swift b/Tests/KingfisherTests/StringExtensionTests.swift index e26e14d27..d1948d050 100644 --- a/Tests/KingfisherTests/StringExtensionTests.swift +++ b/Tests/KingfisherTests/StringExtensionTests.swift @@ -10,8 +10,8 @@ import XCTest @testable import Kingfisher class StringExtensionTests: XCTestCase { - func testStringMD5() { + func testStringSHA256() { let s = "hello" - XCTAssertEqual(s.kf.md5, "5d41402abc4b2a76b9719d911017c592") + XCTAssertEqual(s.kf.sha256, "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824") } }