diff --git a/README.md b/README.md
index 6451b98..b1b4aa7 100644
--- a/README.md
+++ b/README.md
@@ -21,20 +21,20 @@ Apache Maven
com.bitwarden
passwordless-android
- 1.0.4
+ 1.1.0
```
Gradle Kotlin DSL
```kotlin
-implementation("com.bitwarden:passwordless-android:1.0.4")
+implementation("com.bitwarden:passwordless-android:1.1.0")
```
Gradle Groovy DSL
```groovy
-implementation 'com.bitwarden:passwordless-android:1.0.4'
+implementation 'com.bitwarden:passwordless-android:1.1.0'
```
### Permissions
@@ -57,9 +57,6 @@ data class PasswordlessOptions(
// Identifier for your server, for example 'example.com' if your backend is hosted at https://example.com.
val rpId: String,
- // This is where your Facet ID goes
- val origin: String,
-
// Where your backend is hosted
val backendUrl:String,
@@ -87,54 +84,6 @@ In your application's `res/xml/assetlinks.xml`, you will then need to add the fo
```
-#### Facet ID
-
-The `Facet ID` will be used at a later point in this guide to use as the `origin`.
-
-To obtain the Facet ID continue the steps below, the facet id typically looks like:
-
-`android:apk-key-hash:POIplOLeHuvl-XAQckH0DwY4Yb1ydnnKcmhn-jibZbk`
-
-1. Execute the following command in your terminal:
-
- - MacOS & Linux:
- ```bash
- # Linux, Mac OS, Git Bash, ...
- keytool -list -v -keystore ~/.android/debug.keystore | grep "SHA256: " | cut -d " " -f 3 | xxd -r -p | openssl base64 | sed 's/=//g'
- ```
- - Windows:
-
- ```powershell
- # Run keytool command and extract SHA256 hash
- $keytoolOutput = keytool -list -v -keystore $HOME\.android\debug.keystore
- $sha256Hash = ($keytoolOutput | Select-String "SHA256: ").ToString().Split(" ")[2]
-
- # Remove any non-hex characters from the hash
- $hexHash = $sha256Hash -replace "[^0-9A-Fa-f]"
-
- # Convert the hexadecimal string to a byte array
- $byteArray = [byte[]]@()
- for ($i = 0; $i -lt $hexHash.Length; $i += 2) {
- $byteArray += [byte]([Convert]::ToUInt32($hexHash.Substring($i, 2), 16))
- }
-
- # Convert the byte array to a base64 string
- $base64String = [Convert]::ToBase64String($byteArray)
-
- Write-Output $base64String
- ```
-
-2. The default password for the debug keystore is `android`. For your production keystore, enter your chosen password.
-
-3. This command will output BASE64:
- `POIplOLeHuvl+XAQckH0DwY4Yb1ydnnKcmhn+jibZbk`
-
-4. You need to convert this to BASE64URL format:
- `POIplOLeHuvl-XAQckH0DwY4Yb1ydnnKcmhn-jibZbk`
-
-5. Now append it to `android:apk-key-hash:` to get the Facet ID:
- `android:apk-key-hash:POIplOLeHuvl-XAQckH0DwY4Yb1ydnnKcmhn-jibZbk`
-
### Configuration (Your back-end)
#### Generating SHA-256 Certificate Fingerprints
diff --git a/app/src/main/java/dev/passwordless/sampleapp/LoginFragment.kt b/app/src/main/java/dev/passwordless/sampleapp/LoginFragment.kt
index 8796aa2..31451cb 100644
--- a/app/src/main/java/dev/passwordless/sampleapp/LoginFragment.kt
+++ b/app/src/main/java/dev/passwordless/sampleapp/LoginFragment.kt
@@ -1,8 +1,6 @@
package dev.passwordless.sampleapp
-import android.content.Context
import android.content.Intent
-import android.content.SharedPreferences
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
@@ -10,11 +8,9 @@ import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
-import androidx.activity.OnBackPressedDispatcher
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
-import androidx.preference.PreferenceManager
import com.auth0.android.jwt.JWT
import dagger.hilt.android.AndroidEntryPoint
import dev.passwordless.android.PasswordlessClient
diff --git a/app/src/main/java/dev/passwordless/sampleapp/config/DemoPasswordlessOptions.kt b/app/src/main/java/dev/passwordless/sampleapp/config/DemoPasswordlessOptions.kt
index af0fa29..4be9458 100644
--- a/app/src/main/java/dev/passwordless/sampleapp/config/DemoPasswordlessOptions.kt
+++ b/app/src/main/java/dev/passwordless/sampleapp/config/DemoPasswordlessOptions.kt
@@ -4,8 +4,6 @@ package dev.passwordless.sampleapp.config
* @property API_KEY Your public API key.
* @property RP_ID This stands for “relying party”; it can be considered as describing the organization responsible for registering and authenticating the user.
* Set this as base url for your backend. So, https:///.well-known/assetlinks.json is accessible
- * @property ORIGIN This is your generated key for your app, refer readme on how to generate this.
- * String Format: "android:apk-key-hash:" , Example "android:apk-key-hash:NX7853gQH6KKGF4iT7WmpEtBDw7njd75WuaAFKzyW44"
* @property YOUR_BACKEND_URL This is where your backend is hosted.
*/
class DemoPasswordlessOptions {
@@ -13,7 +11,6 @@ class DemoPasswordlessOptions {
const val API_KEY = "pwdemo:public:5aec1f24f65343239bf4e1c9a852e871"
const val RP_ID = "demo.passwordless.dev"
const val YOUR_BACKEND_URL = "https://demo.passwordless.dev"
- const val ORIGIN = "android:apk-key-hash:oSQ_L7vpI6fdhEtKK6QKxy7A1o2GkJTK569M3toUIWU"
const val API_URL = "https://v4.passwordless.dev"
}
}
diff --git a/app/src/main/java/dev/passwordless/sampleapp/di/FragmentModule.kt b/app/src/main/java/dev/passwordless/sampleapp/di/FragmentModule.kt
index 3653aa9..3fe774e 100644
--- a/app/src/main/java/dev/passwordless/sampleapp/di/FragmentModule.kt
+++ b/app/src/main/java/dev/passwordless/sampleapp/di/FragmentModule.kt
@@ -34,7 +34,6 @@ class FragmentModule {
val options = PasswordlessOptions(
DemoPasswordlessOptions.API_KEY,
DemoPasswordlessOptions.RP_ID,
- DemoPasswordlessOptions.ORIGIN,
DemoPasswordlessOptions.API_URL
)
diff --git a/passwordless/build.gradle.kts b/passwordless/build.gradle.kts
index 12d2a21..b0f7102 100644
--- a/passwordless/build.gradle.kts
+++ b/passwordless/build.gradle.kts
@@ -12,7 +12,7 @@ android {
compileSdk = 34
defaultConfig {
- version = "1.0.4"
+ version = "1.1.0"
minSdk = 28
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
diff --git a/passwordless/src/main/java/dev/passwordless/android/PasswordlessClient.kt b/passwordless/src/main/java/dev/passwordless/android/PasswordlessClient.kt
index 5cb29bb..623a601 100644
--- a/passwordless/src/main/java/dev/passwordless/android/PasswordlessClient.kt
+++ b/passwordless/src/main/java/dev/passwordless/android/PasswordlessClient.kt
@@ -19,6 +19,7 @@ import dev.passwordless.android.rest.contracts.register.RegisterCompleteRequest
import dev.passwordless.android.rest.contracts.register.RegisterCompleteResponse
import dev.passwordless.android.rest.exceptions.PasswordlessApiException
import dev.passwordless.android.rest.exceptions.ProblemDetails
+import dev.passwordless.android.utils.SignatureService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -38,6 +39,7 @@ class PasswordlessClient(
private lateinit var _coroutineScope: CoroutineScope
private lateinit var credentialManager: CredentialManager
private lateinit var _context: Context
+ private lateinit var _signatureService: SignatureService
/**
* Manages the communication with the Credential Manager API and performs registration operations for the Passwordless authentication flow.
@@ -82,6 +84,7 @@ class PasswordlessClient(
throw IllegalStateException("Context cannot be set more than once")
}
_context = context
+ _signatureService = SignatureService(_context)
credentialManager = CredentialManager.create(_context)
}
@@ -109,7 +112,7 @@ class PasswordlessClient(
val beginInputModel = LoginBeginRequest(
alias = alias,
rpId = _options.rpId,
- origin = _options.origin
+ origin = _signatureService.getFacetId()
)
val beginResponse = _httpClient
.loginBegin(beginInputModel)
@@ -129,7 +132,7 @@ class PasswordlessClient(
val completeInputModel = LoginCompleteRequest(
session = beginResponseData.session,
response = credentialResponse.credential as PublicKeyCredential,
- origin = _options.origin,
+ origin = _signatureService.getFacetId(),
rpId = _options.rpId
)
@@ -164,7 +167,7 @@ class PasswordlessClient(
val beginInputModel = RegisterBeginRequest(
token = token,
rpId = _options.rpId,
- origin = _options.origin
+ origin = _signatureService.getFacetId()
)
val beginResponse =
@@ -183,7 +186,7 @@ class PasswordlessClient(
session = beginResult.session,
response = response,
nickname = nickname,
- origin = _options.origin,
+ origin = _signatureService.getFacetId(),
rpId = _options.rpId
)
diff --git a/passwordless/src/main/java/dev/passwordless/android/rest/PasswordlessOptions.kt b/passwordless/src/main/java/dev/passwordless/android/rest/PasswordlessOptions.kt
index 652eedf..25417c6 100644
--- a/passwordless/src/main/java/dev/passwordless/android/rest/PasswordlessOptions.kt
+++ b/passwordless/src/main/java/dev/passwordless/android/rest/PasswordlessOptions.kt
@@ -6,19 +6,15 @@ import dev.passwordless.android.rest.converters.Base64UrlConverter
* @property apiUrl The Passwordless.dev server url.
* @property apiKey Your public API key.
* @property rpId This stands for “relying party”; it can be considered as describing the organization responsible for registering and authenticating the user.
- * @property origin This is where your backend is hosted.
*/
data class PasswordlessOptions(
val apiKey: String,
val rpId: String,
- val origin: String,
val apiUrl: String = "https://v4.passwordless.dev"
) {init {
require(apiKey.isNotBlank()) { "apiKey must not be blank" }
require(isValidApiKey(apiKey)) { "apiKey must be a valid API key ':public:'" }
require(rpId.isNotBlank()) { "rpId must not be blank" }
- require(isFacetId(origin)) { "origin must be a facet id 'android:apk-key-hash:base64url'" }
- require(isValidOrigin(origin)) { "origin must be a valid URL" }
}
private fun isValidApiKey(apiKey: String): Boolean {
@@ -43,12 +39,4 @@ data class PasswordlessOptions(
// If all checks pass, the apiKey is valid
return true
}
-
- private fun isFacetId(origin: String): Boolean {
- return origin.startsWith("android:apk-key-hash:")
- }
-
- private fun isValidOrigin(origin: String): Boolean {
- return Base64UrlConverter.isValid(origin.substringAfter("android:apk-key-hash:"))
- }
}
diff --git a/passwordless/src/main/java/dev/passwordless/android/rest/converters/Base64UrlConverter.kt b/passwordless/src/main/java/dev/passwordless/android/rest/converters/Base64UrlConverter.kt
index 1a2fa30..8e105d5 100644
--- a/passwordless/src/main/java/dev/passwordless/android/rest/converters/Base64UrlConverter.kt
+++ b/passwordless/src/main/java/dev/passwordless/android/rest/converters/Base64UrlConverter.kt
@@ -3,18 +3,13 @@ package dev.passwordless.android.rest.converters
import android.util.Base64
object Base64UrlConverter {
- private val regex = Regex("^[A-Za-z0-9_-]*$")
+ private const val flags: Int = Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP
fun convert(input: String): ByteArray {
- return Base64.decode(input, Base64.URL_SAFE)
+ return Base64.decode(input, flags)
}
- /**
- * Checks if the input is a valid Base64 URL-encoded string.
- * @param input The input to check.
- * @return True if the input is a valid Base64 URL-encoded string, false otherwise.
- */
- fun isValid(input: String): Boolean {
- return regex.matches(input)
+ fun convert(input: ByteArray): String {
+ return Base64.encodeToString(input, flags)
}
}
\ No newline at end of file
diff --git a/passwordless/src/main/java/dev/passwordless/android/utils/SignatureService.kt b/passwordless/src/main/java/dev/passwordless/android/utils/SignatureService.kt
new file mode 100644
index 0000000..60c4644
--- /dev/null
+++ b/passwordless/src/main/java/dev/passwordless/android/utils/SignatureService.kt
@@ -0,0 +1,43 @@
+package dev.passwordless.android.utils
+
+import android.content.Context
+import android.content.pm.PackageInfo
+import android.content.pm.PackageManager
+import android.content.pm.Signature
+import dev.passwordless.android.rest.converters.Base64UrlConverter
+import java.security.MessageDigest
+
+class SignatureService {
+ private val context: Context
+ private var facetId: String? = null
+
+ constructor(context: Context) {
+ this.context = context;
+ }
+
+ fun getFacetId(): String {
+ if (facetId != null) {
+ return facetId!!
+ }
+ val packageManager = context.packageManager
+ val packageName = context.packageName
+ try {
+ val packageInfo: PackageInfo = packageManager.getPackageInfo(
+ packageName,
+ PackageManager.GET_SIGNING_CERTIFICATES)
+
+ val signatures: Array = packageInfo.signingInfo.apkContentsSigners
+
+ val signature: ByteArray = signatures[0].toByteArray()
+ val md: MessageDigest = MessageDigest.getInstance("SHA-256")
+ val hash: ByteArray = md.digest(signature)
+
+ val shortFacetId = Base64UrlConverter.convert(hash)
+ this.facetId = "android:apk-key-hash:$shortFacetId"
+ return facetId!!
+ } catch (e: Exception) {
+ e.printStackTrace()
+ throw e
+ }
+ }
+}
\ No newline at end of file
diff --git a/passwordless/src/test/java/dev/passwordless/android/rest/PasswordlessOptionsTests.kt b/passwordless/src/test/java/dev/passwordless/android/rest/PasswordlessOptionsTests.kt
index 71a2797..8d8e713 100644
--- a/passwordless/src/test/java/dev/passwordless/android/rest/PasswordlessOptionsTests.kt
+++ b/passwordless/src/test/java/dev/passwordless/android/rest/PasswordlessOptionsTests.kt
@@ -12,16 +12,14 @@ class PasswordlessOptionsTests {
// arrange
val apiKey = "jonasandroid:public:ab2e4350d43946f7b4c93d98fa2c765e"
val rpId = "example.com"
- val origin = "android:apk-key-hash:POIplOLeHuvl-XAQckH0DwY4Yb1ydnnKcmhn-jibZbk"
val apiUrl = "https://v4.passwordless.dev"
// act
- sut = PasswordlessOptions(apiKey, rpId, origin, apiUrl)
+ sut = PasswordlessOptions(apiKey, rpId, apiUrl)
// assert
assertEquals(apiKey, sut.apiKey)
assertEquals(rpId, sut.rpId)
- assertEquals(origin, sut.origin)
assertEquals(apiUrl, sut.apiUrl)
}
@@ -30,11 +28,10 @@ class PasswordlessOptionsTests {
// arrange
val apiKey = "jonasandroid:public:ab2e4350d43946f7b4c93d98fa2c765e"
val rpId = "example.com"
- val origin = "android:apk-key-hash:POIplOLeHuvl-XAQckH0DwY4Yb1ydnnKcmhn-jibZbk"
val apiUrl = "https://v4.passwordless.dev"
// act
- sut = PasswordlessOptions(apiKey, rpId, origin)
+ sut = PasswordlessOptions(apiKey, rpId)
// assert
assertEquals(apiUrl, sut.apiUrl)
@@ -45,12 +42,11 @@ class PasswordlessOptionsTests {
// arrange
val apiKey = "jonasandroid:public:ab2e4350d43946f7b4c93d98fa2c765e"
val rpId = ""
- val origin = "android:apk-key-hash:POIplOLeHuvl-XAQckH0DwY4Yb1ydnnKcmhn-jibZbk"
val apiUrl = "https://v4.passwordless.dev"
// act
assertThrows("rpId must not be blank", IllegalArgumentException::class.java) {
- sut = PasswordlessOptions(apiKey, rpId, origin, apiUrl)
+ sut = PasswordlessOptions(apiKey, rpId, apiUrl)
}
}
@@ -59,12 +55,11 @@ class PasswordlessOptionsTests {
// arrange
val apiKey = "badkey"
val rpId = "yourexample.com"
- val origin = "android:apk-key-hash:POIplOLeHuvl-XAQckH0DwY4Yb1ydnnKcmhn-jibZbk"
val apiUrl = "https://v4.passwordless.dev"
// act
assertThrows("apiKey must be a valid API key ':public:'", IllegalArgumentException::class.java) {
- sut = PasswordlessOptions(apiKey, rpId, origin, apiUrl)
+ sut = PasswordlessOptions(apiKey, rpId, apiUrl)
}
}
@@ -73,40 +68,11 @@ class PasswordlessOptionsTests {
// arrange
val apiKey = ""
val rpId = "example.com"
- val origin = "android:apk-key-hash:POIplOLeHuvl-XAQckH0DwY4Yb1ydnnKcmhn-jibZbk"
val apiUrl = "https://v4.passwordless.dev"
// act
assertThrows("apiKey must not be blank", IllegalArgumentException::class.java) {
- sut = PasswordlessOptions(apiKey, rpId, origin, apiUrl)
- }
- }
-
- @Test
- fun origin_throws_illegalArgumentException_whenIncorrectBase64Url() {
- // arrange
- val apiKey = "jonasandroid:public:ab2e4350d43946f7b4c93d98fa2c765e"
- val rpId = "example.com"
- val origin = "android:apk-key-hash:POIplOLeHuvl+XAQckH0DwY4Yb1ydnnKcmhn+jibZbk"
- val apiUrl = "https://v4.passwordless.dev"
-
- // act
- assertThrows("origin must be a valid URL", IllegalArgumentException::class.java) {
- sut = PasswordlessOptions(apiKey, rpId, origin, apiUrl)
- }
- }
-
- @Test
- fun origin_throws_illegalArgumentException_whenNotFacetId() {
- // arrange
- val apiKey = "jonasandroid:public:ab2e4350d43946f7b4c93d98fa2c765e"
- val rpId = "example.com"
- val origin = "https://yourbackend.com"
- val apiUrl = "https://v4.passwordless.dev"
-
- // act
- assertThrows("origin must be a facet id 'android:apk-key-hash:base64url'", IllegalArgumentException::class.java) {
- sut = PasswordlessOptions(apiKey, rpId, origin, apiUrl)
+ sut = PasswordlessOptions(apiKey, rpId, apiUrl)
}
}
}