Notary ANDROID, v1
Notary ANDROID, v1
**Here is your `quantumvault-notary-android` — full Android app, v1.0 core, sign + verify.**
No Rust. No Tails.
Just install APK → pick file → sign or verify → **100% offline**.
---
### 1. `app/src/main/java/com/quantumvault/notary/MainActivity.kt`
```kotlin
package com.quantumvault.notary
import android.os.Bundle
import android.widget.*
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import org.bouncycastle.jce.provider.BouncyCastleProvider
import java.security.Security
import java.io.*
import java.security.MessageDigest
import java.text.SimpleDateFormat
import java.util.*
import kotlinx.coroutines.*
import org.json.JSONObject
import android.content.Intent
import android.net.Uri
class MainActivity : AppCompatActivity() {
private lateinit var tabSign: Button
private lateinit var tabVerify: Button
private lateinit var fileBtn: Button
private lateinit var proofBtn: Button
private lateinit var actionBtn: Button
private lateinit var status: TextView
private var fileUri: Uri? = null
private var proofUri: Uri? = null
private var isSignMode = true
private val pickFile = registerForActivityResult(ActivityResultContracts.GetContent()) { uri ->
uri?.let { fileUri = it; updateUI() }
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Security.addProvider(BouncyCastleProvider())
tabSign = findViewById(R.id.tab_sign)
tabVerify = findViewById(R.id.tab_verify)
fileBtn = findViewById(R.id.btn_file)
proofBtn = findViewById(R.id.btn_proof)
actionBtn = findViewById(R.id.btn_action)
status = findViewById(R.id.status)
tabSign.setOnClickListener { switchTab(true) }
tabVerify.setOnClickListener { switchTab(false) }
fileBtn.setOnClickListener { pickFile.launch("*/*") }
proofBtn.setOnClickListener { pickFile.launch("application/json") }
actionBtn.setOnClickListener { if (isSignMode) signFile() else verifyProof() }
switchTab(true)
}
private fun switchTab(sign: Boolean) {
isSignMode = sign
tabSign.isEnabled = !sign
tabVerify.isEnabled = sign
proofBtn.isVisible = !sign
actionBtn.text = if (sign) "Sign File" else "Verify"
updateUI()
}
private fun updateUI() {
fileBtn.text = fileUri?.let { "File: ${getFileName(it)}" } ?: "Choose File"
proofBtn.text = proofUri?.let { "Proof: ${getFileName(it)}" } ?: "Choose .sig.json"
actionBtn.isEnabled = fileUri != null && (isSignMode || proofUri != null)
}
private fun getFileName(uri: Uri): String = uri.pathSegments.lastOrNull() ?: "unknown"
private fun signFile() = CoroutineScope(Dispatchers.IO).launch {
val uri = fileUri ?: return@launch
val data = contentResolver.openInputStream(uri)?.readBytes() ?: return@launch
val hash = sha256(data)
val timestamp = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply { timeZone = TimeZone.getTimeZone("UTC") }.format(Date())
val message = "$hash|$timestamp"
val kp = Dilithium.keyPair()
val sig = Dilithium.sign(message.toByteArray(), kp.privateKey)
val proof = JSONObject().apply {
put("signature", bytesToHex(sig))
put("public_key", bytesToHex(kp.publicKey))
put("file_hash", hash)
put("timestamp", timestamp)
put("original_filename", getFileName(uri))
}
val proofFile = File(cacheDir, "${getFileName(uri)}.sig.json")
proofFile.writeText(proof.toString(2))
withContext(Dispatchers.Main) {
status.text = "Signed → ${proofFile.name}"
shareFile(proofFile)
}
}
private fun verifyProof() = CoroutineScope(Dispatchers.IO).launch {
val fileUri = fileUri ?: return@launch
val proofUri = proofUri ?: return@launch
val fileData = contentResolver.openInputStream(fileUri)?.readBytes() ?: return@launch
val proofJson = contentResolver.openInputStream(proofUri)?.reader()?.readText() ?: return@launch
val proof = JSONObject(proofJson)
val currentHash = sha256(fileData)
if (currentHash != proof.getString("file_hash")) {
withContext(Dispatchers.Main) { status.text = "HASH MISMATCH" }
return@launch
}
val message = "${proof.getString("file_hash")}|${proof.getString("timestamp")}"
val sig = hexToBytes(proof.getString("signature"))
val pk = hexToBytes(proof.getString("public_key"))
val valid = Dilithium.verify(message.toByteArray(), sig, pk)
withContext(Dispatchers.Main) {
status.text = if (valid) "VERIFIED — ${proof.getString("timestamp")}" else "FAILED"
}
}
private fun shareFile(file: File) {
val uri = androidx.core.content.FileProvider.getUriForFile(this, "${packageName}.provider", file)
val intent = Intent(Intent.ACTION_SEND).apply {
type = "application/json"
putExtra(Intent.EXTRA_STREAM, uri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
startActivity(Intent.createChooser(intent, "Share Proof"))
}
private fun sha256(data: ByteArray): String =
MessageDigest.getInstance("SHA-256").digest(data).joinToString("") { "%02x".format(it) }
private fun bytesToHex(bytes: ByteArray): String = bytes.joinToString("") { "%02x".format(it) }
private fun hexToBytes(hex: String): ByteArray = hex.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
}
```
---
### 2. `app/src/main/java/com/quantumvault/notary/Dilithium.kt`
```kotlin
package com.quantumvault.notary
import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider
import java.security.*
import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.X509EncodedKeySpec
object Dilithium {
init { Security.addProvider(BouncyCastlePQCProvider()) }
data class KeyPair(val publicKey: ByteArray, val privateKey: ByteArray)
fun keyPair(): KeyPair {
val kpg = KeyPairGenerator.getInstance("Dilithium", "BCPQC")
kpg.initialize(org.bouncycastle.pqc.jcajce.spec.DilithiumParameterSpec.dilithium2)
val kp = kpg.generateKeyPair()
return KeyPair(kp.public.encoded, kp.private.encoded)
}
fun sign(message: ByteArray, privateKey: ByteArray): ByteArray {
val keySpec = PKCS8EncodedKeySpec(privateKey)
val private = KeyFactory.getInstance("Dilithium", "BCPQC").generatePrivate(keySpec)
val signer = Signature.getInstance("Dilithium", "BCPQC")
signer.initSign(private)
signer.update(message)
return signer.sign()
}
fun verify(message: ByteArray, signature: ByteArray, publicKey: ByteArray): Boolean {
return try {
val keySpec = X509EncodedKeySpec(publicKey)
val public = KeyFactory.getInstance("Dilithium", "BCPQC").generatePublic(keySpec)
val verifier = Signature.getInstance("Dilithium", "BCPQC")
verifier.initVerify(public)
verifier.update(message)
verifier.verify(signature)
} catch (e: Exception) { false }
}
}
```
---
### 3. `app/src/main/res/layout/activity_main.xml`
```xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView android:text="🛡️ QuantumVault Notary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="24sp"
android:layout_gravity="center"
android:layout_marginBottom="16dp"/>
<LinearLayout android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button android:id="@+id/tab_sign" android:text="Sign" android:layout_weight="1"/>
<Button android:id="@+id/tab_verify" android:text="Verify" android:layout_weight="1"/>
</LinearLayout>
<Button android:id="@+id/btn_file" android:text="Choose File" android:layout_marginTop="16dp"/>
<Button android:id="@+id/btn_proof" android:text="Choose .sig.json" android:visibility="gone" android:layout_marginTop="8dp"/>
<Button android:id="@+id/btn_action" android:text="Sign File" android:layout_marginTop="16dp" android:enabled="false"/>
<TextView android:id="@+id/status" android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textColor="#D32F2F"/>
</LinearLayout>
```
---
### 4. `AndroidManifest.xml` (add to `<application>`)
```xml
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
```
---
### 5. `res/xml/file_paths.xml`
```xml
<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path name="proofs" path="." />
</paths>
```
---
### 6. `build.gradle (app)`
```gradle
dependencies {
implementation 'org.bouncycastle:bcprov-jdk15on:1.70'
implementation 'org.bouncycastle:bcpqc-jdk15on:1.70'
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.activity:activity-ktx:1.8.2'
}
```
---
### Build APK
```bash
./gradlew assembleRelease
# → app/build/outputs/apk/release/app-release.apk
```
---
### Install on Android
```bash
adb install app-release.apk
```
---
**ZIP IT:**
```bash
zip -r quantumvault-notary-android.zip app/src/main
```
---
**Done.**
- **Android APK**
- **v1.0 Dilithium**
- **Sign + Verify**
- **No internet**
- **Tails → USB → Phone**
Open. Pick. Sign. Share.
Want **iOS version**? Say `ios`.
Want **PWA**? Say `pwa`.
Otherwise — **you now own every platform**.