How to get the checksum of a file in Swift

Sponsored
RevenueCat logo
Develop with RocketSim, Ship with Helm.

Helm Pro yearly subscribers now get a 30% discount on RocketSim thanks to contingent pricing on the App Store.

A checksum is the result of applying an algorithm to a file’s contents that is used to verify the file’s integrity. It is a common practice to use checksums to ensure that files have not been tampered with or corrupted during transfers such as downloads or uploads. You will be familiar with checksums if you have ever distributed a binary artifact through CocoaPods for example.

There are plenty of command-line tools that take care of this task for you but, when I was looking for a way to get the checksum of a file in Swift, I found a mixed bag of solutions that were either not cross-platform or were dated. For this reason, I decided to write a small article showing the solution I came up with.

Using Crypto

To generate a checksum, we first need to apply a cryptographic algorithm to the file’s contents. While you can use CryptoKit on Apple platforms, I would suggest that you use Apple’s Open-Source crypto-swift which has greater compatibility with other platforms and that is what we will be using in this article.

To get started, you will need to add the crypto-swift package to your project. You can do this by adding the following to your Package.swift file:

Package.swift
// swift-tools-version: 6.0

import PackageDescription

let package = Package(
    name: "SwiftChecksum",
    platforms: [
        .macOS(.v13),
        .iOS(.v16)
    ],
    products: [
        .library(
            name: "SwiftChecksum",
            targets: ["SwiftChecksum"]
        ),
    ],
    dependencies: [
        .package(url: "https://github.com/apple/swift-crypto.git", exact: "3.5.2")
    ],
    targets: [
        .target(
            name: "SwiftChecksum",
            dependencies: [
                .product(name: "Crypto", package: "swift-crypto")
            ]
        ),
        .testTarget(
            name: "SwiftChecksumTests",
            dependencies: ["SwiftChecksum"],
            resources: [
                .process("Resources/test.png")
            ]
        ),
    ]
)

Or to your target in Xcode:

Generating the checksum

While checksums can come in different shapes and forms, the most common format is a hexadecimal string generated with one of the following algorithms:

  • MD5
  • SHA1
  • SHA256
  • SHA512

The extension I wrote allows users to generate checksums with any of the algorithms and its output is always a hexadecimal string:

URL+Checksum.swift
import Foundation
import Crypto

public enum ChecksumAlgorithm: Sendable {
    case md5
    case sha1
    case sha256
    case sha512
}

public enum ChecksumError: Error {
    case notAFile
    case illegibleData
}

public extension URL {
    func checksum(with algorithm: ChecksumAlgorithm) throws(ChecksumError) -> String {
        // 1
        let fileManager = FileManager.default
        var isDirectory: ObjCBool = false
        guard fileManager.fileExists(atPath: self.path(), isDirectory: &isDirectory),
              !isDirectory.boolValue else {
            throw .notAFile
        }
        
        do {
            // 2
            let data = try Data(contentsOf: self)
            let bytes = data.digest(with: algorithm)
            
            // 3
            return bytes.map { String(format: "%02x", $0) }.joined()
        } catch {
            throw .illegibleData
        }
    }
}

fileprivate extension Data {
    func digest(with algorithm: ChecksumAlgorithm) -> [UInt8] {
        switch algorithm {
        case .md5: return Array(Crypto.Insecure.MD5.hash(data: self).makeIterator())
        case .sha1: return Array(Crypto.Insecure.SHA1.hash(data: self).makeIterator())
        case .sha256: return Array(Crypto.SHA256.hash(data: self).makeIterator())
        case .sha512: return Array(Crypto.SHA512.hash(data: self).makeIterator())
        }
    }
}

A lot is going on in the code above, so let’s break it down:

  1. We first make sure that the URL provided is a file and not a directory and that it exists. If it is not a file or does not exist, we throw a ChecksumError.notAFile error.
  2. We then read the contents of the file into a Data object and generate the checksum using the digest(with:) method. If the data is illegible, we throw a ChecksumError.illegibleData error.
  3. Finally, we convert the bytes generated by the digest(with:) method into a hexadecimal string and return it.

Testing the extension

Now that we have the extension in place, we need to make sure that it works as expected. To do this, we can write some unit tests using Apple’s new Testing framework and compare the results of the checksums generated with the expected values generated by the shasum and md5 command-line tools:

SwiftChecksumTests.swift
import Testing
import Foundation
@testable import SwiftChecksum

@Suite
struct SwiftChecksumTests {
    @Test(arguments: [
        (ChecksumAlgorithm.md5, "fa651a17ee6b10d0e342d7a057dd0093"),
        (ChecksumAlgorithm.sha1, "bd437ae4f5dad1864a5132c535ebcc31e52aca70"),
        (ChecksumAlgorithm.sha256, "fe96ce8c4c358f17ee9ac31265505c87cc2e1415fa97ad3b012e3326e9ca553e"),
        (ChecksumAlgorithm.sha512, "e83012be3d252d85d05f2b2f264a886220adb0aa60c32412028993f699bf6d12999b27aed34cb0ecb9b095835ca073836226cb7b965193129319b345a4cdec99")
    ])
    func checksumIsGeneratedAsExpected(algorithm: ChecksumAlgorithm, expectedResult: String) throws {
        let file = try #require(Bundle.module.path(forResource: "test", ofType: ".png"))
        let url = URL(filePath: file)
        
        let checksum = try url.checksum(with: algorithm)
        
        #expect(expectedResult == checksum)
    }
    
    @Test
    func errorsIsThrownIfURLThatIsNotAFileIsProvided() throws {
        let directory = try #require(Bundle.module.resourcePath)
        let url = URL(string: directory)!
        
        #expect(throws: ChecksumError.notAFile, performing: {
            _ = try url.checksum(with: .md5)
        })
    }
}