From ee44e775b0d8ac8e608b93d6978656ead48af64f Mon Sep 17 00:00:00 2001 From: Tanner Date: Wed, 29 Jul 2020 14:06:26 -0400 Subject: [PATCH] Support JWKs with no alg (#34) * jwks without algorithm * make jwksigner private * add msft jwks test --- Sources/JWTKit/Keys/JWK.swift | 26 ++++-- Sources/JWTKit/Signing/JWKSigner.swift | 42 +++++++++ Sources/JWTKit/Signing/JWTSigner+JWK.swift | 64 -------------- Sources/JWTKit/Signing/JWTSigner.swift | 3 +- Sources/JWTKit/Signing/JWTSigners.swift | 44 ++++++++-- Tests/JWTKitTests/JWTKitTests.swift | 99 ++++++++++++++++++++-- 6 files changed, 193 insertions(+), 85 deletions(-) create mode 100644 Sources/JWTKit/Signing/JWKSigner.swift delete mode 100644 Sources/JWTKit/Signing/JWTSigner+JWK.swift diff --git a/Sources/JWTKit/Keys/JWK.swift b/Sources/JWTKit/Keys/JWK.swift index b323a511..cac3327d 100644 --- a/Sources/JWTKit/Keys/JWK.swift +++ b/Sources/JWTKit/Keys/JWK.swift @@ -1,3 +1,6 @@ +import struct Foundation.Data +import class Foundation.JSONDecoder + /// A JSON Web Key. /// /// Read specification (RFC 7517) https://tools.ietf.org/html/rfc7517. @@ -33,12 +36,9 @@ public struct JWK: Decodable { case rs384 /// RSA with SHA512 case rs512 - - /// Decodes from a lowercased string. - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - let value = try container.decode(String.self).lowercased() - switch value { + + init?(string: String) { + switch string.lowercased() { case "rs256": self = .rs256 case "rs384": @@ -46,8 +46,18 @@ public struct JWK: Decodable { case "rs512": self = .rs512 default: + return nil + } + } + + /// Decodes from a lowercased string. + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let string = try container.decode(String.self) + guard let algorithm = Self(string: string) else { throw JWTError.invalidJWK } + self = algorithm } } @@ -87,4 +97,8 @@ public struct JWK: Decodable { case exponent = "e" case privateExponent = "d" } + + public init(json: String) throws { + self = try JSONDecoder().decode(JWK.self, from: Data(json.utf8)) + } } diff --git a/Sources/JWTKit/Signing/JWKSigner.swift b/Sources/JWTKit/Signing/JWKSigner.swift new file mode 100644 index 00000000..6478c4b7 --- /dev/null +++ b/Sources/JWTKit/Signing/JWKSigner.swift @@ -0,0 +1,42 @@ +import Foundation + +struct JWKSigner { + let jwk: JWK + + init(jwk: JWK) { + self.jwk = jwk + } + + func signer(for algorithm: JWK.Algorithm? = nil) -> JWTSigner? { + switch self.jwk.keyType { + case .rsa: + guard let modulus = self.jwk.modulus else { + return nil + } + guard let exponent = self.jwk.exponent else { + return nil + } + + guard let rsaKey = RSAKey( + modulus: modulus, + exponent: exponent, + privateExponent: self.jwk.privateExponent + ) else { + return nil + } + + guard let algorithm = algorithm ?? self.jwk.algorithm else { + return nil + } + + switch algorithm { + case .rs256: + return JWTSigner.rs256(key: rsaKey) + case .rs384: + return JWTSigner.rs384(key: rsaKey) + case .rs512: + return JWTSigner.rs512(key: rsaKey) + } + } + } +} diff --git a/Sources/JWTKit/Signing/JWTSigner+JWK.swift b/Sources/JWTKit/Signing/JWTSigner+JWK.swift deleted file mode 100644 index d2502aa1..00000000 --- a/Sources/JWTKit/Signing/JWTSigner+JWK.swift +++ /dev/null @@ -1,64 +0,0 @@ -import Foundation - -extension JWTSigners { - /// Adds a `JWKS` (JSON Web Key Set) to this signers collection - /// by first decoding the JSON string. - public func use(jwksJSON json: String) throws { - let jwks = try JSONDecoder().decode(JWKS.self, from: Data(json.utf8)) - try self.use(jwks: jwks) - } - - /// Adds a `JWKS` (JSON Web Key Set) to this signers collection. - public func use(jwks: JWKS) throws { - try jwks.keys.forEach { try self.use(jwk: $0) } - } - - /// Adds a `JWK` (JSON Web Key) to this signers collection. - public func use(jwk: JWK) throws { - guard let kid = jwk.keyIdentifier else { - throw JWTError.invalidJWK - } - try self.use(.jwk(jwk), kid: kid) - } -} - -extension JWTSigner { - /// Creates a JWT sign from the supplied JWK json string. - public static func jwk(json: String) throws -> JWTSigner { - let jwk = try JSONDecoder().decode(JWK.self, from: Data(json.utf8)) - return try self.jwk(jwk) - } - - /// Creates a JWT signer with the supplied JWK - public static func jwk(_ key: JWK) throws -> JWTSigner { - switch key.keyType { - case .rsa: - guard let modulus = key.modulus else { - throw JWTError.invalidJWK - } - guard let exponent = key.exponent else { - throw JWTError.invalidJWK - } - guard let algorithm = key.algorithm else { - throw JWTError.invalidJWK - } - - guard let rsaKey = RSAKey( - modulus: modulus, - exponent: exponent, - privateExponent: key.privateExponent - ) else { - throw JWTError.invalidJWK - } - - switch algorithm { - case .rs256: - return JWTSigner.rs256(key: rsaKey) - case .rs384: - return JWTSigner.rs384(key: rsaKey) - case .rs512: - return JWTSigner.rs512(key: rsaKey) - } - } - } -} diff --git a/Sources/JWTKit/Signing/JWTSigner.swift b/Sources/JWTKit/Signing/JWTSigner.swift index b858a698..7240c662 100644 --- a/Sources/JWTKit/Signing/JWTSigner.swift +++ b/Sources/JWTKit/Signing/JWTSigner.swift @@ -8,11 +8,12 @@ public final class JWTSigner { public func sign( _ payload: Payload, + kid: JWKIdentifier? = nil, cty: String? = nil ) throws -> String where Payload: JWTPayload { - try JWTSerializer().sign(payload, using: self, kid: nil, cty: cty) + try JWTSerializer().sign(payload, using: self, kid: kid, cty: cty) } public func unverified( diff --git a/Sources/JWTKit/Signing/JWTSigners.swift b/Sources/JWTKit/Signing/JWTSigners.swift index fc121cbe..f149e1f7 100644 --- a/Sources/JWTKit/Signing/JWTSigners.swift +++ b/Sources/JWTKit/Signing/JWTSigners.swift @@ -5,13 +5,15 @@ import struct Foundation.Data /// A collection of signers labeled by `kid`. public final class JWTSigners { /// Internal storage. - private var storage: [JWKIdentifier: JWTSigner] + private var jwtStorage: [JWKIdentifier: JWTSigner] + private var jwkStorage: [JWKIdentifier: JWKSigner] private var `default`: JWTSigner? /// Create a new `JWTSigners`. public init() { - self.storage = [:] + self.jwtStorage = [:] + self.jwkStorage = [:] } /// Adds a new signer. @@ -21,24 +23,50 @@ public final class JWTSigners { isDefault: Bool? = nil ) { if let kid = kid { - self.storage[kid] = signer + self.jwtStorage[kid] = signer } if self.default == nil && isDefault != false { self.default = signer } } + /// Adds a `JWKS` (JSON Web Key Set) to this signers collection + /// by first decoding the JSON string. + public func use(jwksJSON json: String) throws { + let jwks = try JSONDecoder().decode(JWKS.self, from: Data(json.utf8)) + try self.use(jwks: jwks) + } + + /// Adds a `JWKS` (JSON Web Key Set) to this signers collection. + public func use(jwks: JWKS) throws { + try jwks.keys.forEach { try self.use(jwk: $0) } + } + + /// Adds a `JWK` (JSON Web Key) to this signers collection. + public func use(jwk: JWK) throws { + guard let kid = jwk.keyIdentifier else { + throw JWTError.invalidJWK + } + self.jwkStorage[kid] = JWKSigner(jwk: jwk) + } + /// Gets a signer for the supplied `kid`, if one exists. - public func get(kid: JWKIdentifier? = nil) -> JWTSigner? { + public func get(kid: JWKIdentifier? = nil, alg: String? = nil) -> JWTSigner? { if let kid = kid { - return self.storage[kid] + if let jwt = self.jwtStorage[kid] { + return jwt + } else if let jwk = self.jwkStorage[kid] { + return jwk.signer(for: alg.flatMap(JWK.Algorithm.init)) + } else { + return nil + } } else { return self.default } } - public func require(kid: JWKIdentifier? = nil) throws -> JWTSigner { - guard let signer = self.get(kid: kid) else { + public func require(kid: JWKIdentifier? = nil, alg: String? = nil) throws -> JWTSigner { + guard let signer = self.get(kid: kid, alg: alg) else { if let kid = kid { throw JWTError.unknownKID(kid) } else { @@ -84,7 +112,7 @@ public final class JWTSigners { { let parser = try JWTParser(token: token) let header = try parser.header() - return try self.require(kid: header.kid).verify(parser: parser) + return try self.require(kid: header.kid, alg: header.alg).verify(parser: parser) } public func sign( diff --git a/Tests/JWTKitTests/JWTKitTests.swift b/Tests/JWTKitTests/JWTKitTests.swift index 18b3a26a..a570c47d 100644 --- a/Tests/JWTKitTests/JWTKitTests.swift +++ b/Tests/JWTKitTests/JWTKitTests.swift @@ -40,7 +40,7 @@ class JWTKitTests: XCTestCase { let exp = ExpirationClaim(value: Date(timeIntervalSince1970: 2_000_000_000)) let jwt = try JWTSigner.hs256(key: "secret".bytes) .sign(ExpirationPayload(exp: exp)) - var parser = try JWTParser(token: jwt.bytes) + let parser = try JWTParser(token: jwt.bytes) try XCTAssertEqual(parser.header().typ, "JWT") try XCTAssertEqual(parser.header().alg, "HS256") try XCTAssertEqual(parser.payload(as: ExpirationPayload.self).exp, exp) @@ -194,8 +194,11 @@ class JWTKitTests: XCTestCase { } """ - let privateSigner = try JWTSigner.jwk(json: privateKey) - let publicSigner = try JWTSigner.jwk(json: publicKey) + let publicSigners = JWTSigners() + try publicSigners.use(jwk: .init(json: publicKey)) + + let privateSigners = JWTSigners() + try privateSigners.use(jwk: .init(json: privateKey)) let payload = TestPayload( sub: "vapor", @@ -203,11 +206,11 @@ class JWTKitTests: XCTestCase { admin: false, exp: .init(value: .init(timeIntervalSince1970: 2_000_000_000)) ) - let data = try privateSigner.sign(payload) + let data = try privateSigners.sign(payload, kid: "1234") // test private signer decoding - try XCTAssertEqual(privateSigner.verify(data, as: TestPayload.self), payload) + try XCTAssertEqual(privateSigners.verify(data, as: TestPayload.self), payload) // test public signer decoding - try XCTAssertEqual(publicSigner.verify(data, as: TestPayload.self), payload) + try XCTAssertEqual(publicSigners.verify(data, as: TestPayload.self), payload) } func testJWKS() throws { @@ -332,6 +335,50 @@ class JWTKitTests: XCTestCase { XCTAssertEqual(name, "aud") } } + + func testAlgorithmInJWTHeaderOnly() throws { + // rsa key + let modulus = "mSfWGBcXRBPgnwnL_ymDCkBaL6vcMcLpBEomzf-wZPajcQFiq4n4MHScyo85Te6GU-YuErVvHKK0D72JhMNWAQXbiF5Hh7swSYX9QsycWwHBgOBNfp51Fm_HTU7ikDBEdSonrmSep8wNqi_PX2_jVBsoxYNeiCQyDLFLHOAAcbIE4Y6lpJy76GpdHJscMO2RsUznjv5VPOQVa_BlQRIIZ0YoSsq9EEZna9O370wZy8jnOthQIXoegQ7sItS1JMKk4X5DdoRenIfbfWLy88XxKOPlIHA5ekT8TyzeI2Uqkg3YMETTDPrSROVO1Qdl2W1uMdfIZ94DgKpZN2VW-w0fLw" + let exponent = "AQAB" + let privateExponent = "awDmF9aqLqokmXjiydda8mKboArWwP2Ih7K3Ad3Og_u9nUp2gZrXiCMxGGSQiN5Jg3yiW_ffNYaHfyfRWKyQ_g31n4UfPLmPtw6iL3V9GChV5ZDRE9HpxE88U8r1h__xFFrrdnBeWKW8NldI70jg7vY6uiRae4uuXCfSbs4iAUxmRVKWCnV7JE6sObQKUV_EJkBcyND5Y97xsmWD0nPmXCnloQ84gF-eTErJoZBvQhJ4BhmBeUlREHmDKssaxVOCK4l335DKHD1vbuPk9e49M71BK7r2y4Atqk3TEetnwzMs3u-L9RqHaGIBw5u324uGweY7QeD7HFdAUtpjOq_MQQ" + + // sign jwt + let privateSigner = JWTSigner.rs256(key: RSAKey( + modulus: modulus, + exponent: exponent, + privateExponent: privateExponent + )!) + struct Foo: JWTPayload { + var bar: Int + func verify(using signer: JWTSigner) throws { } + } + let jwt = try privateSigner.sign(Foo(bar: 42), kid: "vapor") + + // verify using jwks without alg + let jwksString = """ + { + "keys": [ + { + "kty": "RSA", + "use": "sig", + "kid": "vapor", + "n": "\(modulus)", + "e": "\(exponent)" + } + ] + } + """ + + let signers = JWTSigners() + try signers.use(jwksJSON: jwksString) + let foo = try signers.verify(jwt, as: Foo.self) + XCTAssertEqual(foo.bar, 42) + } + + func testMicrosoftJWKs() throws { + let signers = JWTSigners() + try signers.use(jwksJSON: microsoftJWKS) + } } struct AudiencePayload: Codable { @@ -379,6 +426,46 @@ struct ExpirationPayload: JWTPayload { } } +let microsoftJWKS = """ +{ + "keys":[ + { + "kty":"RSA", + "use":"sig", + "kid":"huN95IvPfehq34GzBDZ1GXGirnM", + "x5t":"huN95IvPfehq34GzBDZ1GXGirnM", + "n":"6lldKm5Rc_vMKa1RM_TtUv3tmtj52wLRrJqu13yGM3_h0dwru2ZP53y65wDfz6_tLCjoYuRCuVsjoW37-0zXUORJvZ0L90CAX-58lW7NcE4bAzA1pXv7oR9kQw0X8dp0atU4HnHeaTU8LZxcjJO79_H9cxgwa-clKfGxllcos8TsuurM8xi2dx5VqwzqNMB2s62l3MTN7AzctHUiQCiX2iJArGjAhs-mxS1wmyMIyOSipdodhjQWRAcseW-aFVyRTFVi8okl2cT1HJjPXdx0b1WqYSOzeRdrrLUcA0oR2Tzp7xzOYJZSGNnNLQqa9f6h6h52XbX0iAgxKgEDlRpbJw", + "e":"AQAB", + "x5c":[ + "MIIDBTCCAe2gAwIBAgIQPCxFbySVSLZOggeWRzBWOjANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTIwMDYwNzAwMDAwMFoXDTI1MDYwNzAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOpZXSpuUXP7zCmtUTP07VL97ZrY+dsC0ayartd8hjN/4dHcK7tmT+d8uucA38+v7Swo6GLkQrlbI6Ft+/tM11DkSb2dC/dAgF/ufJVuzXBOGwMwNaV7+6EfZEMNF/HadGrVOB5x3mk1PC2cXIyTu/fx/XMYMGvnJSnxsZZXKLPE7LrqzPMYtnceVasM6jTAdrOtpdzEzewM3LR1IkAol9oiQKxowIbPpsUtcJsjCMjkoqXaHYY0FkQHLHlvmhVckUxVYvKJJdnE9RyYz13cdG9VqmEjs3kXa6y1HANKEdk86e8czmCWUhjZzS0KmvX+oeoedl219IgIMSoBA5UaWycCAwEAAaMhMB8wHQYDVR0OBBYEFFXP0ODFhjf3RS6oRijM5Tb+yB8CMA0GCSqGSIb3DQEBCwUAA4IBAQB9GtVikLTbJWIu5x9YCUTTKzNhi44XXogP/v8VylRSUHI5YTMdnWwvDIt/Y1sjNonmSy9PrioEjcIiI1U8nicveafMwIq5VLn+gEY2lg6KDJAzgAvA88CXqwfHHvtmYBovN7goolp8TY/kddMTf6TpNzN3lCTM2MK4Ye5xLLVGdp4bqWCOJ/qjwDxpTRSydYIkLUDwqNjv+sYfOElJpYAB4rTL/aw3ChJ1iaA4MtXEt6OjbUtbOa21lShfLzvNRbYK3+ukbrhmRl9lemJEeUls51vPuIe+jg+Ssp43aw7PQjxt4/MpfNMS2BfZ5F8GVSVG7qNb352cLLeJg5rc398Z" + ] + }, + { + "kty":"RSA", + "use":"sig", + "kid":"jibNbkFSSbmxPYrN9CFqRk4K4gw", + "x5t":"jibNbkFSSbmxPYrN9CFqRk4K4gw", + "n":"2YX-YDuuTzPiaiZKt04IuUzAjCjPLLmBCVA6npKuZyIouMuaSEuM7BP8QctfCprUY16Rq2-KDrAEvaaKJvsD5ZONddt79yFdCs1E8wKlYIPO74fSpePdVDizflr5W-QCFH9tokbZrHBBuluFojgtbvPMXAhHfZTGC4ItZ0i_Lc9eXwtENHJQC4e4m7olweK1ExM-OzsKGzDlOsOUOU5pN2sHY74nXPqQRH1dQKfB0NT0YrfkbnR8fiq8z-soixfECUXkF8FzWnMnqL6X90wngnuIi8OtH2mvDcnsvUVh3K2JgvSgjRWZbsDx6G-mVQL2vEuHXMXoIoe8hd1ZpV16pQ", + "e":"AQAB", + "x5c":[ + "MIIDBTCCAe2gAwIBAgIQUUG7iptQUoVA7bYvX2tHlDANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTIwMDcxODAwMDAwMFoXDTI1MDcxODAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANmF/mA7rk8z4momSrdOCLlMwIwozyy5gQlQOp6SrmciKLjLmkhLjOwT/EHLXwqa1GNekatvig6wBL2miib7A+WTjXXbe/chXQrNRPMCpWCDzu+H0qXj3VQ4s35a+VvkAhR/baJG2axwQbpbhaI4LW7zzFwIR32UxguCLWdIvy3PXl8LRDRyUAuHuJu6JcHitRMTPjs7Chsw5TrDlDlOaTdrB2O+J1z6kER9XUCnwdDU9GK35G50fH4qvM/rKIsXxAlF5BfBc1pzJ6i+l/dMJ4J7iIvDrR9prw3J7L1FYdytiYL0oI0VmW7A8ehvplUC9rxLh1zF6CKHvIXdWaVdeqUCAwEAAaMhMB8wHQYDVR0OBBYEFFOUEOWLUJOTFTOlr7P+6GxsmM90MA0GCSqGSIb3DQEBCwUAA4IBAQCP+LLZw7SSYnWQmRGWHmksBwwJ4Gy32C6g7+wZZv3ombHW9mwLQuzsir97/PP042i/ZIxePHJavpeLm/z3KMSpGIPmiPtmgNcK4HtLTEDnoTprnllobOAqU0TREFWogjkockNo98AvpsmHxNMXuwDikto9o/d9ACBtpkpatS2xgVOZxZtqyMpwZzSJARD5A4qcKov4zdqntVyjpZGK4N6ZaedRbEVd12m1VI+dtDB9+EJRqtTn8zamPYljVTEPNCbDAFgKBDtrhwBnrrrnKTq4/LEOouNQZuUucBTMOGDn4FEejNh3qbxNdWR6tSZbXUnJ+NIQ99IqZMvvMqm9ndL7" + ] + }, + { + "kty":"RSA", + "use":"sig", + "kid":"M6pX7RHoraLsprfJeRCjSxuURhc", + "x5t":"M6pX7RHoraLsprfJeRCjSxuURhc", + "n":"xHScZMPo8FifoDcrgncWQ7mGJtiKhrsho0-uFPXg-OdnRKYudTD7-Bq1MDjcqWRf3IfDVjFJixQS61M7wm9wALDj--lLuJJ9jDUAWTA3xWvQLbiBM-gqU0sj4mc2lWm6nPfqlyYeWtQcSC0sYkLlayNgX4noKDaXivhVOp7bwGXq77MRzeL4-9qrRYKjuzHfZL7kNBCsqO185P0NI2Jtmw-EsqYsrCaHsfNRGRrTvUHUq3hWa859kK_5uNd7TeY2ZEwKVD8ezCmSfR59ZzyxTtuPpkCSHS9OtUvS3mqTYit73qcvprjl3R8hpjXLb8oftfpWr3hFRdpxrwuoQEO4QQ", + "e":"AQAB", + "x5c":[ + "MIIC8TCCAdmgAwIBAgIQfEWlTVc1uINEc9RBi6qHMjANBgkqhkiG9w0BAQsFADAjMSEwHwYDVQQDExhsb2dpbi5taWNyb3NvZnRvbmxpbmUudXMwHhcNMTgxMDE0MDAwMDAwWhcNMjAxMDE0MDAwMDAwWjAjMSEwHwYDVQQDExhsb2dpbi5taWNyb3NvZnRvbmxpbmUudXMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDEdJxkw+jwWJ+gNyuCdxZDuYYm2IqGuyGjT64U9eD452dEpi51MPv4GrUwONypZF/ch8NWMUmLFBLrUzvCb3AAsOP76Uu4kn2MNQBZMDfFa9AtuIEz6CpTSyPiZzaVabqc9+qXJh5a1BxILSxiQuVrI2BfiegoNpeK+FU6ntvAZervsxHN4vj72qtFgqO7Md9kvuQ0EKyo7Xzk/Q0jYm2bD4SypiysJoex81EZGtO9QdSreFZrzn2Qr/m413tN5jZkTApUPx7MKZJ9Hn1nPLFO24+mQJIdL061S9LeapNiK3vepy+muOXdHyGmNctvyh+1+laveEVF2nGvC6hAQ7hBAgMBAAGjITAfMB0GA1UdDgQWBBQ5TKadw06O0cvXrQbXW0Nb3M3h/DANBgkqhkiG9w0BAQsFAAOCAQEAI48JaFtwOFcYS/3pfS5+7cINrafXAKTL+/+he4q+RMx4TCu/L1dl9zS5W1BeJNO2GUznfI+b5KndrxdlB6qJIDf6TRHh6EqfA18oJP5NOiKhU4pgkF2UMUw4kjxaZ5fQrSoD9omjfHAFNjradnHA7GOAoF4iotvXDWDBWx9K4XNZHWvD11Td66zTg5IaEQDIZ+f8WS6nn/98nAVMDtR9zW7Te5h9kGJGfe6WiHVaGRPpBvqC4iypGHjbRwANwofZvmp5wP08hY1CsnKY5tfP+E2k/iAQgKKa6QoxXToYvP7rsSkglak8N5g/+FJGnq4wP6cOzgZpjdPMwaVt5432GA==" + ] + } + ] +} +""" + let rsaModulus = """ gWu7yhI35FScdKARYboJoAm-T7yJfJ9JTvAok_RKOJYcL8oLIRSeLqQX83PPZiWdKTdXaiGWntpDu6vW7VAb-HWPF6tNYSLKDSmR3sEu2488ibWijZtNTCKOSb_1iAKAI5BJ80LTqyQtqaKzT0XUBtMsde8vX1nKI05UxujfTX3kqUtkZgLv1Yk1ZDpUoLOWUTtCm68zpjtBrPiN8bU2jqCGFyMyyXys31xFRzz4MyJ5tREHkQCzx0g7AvW0ge_sBTPQ2U6NSkcZvQyDbfDv27cMUHij1Sjx16SY9a2naTuOgamjtUzyClPLVpchX-McNyS0tjdxWY_yRL9MYuw4AQ """