diff --git a/Sources/CryptoExtras/CMakeLists.txt b/Sources/CryptoExtras/CMakeLists.txt index 8498dc443..e08107032 100644 --- a/Sources/CryptoExtras/CMakeLists.txt +++ b/Sources/CryptoExtras/CMakeLists.txt @@ -33,6 +33,7 @@ add_library(CryptoExtras "ARC/ARCServer.swift" "ChaCha20CTR/BoringSSL/ChaCha20CTR_boring.swift" "ChaCha20CTR/ChaCha20CTR.swift" + "EC/Curve25519+PEM.swift" "EC/ObjectIdentifier.swift" "EC/PKCS8DERRepresentation.swift" "EC/PKCS8PrivateKey.swift" diff --git a/Sources/CryptoExtras/EC/Curve25519+PEM.swift b/Sources/CryptoExtras/EC/Curve25519+PEM.swift new file mode 100644 index 000000000..5e8071c83 --- /dev/null +++ b/Sources/CryptoExtras/EC/Curve25519+PEM.swift @@ -0,0 +1,180 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2025 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 Foundation +import SwiftASN1 + +@available(iOS 14.0, macOS 11.0, watchOS 7.0, tvOS 14.0, *) +extension Curve25519.Signing.PrivateKey { + /// A Distinguished Encoding Rules (DER) encoded representation of the private key. + public var derRepresentation: Data { + let pkey = ASN1.PKCS8PrivateKey(algorithm: .ed25519, privateKey: Array(self.rawRepresentation)) + var serializer = DER.Serializer() + + try! serializer.serialize(pkey) + return Data(serializer.serializedBytes) + } + + /// A Privacy-Enhanced Mail (PEM) representation of the private key. + public var pemRepresentation: String { + let pemDocument = ASN1.PEMDocument(type: "PRIVATE KEY", derBytes: self.derRepresentation) + return pemDocument.pemString + } + + /// Creates a Curve25519 private key for signing from a Privacy-Enhanced Mail + /// (PEM) representation. + /// + /// - Parameters: + /// - pemRepresentation: A PEM representation of the key. + public init(pemRepresentation: String) throws { + let document = try ASN1.PEMDocument(pemString: pemRepresentation) + self = try .init(derRepresentation: document.derBytes) + } + + /// Creates a Curve25519 private key for signing from a Distinguished Encoding + /// Rules (DER) encoded representation. + /// + /// - Parameters: + /// - derRepresentation: A DER-encoded representation of the key. + public init(derRepresentation: Bytes) throws where Bytes.Element == UInt8 { + let bytes = Array(derRepresentation) + let key = try ASN1.PKCS8PrivateKey(derEncoded: bytes) + self = try .init(rawRepresentation: key.privateKey.bytes) + } +} + +@available(iOS 14.0, macOS 11.0, watchOS 7.0, tvOS 14.0, *) +extension Curve25519.Signing.PublicKey { + /// A Distinguished Encoding Rules (DER) encoded representation of the public key. + public var derRepresentation: Data { + let spki = SubjectPublicKeyInfo(algorithmIdentifier: .ed25519, key: Array(self.rawRepresentation)) + var serializer = DER.Serializer() + + try! serializer.serialize(spki) + return Data(serializer.serializedBytes) + } + + /// A Privacy-Enhanced Mail (PEM) representation of the public key. + public var pemRepresentation: String { + let pemDocument = ASN1.PEMDocument(type: "PUBLIC KEY", derBytes: self.derRepresentation) + return pemDocument.pemString + } + + /// Creates a Curve25519 public key for signing from a Privacy-Enhanced Mail + /// (PEM) representation. + /// + /// - Parameters: + /// - pemRepresentation: A PEM representation of the key. + public init(pemRepresentation: String) throws { + let document = try ASN1.PEMDocument(pemString: pemRepresentation) + self = try .init(derRepresentation: document.derBytes) + } + + /// Creates a Curve25519 public key for signing from a Distinguished Encoding + /// Rules (DER) encoded representation. + /// + /// - Parameters: + /// - derRepresentation: A DER-encoded representation of the key. + public init(derRepresentation: Bytes) throws where Bytes.Element == UInt8 { + let bytes = Array(derRepresentation) + let spki = try SubjectPublicKeyInfo(derEncoded: bytes) + guard spki.algorithmIdentifier == .ed25519 else { + throw CryptoKitASN1Error.invalidASN1Object + } + self = try .init(rawRepresentation: spki.key.bytes) + } +} + +@available(iOS 14.0, macOS 11.0, watchOS 7.0, tvOS 14.0, *) +extension Curve25519.KeyAgreement.PrivateKey { + /// A Distinguished Encoding Rules (DER) encoded representation of the private key. + public var derRepresentation: Data { + let pkey = ASN1.PKCS8PrivateKey(algorithm: .x25519, privateKey: Array(self.rawRepresentation)) + var serializer = DER.Serializer() + + // Serializing this key can't throw + try! serializer.serialize(pkey) + return Data(serializer.serializedBytes) + } + + /// A Privacy-Enhanced Mail (PEM) representation of the private key. + public var pemRepresentation: String { + let pemDocument = ASN1.PEMDocument(type: "PRIVATE KEY", derBytes: self.derRepresentation) + return pemDocument.pemString + } + + /// Creates a Curve25519 private key for key agreement from a Privacy-Enhanced Mail + /// (PEM) representation. + /// + /// - Parameters: + /// - pemRepresentation: A PEM representation of the key. + public init(pemRepresentation: String) throws { + let document = try ASN1.PEMDocument(pemString: pemRepresentation) + self = try .init(derRepresentation: document.derBytes) + } + + /// Creates a Curve25519 private key for key agreement from a Distinguished Encoding + /// Rules (DER) encoded representation. + /// + /// - Parameters: + /// - derRepresentation: A DER-encoded representation of the key. + public init(derRepresentation: Bytes) throws where Bytes.Element == UInt8 { + let bytes = Array(derRepresentation) + let key = try ASN1.PKCS8PrivateKey(derEncoded: bytes) + self = try .init(rawRepresentation: key.privateKey.bytes) + } +} + +@available(iOS 14.0, macOS 11.0, watchOS 7.0, tvOS 14.0, *) +extension Curve25519.KeyAgreement.PublicKey { + /// A Distinguished Encoding Rules (DER) encoded representation of the public key. + public var derRepresentation: Data { + let spki = SubjectPublicKeyInfo(algorithmIdentifier: .x25519, key: Array(self.rawRepresentation)) + var serializer = DER.Serializer() + + try! serializer.serialize(spki) + return Data(serializer.serializedBytes) + } + + /// A Privacy-Enhanced Mail (PEM) representation of the public key. + public var pemRepresentation: String { + let pemDocument = ASN1.PEMDocument(type: "PUBLIC KEY", derBytes: self.derRepresentation) + return pemDocument.pemString + } + + /// Creates a Curve25519 public key for key agreement from a Privacy-Enhanced Mail + /// (PEM) representation. + /// + /// - Parameters: + /// - pemRepresentation: A PEM representation of the key. + public init(pemRepresentation: String) throws { + let document = try ASN1.PEMDocument(pemString: pemRepresentation) + self = try .init(derRepresentation: document.derBytes) + } + + /// Creates a Curve25519 public key for key agreement from a Distinguished Encoding + /// Rules (DER) encoded representation. + /// + /// - Parameters: + /// - derRepresentation: A DER-encoded representation of the key. + public init(derRepresentation: Bytes) throws where Bytes.Element == UInt8 { + let bytes = Array(derRepresentation) + let spki = try SubjectPublicKeyInfo(derEncoded: bytes) + guard spki.algorithmIdentifier == .x25519 else { + throw CryptoKitASN1Error.invalidASN1Object + } + self = try .init(rawRepresentation: spki.key.bytes) + } +} diff --git a/Sources/CryptoExtras/EC/PKCS8DERRepresentation.swift b/Sources/CryptoExtras/EC/PKCS8DERRepresentation.swift index a3449ca64..bfaf11f44 100644 --- a/Sources/CryptoExtras/EC/PKCS8DERRepresentation.swift +++ b/Sources/CryptoExtras/EC/PKCS8DERRepresentation.swift @@ -20,12 +20,7 @@ import SwiftASN1 extension Curve25519.Signing.PrivateKey { /// A Distinguished Encoding Rules (DER) encoded representation of the private key in PKCS#8 format. public var pkcs8DERRepresentation: Data { - let pkey = ASN1.PKCS8PrivateKey(algorithm: .ed25519, privateKey: Array(self.rawRepresentation)) - var serializer = DER.Serializer() - - // Serializing this key can't throw - try! serializer.serialize(pkey) - return Data(serializer.serializedBytes) + self.derRepresentation } } @@ -33,12 +28,7 @@ extension Curve25519.Signing.PrivateKey { extension Curve25519.KeyAgreement.PrivateKey { /// A Distinguished Encoding Rules (DER) encoded representation of the private key in PKCS#8 format. public var pkcs8DERRepresentation: Data { - let pkey = ASN1.PKCS8PrivateKey(algorithm: .x25519, privateKey: Array(self.rawRepresentation)) - var serializer = DER.Serializer() - - // Serializing this key can't throw - try! serializer.serialize(pkey) - return Data(serializer.serializedBytes) + self.derRepresentation } } diff --git a/Sources/CryptoExtras/Util/SubjectPublicKeyInfo.swift b/Sources/CryptoExtras/Util/SubjectPublicKeyInfo.swift index 40e778cca..312de34d1 100644 --- a/Sources/CryptoExtras/Util/SubjectPublicKeyInfo.swift +++ b/Sources/CryptoExtras/Util/SubjectPublicKeyInfo.swift @@ -125,3 +125,10 @@ extension SubjectPublicKeyInfo { return serializer.serializedBytes } } + +@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, visionOS 1.0, *) +extension RFC5480AlgorithmIdentifier { + static let ed25519 = RFC5480AlgorithmIdentifier(algorithm: .AlgorithmIdentifier.idEd25519, parameters: nil) + + static let x25519 = RFC5480AlgorithmIdentifier(algorithm: .AlgorithmIdentifier.idX25519, parameters: nil) +} diff --git a/Tests/CryptoExtrasTests/Curve25519DERTests.swift b/Tests/CryptoExtrasTests/Curve25519DERTests.swift new file mode 100644 index 000000000..084a6c732 --- /dev/null +++ b/Tests/CryptoExtrasTests/Curve25519DERTests.swift @@ -0,0 +1,91 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2025 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 CryptoExtras +import XCTest + +final class Curve25519DERTests: XCTestCase { + func testSigningPrivateKeyDERRoundTrip() throws { + let privateKey = Curve25519.Signing.PrivateKey() + + let der = privateKey.derRepresentation + let imported = try Curve25519.Signing.PrivateKey(derRepresentation: der) + + XCTAssertEqual(imported.rawRepresentation, privateKey.rawRepresentation) + } + + func testSigningPublicKeyDERRoundTrip() throws { + let privateKey = Curve25519.Signing.PrivateKey() + let publicKey = privateKey.publicKey + + let der = publicKey.derRepresentation + let imported = try Curve25519.Signing.PublicKey(derRepresentation: der) + + XCTAssertEqual(imported.rawRepresentation, publicKey.rawRepresentation) + } + + func testKeyAgreementPrivateKeyDERRoundTrip() throws { + let privateKey = Curve25519.KeyAgreement.PrivateKey() + + let der = privateKey.derRepresentation + let imported = try Curve25519.KeyAgreement.PrivateKey(derRepresentation: der) + + XCTAssertEqual(imported.rawRepresentation, privateKey.rawRepresentation) + } + + func testKeyAgreementPublicKeyDERRoundTrip() throws { + let privateKey = Curve25519.KeyAgreement.PrivateKey() + let publicKey = privateKey.publicKey + + let der = publicKey.derRepresentation + let imported = try Curve25519.KeyAgreement.PublicKey(derRepresentation: der) + + XCTAssertEqual(imported.rawRepresentation, publicKey.rawRepresentation) + } + + func testInvalidDERThrows() throws { + let invalidDER: [UInt8] = [0x01, 0x02, 0x03] + + XCTAssertThrowsError(try Curve25519.Signing.PrivateKey(derRepresentation: invalidDER)) + XCTAssertThrowsError(try Curve25519.Signing.PublicKey(derRepresentation: invalidDER)) + XCTAssertThrowsError(try Curve25519.KeyAgreement.PrivateKey(derRepresentation: invalidDER)) + XCTAssertThrowsError(try Curve25519.KeyAgreement.PublicKey(derRepresentation: invalidDER)) + } + + func testImportOpenSSLSigningPrivateKeyDER() throws { + // DER extracted from an OpenSSL-generated Ed25519 PKCS#8 PEM + let derBytes: [UInt8] = [ + 0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, + 0x04, 0x22, 0x04, 0x20, 0x3f, 0x10, 0x52, 0x03, 0xb4, 0x06, 0x87, 0x0c, + 0x51, 0x71, 0xfa, 0xdc, 0x8f, 0x96, 0xbd, 0x2a, 0x5f, 0x42, 0xac, 0x5c, + 0xb9, 0x5b, 0x27, 0x4e, 0xf0, 0x06, 0xe5, 0x61, 0x6a, 0x12, 0x00, 0xa5, + ] + + let key = try Curve25519.Signing.PrivateKey(derRepresentation: derBytes) + XCTAssertEqual(key.rawRepresentation.count, 32) + } + + func testImportOpenSSLKeyAgreementPrivateKeyDER() throws { + // DER extracted from an OpenSSL-generated X25519 PKCS#8 PEM + let derBytes: [UInt8] = [ + 0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x6e, + 0x04, 0x22, 0x04, 0x20, 0xb8, 0x38, 0x3f, 0x28, 0xea, 0x8f, 0x1d, 0x71, + 0x49, 0xa2, 0xa3, 0x91, 0x37, 0x00, 0xa1, 0x0c, 0x7c, 0x9d, 0xa9, 0x59, + 0x28, 0x2d, 0x14, 0x7e, 0x9b, 0x1e, 0x1b, 0x8c, 0x04, 0xa5, 0xd8, 0x47, + ] + + let key = try Curve25519.KeyAgreement.PrivateKey(derRepresentation: derBytes) + XCTAssertEqual(key.rawRepresentation.count, 32) + } +} diff --git a/Tests/CryptoExtrasTests/Curve25519PEMTests.swift b/Tests/CryptoExtrasTests/Curve25519PEMTests.swift new file mode 100644 index 000000000..02cf6d922 --- /dev/null +++ b/Tests/CryptoExtrasTests/Curve25519PEMTests.swift @@ -0,0 +1,140 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftCrypto open source project +// +// Copyright (c) 2025 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 CryptoExtras +import XCTest + +final class Curve25519PEMTests: XCTestCase { + func testSigningPrivateKeyPEMRoundTrip() throws { + let privateKey = Curve25519.Signing.PrivateKey() + let imported = try Curve25519.Signing.PrivateKey(pemRepresentation: privateKey.pemRepresentation) + XCTAssertEqual(imported.rawRepresentation, privateKey.rawRepresentation) + } + + func testSigningPublicKeyPEMRoundTrip() throws { + let privateKey = Curve25519.Signing.PrivateKey() + let publicKey = privateKey.publicKey + let imported = try Curve25519.Signing.PublicKey(pemRepresentation: publicKey.pemRepresentation) + XCTAssertEqual(imported.rawRepresentation, publicKey.rawRepresentation) + } + + func testKeyAgreementPrivateKeyPEMRoundTrip() throws { + let privateKey = Curve25519.KeyAgreement.PrivateKey() + let imported = try Curve25519.KeyAgreement.PrivateKey(pemRepresentation: privateKey.pemRepresentation) + XCTAssertEqual(imported.rawRepresentation, privateKey.rawRepresentation) + } + + func testKeyAgreementPublicKeyPEMRoundTrip() throws { + let privateKey = Curve25519.KeyAgreement.PrivateKey() + let publicKey = privateKey.publicKey + let imported = try Curve25519.KeyAgreement.PublicKey(pemRepresentation: publicKey.pemRepresentation) + XCTAssertEqual(imported.rawRepresentation, publicKey.rawRepresentation) + } + + func testImportOpenSSLSigningPrivateKeyPEM() throws { + // Generated via `openssl genpkey -algorithm Ed25519 -outform PEM` + let pem = """ + -----BEGIN PRIVATE KEY----- + MC4CAQAwBQYDK2VwBCIEIIHEchKSA2A/z1r4tVHJ9f+yS6YXhUbBdHJiryZnRqnD + -----END PRIVATE KEY----- + """ + let key = try Curve25519.Signing.PrivateKey(pemRepresentation: pem) + XCTAssertEqual(key.rawRepresentation.count, 32) + } + + func testImportOpenSSLKeyAgreementPrivateKeyPEM() throws { + // Generated via `openssl genpkey -algorithm X25519 -outform PEM` + let pem = """ + -----BEGIN PRIVATE KEY----- + MC4CAQAwBQYDK2VuBCIEIMDJoDr+3A91QpL8BLIeCKSAKI5T7frtzEaE5HzugtR1 + -----END PRIVATE KEY----- + """ + let key = try Curve25519.KeyAgreement.PrivateKey(pemRepresentation: pem) + XCTAssertEqual(key.rawRepresentation.count, 32) + } + + func testInvalidPEMThrows() { + let invalidPEM = """ + -----BEGIN PRIVATE KEY----- + abc + -----END PRIVATE KEY----- + """ + XCTAssertThrowsError(try Curve25519.Signing.PrivateKey(pemRepresentation: invalidPEM)) + XCTAssertThrowsError(try Curve25519.KeyAgreement.PrivateKey(pemRepresentation: invalidPEM)) + + let invalidPublicPEM = """ + -----BEGIN PUBLIC KEY----- + xyz + -----END PUBLIC KEY----- + """ + XCTAssertThrowsError(try Curve25519.Signing.PublicKey(pemRepresentation: invalidPublicPEM)) + XCTAssertThrowsError(try Curve25519.KeyAgreement.PublicKey(pemRepresentation: invalidPEM)) + } + + func testWrongAlgorithmOIDThrows() throws { + XCTAssertThrowsError(try Curve25519.Signing.PrivateKey(pemRepresentation: rsaPEM)) + XCTAssertThrowsError(try Curve25519.KeyAgreement.PublicKey(pemRepresentation: rsaPEM)) + } + + func testSigningPrivatePublicKeyConsistency() throws { + let privateKey = Curve25519.Signing.PrivateKey() + let publicKey = privateKey.publicKey + + let importedPrivate = try Curve25519.Signing.PrivateKey(pemRepresentation: privateKey.pemRepresentation) + let importedPublic = try Curve25519.Signing.PublicKey(pemRepresentation: publicKey.pemRepresentation) + + XCTAssertEqual(importedPrivate.publicKey.rawRepresentation, importedPublic.rawRepresentation) + } + + func testKeyAgreementPrivatePublicKeyConsistency() throws { + let privateKey = Curve25519.KeyAgreement.PrivateKey() + let publicKey = privateKey.publicKey + + let importedPrivate = try Curve25519.KeyAgreement.PrivateKey(pemRepresentation: privateKey.pemRepresentation) + let importedPublic = try Curve25519.KeyAgreement.PublicKey(pemRepresentation: publicKey.pemRepresentation) + + XCTAssertEqual(importedPrivate.publicKey.rawRepresentation, importedPublic.rawRepresentation) + } + + let rsaPEM = """ + -----BEGIN PRIVATE KEY----- + MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDL8W1D9w5zHpmD + JqpTngIRJ+Sm21e42cRnTudhdejzKUiJQWkHSvQV5yC/+0iEXUsUJYEdSyrKhJFD + PT+IFGdjIiwb7IX+rUreWXlD/YYBL3/byMG4kYoO4oiPp2A+WvfeyLpuN549OXhk + 7o5kXEZjKfjHTfmnAbCMoYW5BEpiHQC3HAeJZ5EiwAn8HZn5UY6lxJcf7H9hR83x + D0W7IZTNyxUu4aLNuihFIxJKgP/L/y95Y6ddZsyyHQopM43/7JOYBwufa07MWaxi + AdBdq1bR/ZeOt2aZaXhV+J6QUoUO8Z6fG6b2cQmvMgk4ybqoeciLII0DfFsyqavu + ip4hRr59AgMBAAECggEAUIw994XwMw922hG/W98gOd5jtHMVJnD73UGQqTGEm+VG + PM+Ux8iWtr/ec3Svo3elW4OkhwlVET9ikAf0u64zVzf769ty4K9YzpDQEEZlUrqL + 6SZVPKxetppKDVKx9G7BT0BAQZ+947h7EIIXwxOeyTOeijkFzSwhqqlwwy4qoqzV + FTQS20QHE62hxzwuS5HBqw8ds183qAg9NbzR0Cp4za9qTiBB6C8KEcLqeatO+q+d + VCDsJcAMZOvW14N6BozKgbQ/WXZQ/3kNUPBndZLzzqaILFNmB1Zf2DVVJ9gU7+EK + xOac60StIfG81NllCTBrmRVq8yitNqwmutHMlxrIkQKBgQDvp39MkEHtNunFGkI5 + R8IB5BZjtx5OdRBKkmPasmNU8U0XoQAJUKY/9piIpCtRi87tMXv8WWmlbULi66pu + 4BnMIisw78xlIWRZTSizFrkFcEoVgEnbZBtSrOg/J5PAcjLEGCQoAdmMXAekR2/m + htv7FPijHPNUjyIFLaxwjl9izwKBgQDZ2mQeKNRHjIb5ZBzB0ZCvUy2y4+kaLrhZ + +CWMN1flL4dd1KuZKvCEfHY9kWOjqw6XneN4yT0aPmbBft4fihiiNW0Sm8i+fSpy + g0klw2HJl49wnwctBpRgTdMKGo9n14OGeu0xKOAy7I4j1tKrUXiRWnP9R583Ti7c + w7YHgdHM8wKBgEV147SaPzF08A6bzMPzY2zO4hpmsdcFoQIsKdryR04QXkrR9EO+ + 52C0pYM9Kf0Jq6Ed7ZS3iaJT58YDjjNyqqd648/cQP6yzfYAIiK+HERSRnay5zU6 + b5zn1qyvWOi3cLVbVedumdJPvjtEJU/ImKvOaT5FntVMYwzjLw60hTsLAoGAZJnt + UeAY51GFovUQMpDL96q5l7qXknewuhtVe4KzHCrun+3tsDWcDBJNp/DTymjbvDg1 + KzoC9XOLkB8+A+KJrZ5uWAGImi7Cw07NIJsxNR7AJonJjolTS4Wkxy2su49SNW/e + yKzPm7SRjwtNDb/5pWXX2kaQx8Fa8qeOD7lrYPECgYAwQ6o0vYmr+L1tOZZgMVv9 + Jusa8beVUH5hyduJjmxbYOtFTkggAozdx7rs4BgyRsmDlV48cEmcVf/7IH4gMJLb + O+bbERwCYUChe+piANhnwfwDHzbRd8mmQus54P06X7bWu6Rmi7gbQGVN/Z6VhbIm + D2cOo0w4bk/3yb01xz1MEw== + -----END PRIVATE KEY----- + """ +}