Post

Auth2.0 PKCE

参考:

JavaScript生成code_verifiercode_challenge的代码

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html>
	<body>
		<script src="./crypto.js"></script>
		<script>
				var code_verifier = random_string(48)
				console.log("code_verifier: ", code_verifier)
				var code_challenge = base64_urlencode(sha256bin(code_verifier));
				console.log("code_challenge: ", code_challenge)
		</script>
	</body>
</html>

Swift生成code_verifiercode_challenge的代码

PKCE.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import Foundation
import CryptoKit


///
/// An easy-to-use implementation of the client side of the [PKCE standard](https://datatracker.ietf.org/doc/html/rfc7636).
///
struct PKCE {
    
    typealias PKCECode = String
    
    /// A high-entropy cryptographic random value, as described in [Section 4.1](https://datatracker.ietf.org/doc/html/rfc7636#section-4.1) of the PKCE standard.
    let codeVerifier: PKCECode
    
    /// A transformation of the codeVerifier, as defined in [Section 4.2](https://datatracker.ietf.org/doc/html/rfc7636#section-4.2) of the PKCE standard.
    let codeChallenge: PKCECode
    
    init() throws {
        
        codeVerifier = PKCE.generateCodeVerifier()
        codeChallenge = try PKCE.codeChallenge(fromVerifier: codeVerifier)
    }
    
    static func codeChallenge(fromVerifier verifier: PKCECode) throws -> PKCECode {
        
        guard let verifierData = verifier.data(using: .ascii) else { throw PKCEError.improperlyFormattedVerifier }
        
        let challengeHashed = SHA256.hash(data: verifierData)
        let challengeBase64Encoded = Data(challengeHashed).base64URLEncodedString
        
        return challengeBase64Encoded
    }
    
    ///
    /// Generates a random code verifier (as defined in [Seciton 4.1 of the PKCE standard](https://datatracker.ietf.org/doc/html/rfc7636#section-4.1)).
    ///
    /// This method first attempts to use CryptoKit to generate random bytes. If it fails to generate those random bytes, it falls back on a generic
    /// Base64 random string generator.
    ///
    static func generateCodeVerifier() -> PKCECode {
        
        do {
            
            let rando = try PKCE.generateCryptographicallySecureRandomOctets(count: 32)
            return Data(bytes: rando, count: rando.count).base64URLEncodedString
            
        } catch {
            
            return generateBase64RandomString(ofLength: 43)
        }
    }
    
    private static func generateCryptographicallySecureRandomOctets(count: Int) throws -> [UInt8] {
        
        var octets = [UInt8](repeating: 0, count: count)
        let status = SecRandomCopyBytes(kSecRandomDefault, octets.count, &octets)
        
        if status == errSecSuccess {
            
            return octets
            
        } else {
            
            throw PKCEError.failedToGenerateRandomOctets
        }
    }
    
    private static func generateBase64RandomString(ofLength length: UInt8) -> PKCECode {
        
        let base64 = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
        return String((0..<length).map{ _ in base64.randomElement()! })
    }
    
    enum PKCEError: Error {
        
        case failedToGenerateRandomOctets
        case improperlyFormattedVerifier
    }
}

extension Data {
    
    ///
    /// Returns a Base64 URL-encoded string _without_ padding.
    ///
    /// This string is compatible with the PKCE Code generation process, and uses the algorithm as defined in the [PKCE standard](https://datatracker.ietf.org/doc/html/rfc7636#appendix-A).
    ///
    var base64URLEncodedString: String {
    
        base64EncodedString()
            .replacingOccurrences(of: "=", with: "") // Remove any trailing '='s
            .replacingOccurrences(of: "+", with: "-") // 62nd char of encoding
            .replacingOccurrences(of: "/", with: "_") // 63rd char of encoding
            .trimmingCharacters(in: .whitespaces)
    }
}

调用:

1
2
3
4
5
6
7
8
9
do {
    let pk = try PKCE()
    let code_verifier = pk?.codeVerifier ?? ""
	print("code_verifier: ", code_verifier)
	let code_challenge = pk?.codeChallenge ?? ""
	print("code_challenge: ", code_challenge)
} catch {
   print(error)
}

Create code verifier

Javascript sample

1
2
3
4
5
6
7
8
9
// Dependency: Node.js crypto module
// https://nodejs.org/api/crypto.html#crypto_crypto
function base64URLEncode(str) {
    return str.toString('base64')
        .replace(/\+/g, '-')
        .replace(/\//g, '_')
        .replace(/=/g, '');
}
var verifier = base64URLEncode(crypto.randomBytes(32));

Java sample

1
2
3
4
5
6
7
// See https://developer.android.com/reference/android/util/Base64
// Import the Base64 class
// import android.util.Base64;
SecureRandom sr = new SecureRandom();
byte[] code = new byte[32];
sr.nextBytes(code);
String verifier = Base64.encodeToString(code, Base64.URL_SAFE | Base64.NO_WRAP | Base64.NO_PADDING);

Swift 5 sample

1
2
3
4
5
6
var buffer = [UInt8](repeating: 0, count: 32)
_ = SecRandomCopyBytes(kSecRandomDefault, buffer.count, &buffer)
let verifier = Data(buffer).base64EncodedString()
    .replacingOccurrences(of: "+", with: "-")
    .replacingOccurrences(of: "/", with: "_")
    .replacingOccurrences(of: "=", with: "")

Objective-C sample

1
2
3
4
5
6
NSMutableData *data = [NSMutableData dataWithLength:32];
int result __attribute__((unused)) = SecRandomCopyBytes(kSecRandomDefault, 32, data.mutableBytes);
NSString *verifier = [[[[data base64EncodedStringWithOptions:0]
                        stringByReplacingOccurrencesOfString:@"+" withString:@"-"]
                        stringByReplacingOccurrencesOfString:@"/" withString:@"_"]
                        stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"="]];

Create code challenge

Javascript sample

1
2
3
4
5
6
// Dependency: Node.js crypto module
// https://nodejs.org/api/crypto.html#crypto_crypto
function sha256(buffer) {
    return crypto.createHash('sha256').update(buffer).digest();
}
var challenge = base64URLEncode(sha256(verifier));

Java sample

1
2
3
4
5
6
7
8
9
// Dependency: Apache Commons Codec
// https://commons.apache.org/proper/commons-codec/
// Import the Base64 class.
// import org.apache.commons.codec.binary.Base64;
byte[] bytes = verifier.getBytes("US-ASCII");
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(bytes, 0, bytes.length);
byte[] digest = md.digest();
String challenge = Base64.encodeBase64URLSafeString(digest);

Swift5 sample

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import CommonCrypto

// ...

guard let data = verifier.data(using: .utf8) else { return nil }
var buffer = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
_ = data.withUnsafeBytes {
    CC_SHA256($0.baseAddress, CC_LONG(data.count), &buffer)
}
let hash = Data(buffer)
let challenge = hash.base64EncodedString()
    .replacingOccurrences(of: "+", with: "-")
    .replacingOccurrences(of: "/", with: "_")
    .replacingOccurrences(of: "=", with: "")

Objective-C sample

1
2
3
4
5
6
7
8
9
10
11
// Dependency: Apple Common Crypto library
// http://opensource.apple.com//source/CommonCrypto
u_int8_t buffer[CC_SHA256_DIGEST_LENGTH * sizeof(u_int8_t)];
memset(buffer, 0x0, CC_SHA256_DIGEST_LENGTH);
NSData *data = [verifier dataUsingEncoding:NSUTF8StringEncoding];
CC_SHA256([data bytes], (CC_LONG)[data length], buffer);
NSData *hash = [NSData dataWithBytes:buffer length:CC_SHA256_DIGEST_LENGTH];
NSString *challenge = [[[[hash base64EncodedStringWithOptions:0]
                         stringByReplacingOccurrencesOfString:@"+" withString:@"-"]
                         stringByReplacingOccurrencesOfString:@"/" withString:@"_"]
                         stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"="]];
This post is licensed under CC BY 4.0 by the author.