diff --git a/modules/expo-bluesky-oauth-client/android/src/main/java/JWK.kt b/modules/expo-bluesky-oauth-client/android/src/main/java/JWK.kt deleted file mode 100644 index 64835b370b..0000000000 --- a/modules/expo-bluesky-oauth-client/android/src/main/java/JWK.kt +++ /dev/null @@ -1,19 +0,0 @@ -package expo.modules.blueskyoauthclient - -import expo.modules.kotlin.records.Record -import expo.modules.kotlin.records.Field - -class JWK( - @Field var alg: String = "", - @Field var kty: String = "", - @Field var crv: String? = null, - @Field var x: String? = null, - @Field var y: String? = null, - @Field var e: String? = null, - @Field var n: String? = null, - @Field var d: String? = null, - @Field var use: String? = null, - @Field var kid: String? = null -) : Record - -class JWKPair(@Field val privateKey: JWK, @Field val publicKey: JWK) : Record \ No newline at end of file diff --git a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/CryptoUtil.kt b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/CryptoUtil.kt index 1c8ea0eaa5..eab729a8b8 100644 --- a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/CryptoUtil.kt +++ b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/CryptoUtil.kt @@ -52,6 +52,7 @@ class CryptoUtil { crv = privateJwk.curve.toString(), x = privateJwk.x.toString(), y = privateJwk.y.toString(), + d = privateJwk.d.toString(), use = privateJwk.keyUse.toString(), kid = privateJwk.keyID ), diff --git a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/ExpoBlueskyOAuthClientModule.kt b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/ExpoBlueskyOAuthClientModule.kt index 93906a3ab1..15e6305b45 100644 --- a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/ExpoBlueskyOAuthClientModule.kt +++ b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/ExpoBlueskyOAuthClientModule.kt @@ -1,5 +1,6 @@ package expo.modules.blueskyoauthclient +import android.util.Log import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition @@ -15,21 +16,19 @@ class ExpoBlueskyOAuthClientModule : Module() { return@Function CryptoUtil().getRandomValues(byteLength) } - AsyncFunction("generateKeyPair") { keyId: String? -> - val res = CryptoUtil().generateKeyPair(keyId) - - return@AsyncFunction mapOf( - "publicKey" to res.first, - "privateKey" to res.second - ) + AsyncFunction("generateJwk") { algorithim: String -> + if (algorithim != "ES256") { + throw Exception("Unsupported algorithm") + } + return@AsyncFunction CryptoUtil().generateKeyPair() } - AsyncFunction("createJwt") { jwkString: String, headerString: String, payloadString: String -> - return@AsyncFunction JWTUtil().createJwt(jwkString, headerString, payloadString) + AsyncFunction("createJwt") { header: JWTHeader, payload: JWTPayload, jwk: JWK -> + return@AsyncFunction JWTUtil().createJwt(header, payload, jwk) } - AsyncFunction("verifyJwt") { jwkString: String, tokenString: String, options: String? -> - return@AsyncFunction JWTUtil().verifyJwt(jwkString, tokenString, options) + AsyncFunction("verifyJwt") { token: String, jwk: JWK -> + return@AsyncFunction JWTUtil().verifyJwt(token, jwk) } } } diff --git a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWK.kt b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWK.kt new file mode 100644 index 0000000000..2ce71dd8fa --- /dev/null +++ b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWK.kt @@ -0,0 +1,34 @@ +package expo.modules.blueskyoauthclient + +import expo.modules.kotlin.records.Record +import expo.modules.kotlin.records.Field + +class JWK( + @Field var alg: String = "", + @Field var kty: String = "", + @Field var crv: String? = null, + @Field var x: String? = null, + @Field var y: String? = null, + @Field var e: String? = null, + @Field var n: String? = null, + @Field var d: String? = null, + @Field var use: String? = null, + @Field var kid: String? = null +) : Record { + fun toJson(): String { + val parts = mutableListOf() + if (alg.isNotEmpty()) parts.add("\"alg\": \"$alg\"") + if (kty.isNotEmpty()) parts.add("\"kty\": \"$kty\"") + if (crv != null) parts.add("\"crv\": \"$crv\"") + if (x != null) parts.add("\"x\": \"$x\"") + if (y != null) parts.add("\"y\": \"$y\"") + if (e != null) parts.add("\"e\": \"$e\"") + if (n != null) parts.add("\"n\": \"$n\"") + if (d != null) parts.add("\"d\": \"$d\"") + if (use != null) parts.add("\"use\": \"$use\"") + if (kid != null) parts.add("\"kid\": \"$kid\"") + return "{ ${parts.joinToString()} }" + } +} + +class JWKPair(@Field val privateKey: JWK, @Field val publicKey: JWK) : Record \ No newline at end of file diff --git a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWT.kt b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWT.kt index a36df26a3a..8b761615a0 100644 --- a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWT.kt +++ b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWT.kt @@ -6,12 +6,24 @@ import expo.modules.kotlin.records.Field class JWTHeader( @Field var alg: String = "", @Field var jku: String? = null, - @Field var jwk: JWK = JWK(), + @Field var jwk: JWK? = null, @Field var kid: String? = null, @Field var typ: String? = null, @Field var cty: String? = null, @Field var crit: String? = null -) : Record +) : Record { + fun toJson(): String { + val parts = mutableListOf() + if (alg.isNotEmpty()) parts.add("\"alg\": \"$alg\"") + if (jku != null) parts.add("\"jku\": \"$jku\"") + if (jwk != null) parts.add("\"jwk\": ${jwk?.toJson()}") + if (kid != null) parts.add("\"kid\": \"$kid\"") + if (typ != null) parts.add("\"typ\": \"$typ\"") + if (cty != null) parts.add("\"cty\": \"$cty\"") + if (crit != null) parts.add("\"crit\": \"$crit\"") + return "{ ${parts.joinToString()} }" + } +} class JWTPayload( @Field var iss: String? = null, @@ -41,7 +53,63 @@ class JWTPayload( @Field var middle_name: String? = null, @Field var nickname: String? = null, @Field var preferred_username: String? = null, -) : Record + @Field var gender: String? = null, + @Field var picture: String? = null, + @Field var profile: String? = null, + @Field var birthdate: String? = null, + @Field var zoneinfo: String? = null, + @Field var updated_at: Int? = null, + @Field var email: String? = null, + @Field var email_verified: Boolean? = null, + @Field var phone_number: String? = null, + @Field var phone_number_verified: Boolean? = null, + @Field var address: JWTPayloadAddress? = null, + @Field var authorization_details: JWTPayloadAuthorizationDetails? = null +) : Record { + fun toJson(): String { + val parts = mutableListOf() + if (iss != null) parts.add("\"iss\": \"$iss\"") + if (aud != null) parts.add("\"aud\": \"$aud\"") + if (sub != null) parts.add("\"sub\": \"$sub\"") + if (exp != null) parts.add("\"exp\": $exp") + if (nbr != null) parts.add("\"nbr\": $nbr") + if (iat != null) parts.add("\"iat\": $iat") + if (jti != null) parts.add("\"jti\": \"$jti\"") + if (htm != null) parts.add("\"htm\": \"$htm\"") + if (htu != null) parts.add("\"htu\": \"$htu\"") + if (ath != null) parts.add("\"ath\": \"$ath\"") + if (acr != null) parts.add("\"acr\": \"$acr\"") + if (azp != null) parts.add("\"azp\": \"$azp\"") + if (amr != null) parts.add("\"amr\": \"$amr\"") + if (cnf != null) parts.add("\"cnf\": ${cnf?.toJson()}") + if (client_id != null) parts.add("\"client_id\": \"$client_id\"") + if (scope != null) parts.add("\"scope\": \"$scope\"") + if (nonce != null) parts.add("\"nonce\": \"$nonce\"") + if (at_hash != null) parts.add("\"at_hash\": \"$at_hash\"") + if (c_hash != null) parts.add("\"c_hash\": \"$c_hash\"") + if (s_hash != null) parts.add("\"s_hash\": \"$s_hash\"") + if (auth_time != null) parts.add("\"auth_time\": $auth_time") + if (name != null) parts.add("\"name\": \"$name\"") + if (family_name != null) parts.add("\"family_name\": \"$family_name\"") + if (given_name != null) parts.add("\"given_name\": \"$given_name\"") + if (middle_name != null) parts.add("\"middle_name\": \"$middle_name\"") + if (nickname != null) parts.add("\"nickname\": \"$nickname\"") + if (preferred_username != null) parts.add("\"preferred_username\": \"$preferred_username\"") + if (gender != null) parts.add("\"gender\": \"$gender\"") + if (picture != null) parts.add("\"picture\": \"$picture\"") + if (profile != null) parts.add("\"profile\": \"$profile\"") + if (birthdate != null) parts.add("\"birthdate\": \"$birthdate\"") + if (zoneinfo != null) parts.add("\"zoneinfo\": \"$zoneinfo\"") + if (updated_at != null) parts.add("\"updated_at\": $updated_at") + if (email != null) parts.add("\"email\": \"$email\"") + if (email_verified != null) parts.add("\"email_verified\": $email_verified") + if (phone_number != null) parts.add("\"phone_number\": \"$phone_number\"") + if (phone_number_verified != null) parts.add("\"phone_number_verified\": $phone_number_verified") + if (address != null) parts.add("\"address\": ${address?.toJson()}") + if (authorization_details != null) parts.add("\"authorization_details\": ${authorization_details?.toJson()}") + return "{ ${parts.joinToString()} }" + } +} class JWTPayloadCNF( @Field var jwk: JWK? = null, @@ -49,7 +117,17 @@ class JWTPayloadCNF( @Field var jku: String? = null, @Field var jkt: String? = null, @Field var osc: String? = null -) : Record +) : Record { + fun toJson(): String { + val parts = mutableListOf() + if (jwk != null) parts.add("\"jwk\": ${jwk?.toJson()}") + if (jwe != null) parts.add("\"jwe\": \"$jwe\"") + if (jku != null) parts.add("\"jku\": \"$jku\"") + if (jkt != null) parts.add("\"jkt\": \"$jkt\"") + if (osc != null) parts.add("\"osc\": \"$osc\"") + return "{ ${parts.joinToString()} }" + } +} class JWTPayloadAddress( @Field var formatted: String? = null, @@ -58,7 +136,18 @@ class JWTPayloadAddress( @Field var region: String? = null, @Field var postal_code: String? = null, @Field var country: String? = null -) : Record +) : Record { + fun toJson(): String { + val parts = mutableListOf() + if (formatted != null) parts.add("\"formatted\": \"$formatted\"") + if (street_address != null) parts.add("\"street_address\": \"$street_address\"") + if (locality != null) parts.add("\"locality\": \"$locality\"") + if (region != null) parts.add("\"region\": \"$region\"") + if (postal_code != null) parts.add("\"postal_code\": \"$postal_code\"") + if (country != null) parts.add("\"country\": \"$country\"") + return "{ ${parts.joinToString()} }" + } +} class JWTPayloadAuthorizationDetails( @Field var type: String? = null, @@ -67,10 +156,20 @@ class JWTPayloadAuthorizationDetails( @Field var datatypes: Array? = null, @Field var identifier: String? = null, @Field var privileges: Array? = null -) : Record +) : Record { + fun toJson(): String { + val parts = mutableListOf() + if (type != null) parts.add("\"type\": \"$type\"") + if (locations != null) parts.add("\"locations\": [${locations?.joinToString()}]") + if (actions != null) parts.add("\"actions\": [${actions?.joinToString()}]") + if (datatypes != null) parts.add("\"datatypes\": [${datatypes?.joinToString()}]") + if (identifier != null) parts.add("\"identifier\": \"$identifier\"") + if (privileges != null) parts.add("\"privileges\": [${privileges?.joinToString()}]") + return "{ ${parts.joinToString()} }" + } +} class JWTVerifyResponse( - @Field var header: JWTHeader = JWTHeader(), - @Field var payload: JWTPayload = JWTPayload(), - @Field var signature: String = "" + @Field var protectedHeader: JWTHeader = JWTHeader(), + @Field var payload: String = "", ) : Record \ No newline at end of file diff --git a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWTUtil.kt b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWTUtil.kt index 8c099d198c..1fb9276e5d 100644 --- a/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWTUtil.kt +++ b/modules/expo-bluesky-oauth-client/android/src/main/java/expo/modules/blueskyoauthclient/JWTUtil.kt @@ -7,29 +7,64 @@ import com.nimbusds.jose.jwk.ECKey import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.SignedJWT - class JWTUtil { - fun createJwt(jwkString: String, headerString: String, payloadString: String): String { - val key = ECKey.parse(jwkString) - val header = JWSHeader.parse(headerString) - val payload = JWTClaimsSet.parse(payloadString) + fun createJwt(header: JWTHeader, payload: JWTPayload, jwk: JWK): String { + val parsedKey = ECKey.parse(jwk.toJson()) + val parsedHeader = JWSHeader.parse(header.toJson()) + val parsedPayload = JWTClaimsSet.parse(payload.toJson()) - val signer = ECDSASigner(key) - val jwt = SignedJWT(header, payload) + val signer = ECDSASigner(parsedKey) + val jwt = SignedJWT(parsedHeader, parsedPayload) jwt.sign(signer) return jwt.serialize() } - fun verifyJwt(jwkString: String, tokenString: String, options: String?): Boolean { - return try { - val key = ECKey.parse(jwkString) - val jwt = SignedJWT.parse(tokenString) - val verifier = ECDSAVerifier(key) + fun verifyJwt(token: String, jwk: JWK): JWTVerifyResponse { + try { + val parsedKey = ECKey.parse(jwk.toJson()) + val jwt = SignedJWT.parse(token) + val verifier = ECDSAVerifier(parsedKey) - jwt.verify(verifier) + if (!jwt.verify(verifier)) { + throw Exception("Invalid signature") + } + + val header = jwt.header + val payload = jwt.payload + val ecKey = header.jwk?.toECKey() + val serializedJwk = if (ecKey != null) { + JWK( + alg = ecKey.algorithm.toString(), + kty = ecKey.keyType.toString(), + crv = ecKey.curve.toString(), + x = ecKey.x.toString(), + y = ecKey.y.toString(), + d = ecKey.d.toString(), + use = ecKey.keyUse.toString(), + kid = ecKey.keyID + ) + } else { + null + } + + val serializedHeader = JWTHeader( + alg = header.algorithm.toString(), + jku = header.jwkurl?.toString(), + jwk = serializedJwk, + kid = header.keyID, + typ = header.type?.toString(), + cty = header.contentType, + crit = header.criticalParams?.joinToString() + ) + val serializedPayload = payload.toString() + + return JWTVerifyResponse( + protectedHeader = serializedHeader, + payload = serializedPayload, + ) } catch(e: Exception) { - false + throw e } } } diff --git a/modules/expo-bluesky-oauth-client/ios/JWT.swift b/modules/expo-bluesky-oauth-client/ios/JWT.swift index dc3dbf9f54..8ce014048a 100644 --- a/modules/expo-bluesky-oauth-client/ios/JWT.swift +++ b/modules/expo-bluesky-oauth-client/ios/JWT.swift @@ -100,7 +100,7 @@ struct JWTPayload : Record { @Field var email: String? @Field - var email_verified: String? + var email_verified: Bool? @Field var phone_number: String? @Field diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index bb4e9e1aa8..cce0121328 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -68,6 +68,9 @@ function HomeScreenReady({ }, ) + console.log(jwt) + console.log(key.publicJwk) + const verified = await key.verifyJwt(jwt) console.log(verified)