Skip to content

Swift Package for the Kotlin Multiplatform library implementing the W3C VC Data Model

Notifications You must be signed in to change notification settings

a-sit-plus/swift-package-kmm-vc-library

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 

Repository files navigation

KMM VC Library Swift Package

This Kotlin Mulitplatform library implements the W3C VC Data Model to support several use cases of verifiable credentials, verifiable presentations, and validation thereof. This library may be shared between Wallet Apps, Verifier Apps and a Backend Service issuing credentials.

This repository is a Swift Package (see documentation) to wrap the kmm-vc-library.

The Package.swift contains a remote binary target, which is the framework zipped attached to a Github release, in the form of https://github.com/a-sit-plus/kmm-vc-library/releases/download/3.0.0/VcLibKMM-release.xcframework.zip.

We'll recommend a few extensions in Swift to make using Kotlin code easier:

import VcLibKMM

// to avoid nameclash with CyptoKit.Digest when importing both
// CryptoKit, and VcLibKMM (in other files)
public typealias VcLibKMMDigest = Digest

extension Data {
    public var kotlinByteArray : KotlinByteArray {
        let bytes = self.bytes
        let kotlinByteArray = KotlinByteArray(size: Int32(self.count))
        for index in 0..<bytes.count {
            kotlinByteArray.set(index: Int32(index), value: bytes[index])
        }
        return kotlinByteArray
    }

    var bytes: [Int8] {
        return self.map { Int8(bitPattern: $0)}
    }
}

extension Int8 {
    var kotlinByte : KotlinByte {
        return KotlinByte(value: self)
    }
}

extension KotlinByteArray {
    public var data : Data {
        var bytes = [UInt8]()
        for index in 0..<self.size {
            bytes.append(UInt8(bitPattern: self.get(index: index)))
        }
        return Data(bytes)
    }
}

func KmmResultFailure<T>(_ error: KotlinThrowable) -> KmmResult<T> where T: AnyObject {
    return KmmResult(failure: error) as! KmmResult<T>
}

func KmmResultSuccess<T>(_ value: T) -> KmmResult<T> where T: AnyObject {
    return KmmResult(value: value) as! KmmResult<T>
}

The DefaultCryptoService for iOS should not be used in production as it does not implement encryption, decryption, key agreement and message digests correctly.

A more correct implementation in Swift, using Apple CryptoKit would be:

import Foundation
import CryptoKit

// KeyChainService.loadPrivateKey() provides a SecureEnclave.P256.Signing.PrivateKey?

public class VcLibCryptoServiceCryptoKit: CryptoService {

    public var identifier: String
    public var jwsAlgorithm: JwsAlgorithm
    public var coseAlgorithm: CoseAlgorithm
    public var certificate: KotlinByteArray?
    private let cryptoPublicKey: CryptoPublicKey
    private let keyChainService: KeyChainService

    public init?(keyChainService: KeyChainService) {
        guard let privateKey = keyChainService.loadPrivateKey() else {
            return nil
        }
        self.keyChainService = keyChainService
        self.cryptoPublicKey = CryptoPublicKey.Ec.companion.fromAnsiX963Bytes(curve: .secp256R1, it: privateKey.publicKey.x963Representation.kotlinByteArray)!
        self.identifier = cryptoPublicKey.toJsonWebKey().identifier
        self.jwsAlgorithm = .es256
        self.coseAlgorithm = .es256
        self.certificate = nil
    }

    public func decrypt(key: KotlinByteArray, iv: KotlinByteArray, aad: KotlinByteArray, input: KotlinByteArray, authTag: KotlinByteArray, algorithm: JweEncryption) async throws -> KmmResult<KotlinByteArray> {
        switch algorithm {
        case .a256gcm:
            let key = SymmetricKey(data: key.data)
            guard let nonce = try? AES.GCM.Nonce(data: iv.data),
                  let sealedBox = try? AES.GCM.SealedBox(nonce: nonce, ciphertext: input.data, tag: authTag.data),
                  let decryptedData = try? AES.GCM.open(sealedBox, using: key, authenticating: aad.data) else {
                return KmmResultFailure(KotlinThrowable(message: "Error in AES.GCM.open"))
            }
            return KmmResultSuccess(decryptedData.kotlinByteArray)
        default:
            return KmmResultFailure(KotlinThrowable(message: "Algorithm unknown \(algorithm)"))
        }
    }

    public func encrypt(key: KotlinByteArray, iv: KotlinByteArray, aad: KotlinByteArray, input: KotlinByteArray, algorithm: JweEncryption) -> KmmResult<AuthenticatedCiphertext> {
        switch algorithm {
        case .a256gcm:
            let key = SymmetricKey(data: key.data)
            guard let nonce = try? AES.GCM.Nonce(data: iv.data),
                  let encryptedData = try? AES.GCM.seal(input.data, using: key, nonce: nonce, authenticating: aad.data) else {
                return KmmResultFailure(KotlinThrowable(message: "Error in AES.GCM.seal"))
            }
            let ac = AuthenticatedCiphertext(ciphertext: encryptedData.ciphertext.kotlinByteArray, authtag: encryptedData.tag.kotlinByteArray)
            return KmmResultSuccess(ac)
        default:
            return KmmResultFailure(KotlinThrowable(message: "Algorithm unknown \(algorithm)"))
        }
    }

    public func generateEphemeralKeyPair(ecCurve: EcCurve) -> KmmResult<EphemeralKeyHolder> {
        switch ecCurve {
        case .secp256R1:
            return KmmResultSuccess(VcLibEphemeralKeyHolder())
        default:
            return KmmResultFailure(KotlinThrowable(message: "ecCurve unknown \(ecCurve)"))
        }
    }

    public func messageDigest(input: KotlinByteArray, digest: VcLibKMMDigest) -> KmmResult<KotlinByteArray> {
        switch digest {
        case .sha256:
            let digest = SHA256.hash(data: input.data)
            let data = Data(digest.compactMap { $0 })
            return KmmResultSuccess(data.kotlinByteArray)
        default:
            return KmmResultFailure(KotlinThrowable(message: "Digest unknown \(digest)"))
        }
    }

    public func performKeyAgreement(ephemeralKey: EphemeralKeyHolder, recipientKey: JsonWebKey, algorithm: JweAlgorithm) -> KmmResult<KotlinByteArray> {
        switch algorithm {
        case .ecdhEs:
            let recipientKeyBytes = recipientKey.toAnsiX963ByteArray()
            if let throwable = recipientKeyBytes.exceptionOrNull() {
                return KmmResultFailure(throwable)
            }
            guard let recipientKeyBytesValue = recipientKeyBytes.getOrNull(),
                  let recipientKey = try? P256.KeyAgreement.PublicKey(x963Representation: recipientKeyBytesValue.data),
                  let ephemeralKey = ephemeralKey as? VcLibEphemeralKeyHolder,
                  let sharedSecret = try? ephemeralKey.privateKey.sharedSecretFromKeyAgreement(with: recipientKey) else {
                return KmmResultFailure(KotlinThrowable(message: "Error in KeyAgreement"))
            }
            let data = sharedSecret.withUnsafeBytes {
                return Data(Array($0))
            }
            return KmmResultSuccess(data.kotlinByteArray)
        default:
            return KmmResultFailure(KotlinThrowable(message: "Algorithm unknown \(algorithm)"))
        }
    }

    public func performKeyAgreement(ephemeralKey: JsonWebKey, algorithm: JweAlgorithm) -> KmmResult<KotlinByteArray> {
        switch algorithm {
        case .ecdhEs:
            guard let privateKey = keyChainService.loadPrivateKey() else {
                return KmmResultFailure(KotlinThrowable(message: "Could not load private key"))
            }
            let ephemeralKeyBytes = ephemeralKey.toAnsiX963ByteArray()
            if let throwable = ephemeralKeyBytes.exceptionOrNull() {
                return KmmResultFailure(throwable)
            }
            guard let recipientKeyBytesValue = ephemeralKeyBytes.getOrNull(),
                  let recipientKey = try? P256.KeyAgreement.PublicKey(x963Representation: recipientKeyBytesValue.data),
                  let privateAgreementKey = try? SecureEnclave.P256.KeyAgreement.PrivateKey(dataRepresentation: privateKey.dataRepresentation),
                  let sharedSecret = try? privateAgreementKey.sharedSecretFromKeyAgreement(with: recipientKey) else {
                return KmmResultFailure(KotlinThrowable(message: "Error in KeyAgreement"))
            }
            let data = sharedSecret.withUnsafeBytes {
                return Data($0)
            }
            return KmmResultSuccess(data.kotlinByteArray)
        default:
            return KmmResultFailure(KotlinThrowable(message: "Algorithm unknown \(algorithm)"))
        }
    }

    public func sign(input: KotlinByteArray) async throws -> KmmResult<KotlinByteArray> {
        guard let privateKey = keyChainService.loadPrivateKey() else {
            return KmmResultFailure(KotlinThrowable(message: "Could not load private key"))
        }
        guard let signature = try? privateKey.signature(for: input.data) else {
            return KmmResultFailure(KotlinThrowable(message: "Signature error"))
        }
        return KmmResultSuccess(signature.derRepresentation.kotlinByteArray)
    }

    public func toPublicKey() -> CryptoPublicKey {
        return cryptoPublicKey
    }
    
}

public class VcLibVerifierCryptoService : VerifierCryptoService {
    
    public func verify(input: KotlinByteArray, signature: KotlinByteArray, algorithm: JwsAlgorithm, publicKey: CryptoPublicKey) -> KmmResult<KotlinBoolean> {
        if algorithm != .es256 {
            return KmmResultFailure(KotlinThrowable(message: "Can not verify algorithm \(algorithm.name)"))
        }
        if !(publicKey is CryptoPublicKey.Ec) {
            return KmmResultFailure(KotlinThrowable(message: "Public key is not an EC key \(publicKey)"))
        }
        let ansiX963Result = publicKey.toAnsiX963ByteArray()
        if let throwable = ansiX963Result.exceptionOrNull() {
            return KmmResultFailure(throwable)
        }
        guard let publicKeyBytes = ansiX963Result.getOrNull(),
            let cryptoKitPublicKey = try? P256.Signing.PublicKey(x963Representation: publicKeyBytes.data) else {
            return KmmResultFailure(KotlinThrowable(message: "Can not create CryptoKit key")) 
        }
        if let cryptoKitSignature = try? P256.Signing.ECDSASignature(derRepresentation: signature.data) {
            let valid = cryptoKitPublicKey.isValidSignature(cryptoKitSignature, for: input.data)
            return KmmResultSuccess(KotlinBoolean(value: valid))
        } else if let cryptoKitSignature = try? P256.Signing.ECDSASignature(rawRepresentation: signature.data) {
            let valid = cryptoKitPublicKey.isValidSignature(cryptoKitSignature, for: input.data)
            return KmmResultSuccess(KotlinBoolean(value: valid))
        } else {
            return KmmResultFailure(KotlinThrowable(message: "Can not read signature"))
        }
    }
    
    public func extractPublicKeyFromX509Cert(it: KotlinByteArray) -> JsonWebKey? {
        guard let certificate = SecCertificateCreateWithData(nil, it.data as CFData),
              let publicKey = SecCertificateCopyKey(certificate),
              let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, nil) as? Data else {
            return nil
        }
        return JsonWebKey.companion.fromAnsiX963Bytes(type: .ec, curve: .secp256R1, it: publicKeyData.kotlinByteArray)
    }
    
}

public class VcLibEphemeralKeyHolder : EphemeralKeyHolder {
    
    let privateKey: P256.KeyAgreement.PrivateKey
    let publicKey: P256.KeyAgreement.PublicKey
    let jsonWebKey: JsonWebKey
    
    public init() {
        self.privateKey = P256.KeyAgreement.PrivateKey()
        self.publicKey = privateKey.publicKey
        self.jsonWebKey = JsonWebKey.companion.fromAnsiX963Bytes(type: .ec, curve: .secp256R1, it: publicKey.x963Representation.kotlinByteArray)!
    }
    
    public func toPublicJsonWebKey() -> JsonWebKey {
        return jsonWebKey
    }
    
}

An example for the implementation of loadPrivateKey() would be:

extension String: Error {}

    public func loadPrivateKey() -> SecureEnclave.P256.Signing.PrivateKey throws {
        let flags: SecAccessControlCreateFlags = [.privateKeyUsage]
        var error: Unmanaged<CFError>?
        guard let access = SecAccessControlCreateWithFlags(kCFAllocatorDefault, kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly, flags, &error) else {
            throw "cannot create key access flags"
        }
        guard let privateKey = try? SecureEnclave.P256.Signing.PrivateKey(compactRepresentable: true, accessControl: access, authenticationContext: nil) else {
            throw "Can not create SecureEnclave key"
        }
        // SecureEnclave keys from CryptoKit shall be stored as "passwords"
        // (their data representation is an encrypted blob)
        let query = [kSecClass: kSecClassGenericPassword,
                     kSecAttrAccessible: kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
                     kSecUseDataProtectionKeychain: true,
                     kSecAttrLabel: RealKeyChainService.KEY_PAIR_ALIAS,
                     kSecAttrAccount: RealKeyChainService.ACCOUNT,
                     kSecValueData: privateKey.dataRepresentation] as [String: Any]

        let status = SecItemAdd(query as CFDictionary, nil)
        guard status == errSecSuccess else {
            throw "unable to store item: \(status)"
        }
        return privateKey
    }

About

Swift Package for the Kotlin Multiplatform library implementing the W3C VC Data Model

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages