mount public data from the atmosphere to a virtual filesystem (macos only) pdfs.at
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(atproto/oauth): add PKCE challenge + verifier generation

+77
+28
Packages/ATProto/Sources/ATProto/OAuth/PKCE.swift
··· 1 + import Foundation 2 + import CryptoKit 3 + 4 + public struct PKCE: Sendable, Equatable { 5 + public let verifier: String 6 + public let challenge: String 7 + public let method = "S256" 8 + 9 + /// Generates a fresh (verifier, challenge) pair. Verifier is 64 bytes 10 + /// of randomness → 86 base64url-no-pad chars. 11 + public static func generate() -> PKCE { 12 + let rawBytes = Data((0..<64).map { _ in UInt8.random(in: 0...255) }) 13 + let verifier = base64URLEncode(rawBytes) 14 + return PKCE(verifier: verifier, challenge: deriveChallenge(from: verifier)) 15 + } 16 + 17 + public static func deriveChallenge(from verifier: String) -> String { 18 + let digest = SHA256.hash(data: Data(verifier.utf8)) 19 + return base64URLEncode(Data(digest)) 20 + } 21 + 22 + static func base64URLEncode(_ data: Data) -> String { 23 + data.base64EncodedString() 24 + .replacingOccurrences(of: "+", with: "-") 25 + .replacingOccurrences(of: "/", with: "_") 26 + .replacingOccurrences(of: "=", with: "") 27 + } 28 + }
+49
Packages/ATProto/Tests/ATProtoTests/OAuth/PKCETests.swift
··· 1 + import Foundation 2 + import Testing 3 + @testable import ATProto 4 + 5 + @Suite("PKCE") 6 + struct PKCETests { 7 + @Test("verifier length is between 43 and 128") 8 + func verifierLength() { 9 + for _ in 0..<50 { 10 + let p = PKCE.generate() 11 + #expect(p.verifier.count >= 43) 12 + #expect(p.verifier.count <= 128) 13 + } 14 + } 15 + 16 + @Test("verifier uses only unreserved base64url-no-pad characters") 17 + func verifierCharset() { 18 + let allowed = CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~") 19 + for _ in 0..<10 { 20 + let p = PKCE.generate() 21 + let scalars = CharacterSet(charactersIn: p.verifier) 22 + #expect(scalars.isSubset(of: allowed)) 23 + } 24 + } 25 + 26 + @Test("challenge is base64url-encoded SHA256 of verifier") 27 + func challengeIsHash() throws { 28 + // Known-answer test vectors from RFC 7636 appendix B. 29 + let verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" 30 + let expectedChallenge = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM" 31 + let challenge = PKCE.deriveChallenge(from: verifier) 32 + #expect(challenge == expectedChallenge) 33 + } 34 + 35 + @Test("generate() produces challenge matching verifier") 36 + func generateConsistency() { 37 + for _ in 0..<5 { 38 + let p = PKCE.generate() 39 + #expect(p.challenge == PKCE.deriveChallenge(from: p.verifier)) 40 + } 41 + } 42 + 43 + @Test("each generate() returns a unique pair") 44 + func generateUnique() { 45 + let pairs = (0..<10).map { _ in PKCE.generate() } 46 + let verifiers = Set(pairs.map(\.verifier)) 47 + #expect(verifiers.count == pairs.count) 48 + } 49 + }