Skip to content

Instantly share code, notes, and snippets.

@sandys
Last active September 7, 2024 07:12
Show Gist options
  • Save sandys/d7152159fcfc61ef9361b1cef26fd0a4 to your computer and use it in GitHub Desktop.
Save sandys/d7152159fcfc61ef9361b1cef26fd0a4 to your computer and use it in GitHub Desktop.
finternet integration into browser checkout

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 :

  1. shopify checkout page has webauthn integrated into it
  2. when u click the "pay now with finternet" button on the shopify site
  3. a webauthn+simd48 key is created using the users fingerprint on the phone (which wootzapp pops up
  4. then the user uses this key + wallet key to sign the finternet MPC contract
  5. 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.

2. Objectives

  • 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

3. Technical Architecture

3.1 Android Application

The Android application will consist of the following key components:

  1. WebView to load the web interface
  2. WebAuthn integration using the FIDO2 API
  3. SIMD48 processor for secure key derivation
  4. Solana transaction handler

MainActivity.kt

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
    }
}

3.2 SIMD48 Processor

The SIMD48 processor will handle the secure derivation of keys from WebAuthn data and partial transaction signing.

SIMD48SolanaProcessor.kt

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
    }
}

3.3 Web Application

The web application will provide the user interface and initiate WebAuthn requests.

index.html

<!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>

app.js

(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;
        }
    });
})();

3.4 Solana Multi-Sig Contract

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>

4.2.2 Update MainActivity.kt

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()
}

4.3 Benefits of Wootzapp Custom URL Format

  1. Simplicity for Websites: Websites can initiate WebAuthn-secured Solana transactions without implementing complex libraries or code.
  2. Consistent User Experience: The app handles the WebAuthn process, ensuring a consistent and secure experience across different websites.
  3. Flexibility: The URL format can be easily extended to support additional parameters or transaction types in the future.
  4. 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment