diff --git a/.github/workflows/apikeys-ci.xml b/.github/workflows/apikeys-ci.xml index 841112cbe..64ce8350c 100644 --- a/.github/workflows/apikeys-ci.xml +++ b/.github/workflows/apikeys-ci.xml @@ -6,4 +6,5 @@ ci ci ci:ci + ci:ci diff --git a/app/build.gradle b/app/build.gradle index 5e24858e0..fe940a246 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -152,6 +152,13 @@ android { if (acraKey != null) { variant.resValue "string", "acra_credentials", acraKey } + def teslaKey = env.TESLA_CREDENTIALS ?: project.findProperty("TESLA_CREDENTIALS") + if (teslaKey == null && project.hasProperty("TESLA_CREDENTIALS_ENCRYPTED")) { + teslaKey = decode(project.findProperty("TESLA_CREDENTIALS_ENCRYPTED"), "FmK.d,-f*p+rD+WK!eds") + } + if (teslaKey != null) { + variant.resValue "string", "tesla_credentials", teslaKey + } } packagingOptions { diff --git a/app/src/main/java/net/vonforst/evmap/api/availability/AvailabilityDetector.kt b/app/src/main/java/net/vonforst/evmap/api/availability/AvailabilityDetector.kt index f3d95f331..b09daef87 100644 --- a/app/src/main/java/net/vonforst/evmap/api/availability/AvailabilityDetector.kt +++ b/app/src/main/java/net/vonforst/evmap/api/availability/AvailabilityDetector.kt @@ -3,6 +3,7 @@ package net.vonforst.evmap.api.availability import android.content.Context import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import net.vonforst.evmap.R import net.vonforst.evmap.addDebugInterceptors import net.vonforst.evmap.api.RateLimitInterceptor import net.vonforst.evmap.api.await @@ -170,8 +171,15 @@ class AvailabilityRepository(context: Context) { .connectTimeout(10, TimeUnit.SECONDS) .cookieJar(JavaNetCookieJar(cookieManager)) .build() - private val teslaAvailabilityDetector = - TeslaAvailabilityDetector(okhttp, EncryptedPreferenceDataStore(context)) + private val teslaAvailabilityDetector = run { + val (clientId, clientSecret) = context.getString(R.string.tesla_credentials).split(":") + TeslaAvailabilityDetector( + okhttp, + EncryptedPreferenceDataStore(context), + clientId, + clientSecret + ) + } private val availabilityDetectors = listOf( RheinenergieAvailabilityDetector(okhttp), teslaAvailabilityDetector, diff --git a/app/src/main/java/net/vonforst/evmap/api/availability/TeslaAvailabilityDetector.kt b/app/src/main/java/net/vonforst/evmap/api/availability/TeslaAvailabilityDetector.kt index 3bc8c1ddb..cb25a0e0f 100644 --- a/app/src/main/java/net/vonforst/evmap/api/availability/TeslaAvailabilityDetector.kt +++ b/app/src/main/java/net/vonforst/evmap/api/availability/TeslaAvailabilityDetector.kt @@ -35,22 +35,24 @@ interface TeslaAuthenticationApi { @JsonClass(generateAdapter = true) class AuthCodeRequest( val code: String, - @Json(name = "code_verifier") val codeVerifier: String, - @Json(name = "redirect_uri") val redirectUri: String = "https://auth.tesla.com/void/callback", - scope: String = "openid email offline_access", - @Json(name = "client_id") clientId: String = "ownerapi" - ) : OAuth2Request(scope, clientId) + @Json(name = "redirect_uri") val redirectUri: String = "https://ev-map.app/void/callback", + scope: String = "openid offline_access vehicle_device_data", + @Json(name = "client_id") clientId: String, + @Json(name = "client_secret") clientSecret: String + ) : OAuth2Request(scope, clientId, clientSecret) @JsonClass(generateAdapter = true) class RefreshTokenRequest( @Json(name = "refresh_token") val refreshToken: String, - scope: String = "openid email offline_access", - @Json(name = "client_id") clientId: String = "ownerapi" - ) : OAuth2Request(scope, clientId) + scope: String = "openid offline_access vehicle_device_data", + @Json(name = "client_id") clientId: String, + @Json(name = "client_secret") clientSecret: String, + ) : OAuth2Request(scope, clientId, clientSecret) sealed class OAuth2Request( val scope: String, - val clientId: String + val clientId: String, + val clientSecret: String ) @JsonClass(generateAdapter = true) @@ -85,36 +87,15 @@ interface TeslaAuthenticationApi { return retrofit.create(TeslaAuthenticationApi::class.java) } - fun generateCodeVerifier(): String { - val code = ByteArray(64) - SecureRandom().nextBytes(code) - return Base64.encodeToString( - code, - Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING - ) - } - - fun generateCodeChallenge(codeVerifier: String): String { - val bytes = codeVerifier.toByteArray() - val messageDigest = MessageDigest.getInstance("SHA-256") - messageDigest.update(bytes, 0, bytes.size) - return Base64.encodeToString( - messageDigest.digest(), - Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING - ) - } - - fun buildSignInUri(codeChallenge: String): Uri = + fun buildSignInUri(clientId: String): Uri = Uri.parse("https://auth.tesla.com/oauth2/v3/authorize").buildUpon() - .appendQueryParameter("client_id", "ownerapi") - .appendQueryParameter("code_challenge", codeChallenge) - .appendQueryParameter("code_challenge_method", "S256") - .appendQueryParameter("redirect_uri", "https://auth.tesla.com/void/callback") + .appendQueryParameter("client_id", clientId) + .appendQueryParameter("redirect_uri", "https://ev-map.app/void/callback") .appendQueryParameter("response_type", "code") - .appendQueryParameter("scope", "openid email offline_access") + .appendQueryParameter("scope", "openid offline_access vehicle_device_data") .appendQueryParameter("state", "123").build() - val resultUrlPrefix = "https://auth.tesla.com/void/callback" + val resultUrlPrefix = "https://ev-map.app/void/callback" } } @@ -500,6 +481,8 @@ fun Coordinate.asTeslaCoord() = class TeslaAvailabilityDetector( private val client: OkHttpClient, private val tokenStore: TokenStore, + private val clientId: String, + private val clientSecret: String, private val baseUrl: String? = null ) : BaseAvailabilityDetector(client) { @@ -644,7 +627,9 @@ class TeslaAvailabilityDetector( val response = authApi.getToken( TeslaAuthenticationApi.RefreshTokenRequest( - refreshToken + refreshToken, + clientId = clientId, + clientSecret = clientSecret ) ) tokenStore.teslaAccessToken = response.accessToken diff --git a/app/src/main/java/net/vonforst/evmap/auto/SettingsScreens.kt b/app/src/main/java/net/vonforst/evmap/auto/SettingsScreens.kt index 55d7c83b9..b27975118 100644 --- a/app/src/main/java/net/vonforst/evmap/auto/SettingsScreens.kt +++ b/app/src/main/java/net/vonforst/evmap/auto/SettingsScreens.kt @@ -8,9 +8,6 @@ import android.content.pm.PackageManager.NameNotFoundException import android.hardware.Sensor import android.hardware.SensorManager import android.net.Uri -import android.os.Bundle -import android.os.IInterface -import android.text.Html import androidx.annotation.StringRes import androidx.car.app.CarContext import androidx.car.app.CarToast @@ -23,7 +20,6 @@ import androidx.core.graphics.drawable.IconCompat import androidx.core.text.HtmlCompat import androidx.lifecycle.lifecycleScope import androidx.localbroadcastmanager.content.LocalBroadcastManager -import com.google.android.material.snackbar.Snackbar import kotlinx.coroutines.launch import net.vonforst.evmap.* import net.vonforst.evmap.api.availability.TeslaAuthenticationApi @@ -240,12 +236,9 @@ class DataSettingsScreen(ctx: CarContext) : Screen(ctx) { } private fun teslaLogin() { - val codeVerifier = TeslaAuthenticationApi.generateCodeVerifier() - val codeChallenge = TeslaAuthenticationApi.generateCodeChallenge(codeVerifier) - val uri = TeslaAuthenticationApi.buildSignInUri(codeChallenge) - + val (clientId, _) = carContext.getString(R.string.tesla_credentials).split(":") val args = OAuthLoginFragmentArgs( - uri.toString(), + TeslaAuthenticationApi.buildSignInUri(clientId = clientId).toString(), TeslaAuthenticationApi.resultUrlPrefix, "#000000" ).toBundle() @@ -261,7 +254,7 @@ class DataSettingsScreen(ctx: CarContext) : Screen(ctx) { OAuthLoginFragment.EXTRA_URL, Uri::class.java ) - teslaGetAccessToken(url!!, codeVerifier) + teslaGetAccessToken(url!!) } }, IntentFilter(OAuthLoginFragment.ACTION_OAUTH_RESULT)) @@ -276,22 +269,27 @@ class DataSettingsScreen(ctx: CarContext) : Screen(ctx) { } } - private fun teslaGetAccessToken(url: Uri, codeVerifier: String) { + private fun teslaGetAccessToken(url: Uri) { teslaLoggingIn = true invalidate() val code = url.getQueryParameter("code") ?: return val okhttp = OkHttpClient.Builder().addDebugInterceptors().build() - val request = TeslaAuthenticationApi.AuthCodeRequest(code, codeVerifier) + val (clientId, clientSecret) = carContext.getString(R.string.tesla_credentials).split(":") + val request = TeslaAuthenticationApi.AuthCodeRequest( + code, + clientId = clientId, + clientSecret = clientSecret + ) lifecycleScope.launch { try { val time = Instant.now().epochSecond val response = TeslaAuthenticationApi.create(okhttp).getToken(request) - val userResponse = - TeslaOwnerApi.create(okhttp, response.accessToken).getUserInfo() +// val userResponse = +// TeslaOwnerApi.create(okhttp, response.accessToken).getUserInfo() - encryptedPrefs.teslaEmail = userResponse.response.email + encryptedPrefs.teslaEmail = "user@example.com" encryptedPrefs.teslaAccessToken = response.accessToken encryptedPrefs.teslaAccessTokenExpiry = time + response.expiresIn encryptedPrefs.teslaRefreshToken = response.refreshToken diff --git a/app/src/main/java/net/vonforst/evmap/fragment/preference/DataSettingsFragment.kt b/app/src/main/java/net/vonforst/evmap/fragment/preference/DataSettingsFragment.kt index 311d75b7e..ed7cc4dd2 100644 --- a/app/src/main/java/net/vonforst/evmap/fragment/preference/DataSettingsFragment.kt +++ b/app/src/main/java/net/vonforst/evmap/fragment/preference/DataSettingsFragment.kt @@ -139,10 +139,8 @@ class DataSettingsFragment : BaseSettingsFragment() { } private fun teslaLogin() { - val codeVerifier = TeslaAuthenticationApi.generateCodeVerifier() - val codeChallenge = TeslaAuthenticationApi.generateCodeChallenge(codeVerifier) - val uri = TeslaAuthenticationApi.buildSignInUri(codeChallenge) - + val (clientId, _) = getString(R.string.tesla_credentials).split(":") + val uri = TeslaAuthenticationApi.buildSignInUri(clientId = clientId) val args = OAuthLoginFragmentArgs( uri.toString(), TeslaAuthenticationApi.resultUrlPrefix, @@ -150,28 +148,33 @@ class DataSettingsFragment : BaseSettingsFragment() { ).toBundle() setFragmentResultListener(uri.toString()) { _, result -> - teslaGetAccessToken(result, codeVerifier) + teslaGetAccessToken(result) } findNavController().navigate(R.id.oauth_login, args) } - private fun teslaGetAccessToken(result: Bundle, codeVerifier: String) { + private fun teslaGetAccessToken(result: Bundle) { teslaAccountPreference.summary = getString(R.string.logging_in) val url = Uri.parse(result.getString("url")) val code = url.getQueryParameter("code") ?: return val okhttp = OkHttpClient.Builder().addDebugInterceptors().build() - val request = TeslaAuthenticationApi.AuthCodeRequest(code, codeVerifier) + val (clientId, clientSecret) = getString(R.string.tesla_credentials).split(":") + val request = TeslaAuthenticationApi.AuthCodeRequest( + code, + clientId = clientId, + clientSecret = clientSecret + ) lifecycleScope.launch { try { val time = Instant.now().epochSecond val response = TeslaAuthenticationApi.create(okhttp).getToken(request) - val userResponse = - TeslaOwnerApi.create(okhttp, response.accessToken).getUserInfo() +// val userResponse = +// TeslaOwnerApi.create(okhttp, response.accessToken).getUserInfo() - encryptedPrefs.teslaEmail = userResponse.response.email + encryptedPrefs.teslaEmail = "user@example.com" encryptedPrefs.teslaAccessToken = response.accessToken encryptedPrefs.teslaAccessTokenExpiry = time + response.expiresIn encryptedPrefs.teslaRefreshToken = response.refreshToken diff --git a/doc/api_keys.md b/doc/api_keys.md index 273399d04..9470b7cfe 100644 --- a/doc/api_keys.md +++ b/doc/api_keys.md @@ -149,6 +149,21 @@ in German. +### **Tesla** + +[API documentation](https://developer.tesla.com/docs/fleet-api) + +
+How to obtain an API key + +1. [Sign up](https://www.tesla.com/teslaaccount) for a Tesla account +2. In the [Tesla Developer Portal](https://developer.tesla.com/), click on "Request app access" +3. Enter the details of your app +4. You will receive a *Client ID* and *Client Secret*. Enter them both into `tesla_credentials`, + separated by a colon (`:`). + +
+ Pricing providers -----------------