Android WebAuthn + SIMD48 Solana Multi-Sig Application : Ecommerce Checkout integrated into browser using Finternet
We propose the development of an Android application that leverages WebAuthn, SIMD48 processing, and Solana's multi-signature capabilities to create a secure and user-friendly Finternet browser. This browser will enable anyone to open ecommerce websites, process Finternet WebAuthn requests using the device's fingerprint sensor, and complete transactions through a Solana multi-sig contract.
The ideal finternet checkout flow on a browser would be :
- shopify checkout page has webauthn integrated into it
- when u click the "pay now with finternet" button on the shopify site
- a webauthn+simd48 key is created using the users fingerprint on the phone (which wootzapp pops up
- then the user uses this key + wallet key to sign the finternet MPC contract
- the finternet mpc contract moves the funds from my wallet to the shopify merchant's wallet.
this is what we are making possible through wootzapp.
- Create a secure browser for Solana multi-sig transactions
- Implement WebAuthn for biometric authentication
- Utilize SIMD48 processing for enhanced security
- Provide a seamless user experience through a web-based interface in the browser renderer
- Develop a Solana multi-sig smart contract for secure transaction execution
The Android application will consist of the following key components:
- WebView to load the web interface
- WebAuthn integration using the FIDO2 API
- SIMD48 processor for secure key derivation
- Solana transaction handler
import android.os.Bundle
import android.webkit.WebView
import android.webkit.WebViewClient
import android.webkit.JavascriptInterface
import androidx.appcompat.app.AppCompatActivity
import com.google.android.gms.fido.fido2.Fido2ApiClient
import com.google.android.gms.fido.fido2.api.common.AuthenticatorAssertionResponse
import org.json.JSONObject
import android.util.Base64
class MainActivity : AppCompatActivity() {
private lateinit var webView: WebView
private lateinit var fido2ApiClient: Fido2ApiClient
private lateinit var simd48Processor: SIMD48SolanaProcessor
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
webView = findViewById(R.id.webView)
fido2ApiClient = Fido2ApiClient.createInstance(this)
simd48Processor = SIMD48SolanaProcessor()
setupWebView()
}
private fun setupWebView() {
webView.settings.javaScriptEnabled = true
webView.addJavascriptInterface(WebAuthnJSInterface(), "AndroidWebAuthn")
webView.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
webView.evaluateJavascript("""
navigator.credentials.create = function(options) {
return new Promise((resolve, reject) => {
AndroidWebAuthn.createCredential(JSON.stringify(options), 'create');
});
};
navigator.credentials.get = function(options) {
return new Promise((resolve, reject) => {
AndroidWebAuthn.getCredential(JSON.stringify(options), 'get');
});
};
""".trimIndent(), null)
}
}
webView.loadUrl("https://your-web-app-url.com")
}
inner class WebAuthnJSInterface {
@JavascriptInterface
fun createCredential(optionsJson: String, type: String) {
runOnUiThread {
handleWebAuthnRequest(optionsJson, type)
}
}
@JavascriptInterface
fun getCredential(optionsJson: String, type: String) {
runOnUiThread {
handleWebAuthnRequest(optionsJson, type)
}
}
@JavascriptInterface
fun signTransaction(transactionData: String) {
runOnUiThread {
partialSignTransaction(transactionData)
}
}
}
private fun handleWebAuthnRequest(optionsJson: String, type: String) {
val options = JSONObject(optionsJson)
val pendingIntent = fido2ApiClient.getSignPendingIntent(/* create PublicKeyCredentialRequestOptions from options */)
startIntentSenderForResult(pendingIntent.intentSender, REQUEST_CODE_FIDO, null, 0, 0, 0)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_CODE_FIDO && resultCode == Activity.RESULT_OK) {
data?.let { intent ->
val authenticatorResponse = AuthenticatorAssertionResponse.deserializeFromBytes(
intent.getByteArrayExtra(Fido.FIDO2_KEY_RESPONSE_EXTRA)
)
processWebAuthnResponse(authenticatorResponse)
}
}
}
private fun processWebAuthnResponse(response: AuthenticatorAssertionResponse) {
val combinedData = response.authenticatorData +
response.signature +
response.clientDataJSON
val (solanaPublicKey, derivedKey) = simd48Processor.processWebAuthnDataForSolana(combinedData)
webView.evaluateJavascript("""
window.postMessage({
type: 'SOLANA_PUBKEY',
publicKey: '${solanaPublicKey.toBase58()}'
}, '*');
""".trimIndent(), null)
securelyStoreKey(derivedKey)
}
private fun securelyStoreKey(key: ByteArray) {
// Implement secure key storage, e.g., using Android Keystore
}
private fun partialSignTransaction(transactionData: String) {
val transaction = Transaction.from(Base64.decode(transactionData, Base64.DEFAULT))
val derivedKey = securelyRetrieveKey()
val partiallySignedTransaction = simd48Processor.partialSignTransaction(transaction, derivedKey)
val partiallySignedTransactionData = Base64.encodeToString(partiallySignedTransaction.serialize(), Base64.DEFAULT)
webView.evaluateJavascript("""
window.postMessage({
type: 'PARTIAL_SIGNED_TRANSACTION',
transaction: '$partiallySignedTransactionData'
}, '*');
""".trimIndent(), null)
}
private fun securelyRetrieveKey(): ByteArray {
// Implement secure key retrieval from Android Keystore
return ByteArray(32) // Placeholder
}
}
The SIMD48 processor will handle the secure derivation of keys from WebAuthn data and partial transaction signing.
import org.bouncycastle.crypto.digests.SHA256Digest
import org.bouncycastle.crypto.generators.HKDFBytesGenerator
import org.bouncycastle.crypto.params.HKDFParameters
import java.nio.ByteBuffer
import java.security.SecureRandom
import org.bitcoinj.core.ECKey
import org.solana.core.PublicKey
import org.solana.core.Transaction
class SIMD48SolanaProcessor {
companion object {
private const val SIMD_CHUNK_SIZE = 6 // 48 bits
private const val DERIVED_KEY_LENGTH = 32 // 256 bits
}
fun processWebAuthnDataForSolana(webAuthnData: ByteArray): Pair<PublicKey, ByteArray> {
val simd48Result = applySIMD48(webAuthnData)
val derivedKey = deriveKeyFromSIMD48(simd48Result)
val publicKey = generateSolanaPublicKey(derivedKey)
return Pair(publicKey, derivedKey)
}
private fun applySIMD48(data: ByteArray): List<Long> {
return data.toList()
.chunked(SIMD_CHUNK_SIZE)
.map { chunk ->
chunk.foldIndexed(0L) { index, acc, byte ->
acc or (byte.toLong() and 0xFFL shl (8 * index))
}
}
}
private fun deriveKeyFromSIMD48(simd48Result: List<Long>): ByteArray {
val combinedData = ByteBuffer.allocate(simd48Result.size * Long.SIZE_BYTES)
.apply { simd48Result.forEach { putLong(it) } }
.array()
val hkdf = HKDFBytesGenerator(SHA256Digest())
val salt = SecureRandom().generateSeed(16)
val hkdfParams = HKDFParameters(combinedData, salt, null)
hkdf.init(hkdfParams)
val derivedKey = ByteArray(DERIVED_KEY_LENGTH)
hkdf.generateBytes(derivedKey, 0, DERIVED_KEY_LENGTH)
return derivedKey
}
private fun generateSolanaPublicKey(derivedKey: ByteArray): PublicKey {
val ecKey = ECKey.fromPrivate(derivedKey)
val publicKeyBytes = ecKey.pubKey
return PublicKey(publicKeyBytes)
}
fun partialSignTransaction(transaction: Transaction, derivedKey: ByteArray): Transaction {
val keyPair = ECKey.fromPrivate(derivedKey)
val partialSignature = keyPair.sign(transaction.serializeMessage())
transaction.addSignature(keyPair.publicKey, partialSignature.encodeToDER())
return transaction
}
}
The web application will provide the user interface and initiate WebAuthn requests.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebAuthn Solana Multi-Sig Demo</title>
</head>
<body>
<h1>WebAuthn Solana Multi-Sig Demo</h1>
<button id="signButton">Sign with WebAuthn</button>
<div id="result"></div>
<script src="https://unpkg.com/@solana/web3.js@latest/lib/index.iife.min.js"></script>
<script src="app.js"></script>
</body>
</html>
(async () => {
const signButton = document.getElementById('signButton');
const resultDiv = document.getElementById('result');
const connection = new solanaWeb3.Connection('https://api.devnet.solana.com');
const multiSigProgramId = new solanaWeb3.PublicKey('MULTI_SIG_PROGRAM_ID_HERE');
function createChallenge() {
return crypto.getRandomValues(new Uint8Array(32));
}
async function initiateWebAuthn() {
const challenge = createChallenge();
const publicKeyCredentialRequestOptions = {
challenge: challenge,
rpId: window.location.hostname,
timeout: 60000,
userVerification: 'required'
};
try {
const assertion = await navigator.credentials.get({
publicKey: publicKeyCredentialRequestOptions
});
return assertion;
} catch (error) {
console.error('Error during WebAuthn:', error);
throw error;
}
}
async function handleWebAuthnAndCreateTransaction(assertion) {
return new Promise((resolve) => {
window.addEventListener('message', async (event) => {
if (event.data.type === 'SOLANA_PUBKEY') {
const webAuthnPublicKey = new solanaWeb3.PublicKey(event.data.publicKey);
const otherSignerPublicKey = new solanaWeb3.PublicKey('OTHER_SIGNER_PUBKEY_HERE');
const transaction = await createMultiSigTransaction(webAuthnPublicKey, otherSignerPublicKey);
resolve(transaction);
}
}, { once: true });
});
}
async function createMultiSigTransaction(webAuthnPublicKey, otherSignerPublicKey) {
const [multiSigAccount] = await solanaWeb3.PublicKey.findProgramAddress(
[webAuthnPublicKey.toBuffer(), otherSignerPublicKey.toBuffer()],
multiSigProgramId
);
const transaction = new solanaWeb3.Transaction().add(
new solanaWeb3.TransactionInstruction({
keys: [
{ pubkey: multiSigAccount, isSigner: false, isWritable: true },
{ pubkey: webAuthnPublicKey, isSigner: true, isWritable: false },
{ pubkey: otherSignerPublicKey, isSigner: false, isWritable: false },
{ pubkey: solanaWeb3.SystemProgram.programId, isSigner: false, isWritable: false },
],
programId: multiSigProgramId,
data: Buffer.from([/* Instruction data */])
})
);
return transaction;
}
async function partialSignTransaction(transaction) {
const serializedTransaction = transaction.serialize();
const base64Transaction = btoa(String.fromCharCode.apply(null, serializedTransaction));
return new Promise((resolve) => {
window.AndroidWebAuthn.signTransaction(base64Transaction);
window.addEventListener('message', (event) => {
if (event.data.type === 'PARTIAL_SIGNED_TRANSACTION') {
const partiallySignedTransaction = solanaWeb3.Transaction.from(
Buffer.from(atob(event.data.transaction), 'binary')
);
resolve(partiallySignedTransaction);
}
}, { once: true });
});
}
signButton.addEventListener('click', async () => {
try {
resultDiv.textContent = 'Initiating WebAuthn...';
const assertion = await initiateWebAuthn();
resultDiv.textContent = 'WebAuthn successful. Creating Solana multi-sig transaction...';
const transaction = await handleWebAuthnAndCreateTransaction(assertion);
resultDiv.textContent = 'Partially signing transaction...';
const partiallySignedTransaction = await partialSignTransaction(transaction);
resultDiv.textContent = 'Solana multi-sig transaction partially signed. Ready for second signature.';
console.log('Partially Signed Transaction:', partiallySignedTransaction);
// Here you would typically send the partially signed transaction to a backend service
// for the other signer to complete the multi-sig process
} catch (error) {
resultDiv.textContent = 'Error: ' + error.message;
}
});
})();
The Solana multi-sig contract will be implemented in Rust. We could use one of the finternet contracts (if compatible with SIMD48 already). Or create our own.
use solana_program::{
account_info::AccountInfo,
entrypoint,
entrypoint::ProgramResult,
pubkey::Pubkey,
msg,
program_error::ProgramError,
sysvar
[Previous sections remain unchanged]
## 4. Wootzapp Custom URL Format
To simplify the integration of WebAuthn and Solana transactions for websites, we propose implementing a custom URL format called "Wootzapp custom URL format". This format allows websites to initiate transactions without including WebAuthn libraries or any specific code.
NOTE: this will mean that section 3.3 would be optional. Existing websites can use finternet without substantial changes.
### 4.1 URL Format Specification
The custom URL format follows this structure:
wz+finternet://///<destination_type>/<destination_address>
Example:
wz+finternet://transfer/1000/SOL/dest/0x012334566
Where:
1. URL scheme is `wz+finternet`
2. The method is `transfer` (for fund transfers)
3. The amount is `1000`
4. The token is `SOL`
5. The destination type is `dest` (for destination address)
6. The destination address is `0x012334566`
### 4.2 Implementation in Android Application
To implement this feature, we need to modify our Android application to recognize and handle these custom URLs. Here's how we can achieve this:
1. Update the Android Manifest to register the custom URL scheme.
2. Implement a URL intent filter in our main activity.
3. Parse the custom URL and initiate the WebAuthn process.
#### 4.2.1 Update AndroidManifest.xml
Add the following intent filter to the main activity in `AndroidManifest.xml`:
```xml
<activity android:name=".MainActivity">
<!-- Existing intent filters -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="wz+finternet" />
</intent-filter>
</activity>
Add the following methods to MainActivity.kt
:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Existing setup code...
handleIntent(intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleIntent(intent)
}
private fun handleIntent(intent: Intent) {
when (intent.action) {
Intent.ACTION_VIEW -> {
val uri = intent.data
if (uri != null && uri.scheme == "wz+finternet") {
handleWootzappUrl(uri)
}
}
}
}
private fun handleWootzappUrl(uri: Uri) {
val segments = uri.pathSegments
if (segments.size >= 5 && segments[0] == "transfer") {
val amount = segments[1].toLongOrNull()
val token = segments[2]
val destinationType = segments[3]
val destinationAddress = segments[4]
if (amount != null && destinationType == "dest") {
initiateWebAuthnTransaction(amount, token, destinationAddress)
} else {
showError("Invalid Wootzapp URL format")
}
} else {
showError("Invalid Wootzapp URL format")
}
}
private fun initiateWebAuthnTransaction(amount: Long, token: String, destinationAddress: String) {
// Inject WebAuthn libraries into WebView
injectWebAuthnLibraries()
// Create and load a minimal HTML page with the transaction details
val transactionHtml = """
<!DOCTYPE html>
<html>
<body>
<h1>Confirm Transaction</h1>
<p>Amount: $amount $token</p>
<p>Destination: $destinationAddress</p>
<button onclick="initiateWebAuthn()">Confirm with WebAuthn</button>
<script>
function initiateWebAuthn() {
// WebAuthn code will be injected here
AndroidWebAuthn.startWebAuthnProcess('$amount', '$token', '$destinationAddress');
}
</script>
</body>
</html>
""".trimIndent()
webView.loadDataWithBaseURL(null, transactionHtml, "text/html", "UTF-8", null)
}
private fun injectWebAuthnLibraries() {
val webAuthnScript = """
// Inject necessary WebAuthn libraries and polyfills
// This is a simplified example and should be expanded with actual WebAuthn code
navigator.credentials = navigator.credentials || {};
navigator.credentials.create = function(options) {
return AndroidWebAuthn.createCredential(JSON.stringify(options));
};
navigator.credentials.get = function(options) {
return AndroidWebAuthn.getCredential(JSON.stringify(options));
};
""".trimIndent()
webView.evaluateJavascript(webAuthnScript, null)
}
inner class WebAuthnJSInterface {
// Existing methods...
@JavascriptInterface
fun startWebAuthnProcess(amount: String, token: String, destinationAddress: String) {
// Initiate the WebAuthn process with the transaction details
// This method will be called from the injected HTML
runOnUiThread {
// Start the WebAuthn process as before, but with the provided transaction details
initiateWebAuthnForTransaction(amount, token, destinationAddress)
}
}
}
private fun initiateWebAuthnForTransaction(amount: String, token: String, destinationAddress: String) {
// Implement the WebAuthn process for the specific transaction
// This would be similar to the previous WebAuthn implementation,
// but tailored for the specific transaction details
}
private fun showError(message: String) {
// Show an error message to the user
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
}
- Simplicity for Websites: Websites can initiate WebAuthn-secured Solana transactions without implementing complex libraries or code.
- Consistent User Experience: The app handles the WebAuthn process, ensuring a consistent and secure experience across different websites.
- Flexibility: The URL format can be easily extended to support additional parameters or transaction types in the future.
- Enhanced Security: By handling the WebAuthn process within the app, we reduce the risk of malicious websites tampering with the authentication flow.
- Ensure that the WebView is properly sandboxed to prevent potential security vulnerabilities.
This Wootzapp custom URL format provides a streamlined way for websites to integrate with our WebAuthn and Solana multi-sig functionality, significantly lowering the barrier to entry for potential integrators.