feat(mobile): reconstruir+ampliar binding gomobile para paridad con la web

El wrapper mobile/unibus.go se habia perdido del repo (solo quedaba compilado
en el .aar del 5 jun). Se reconstruye y amplia con:

- Wallet BIP39 determinista: NewMnemonic, ValidateMnemonic, DeriveAndSaveIdentity.
  Deriva la MISMA identidad que uniweb (web/src/wallet/derive.ts): PBKDF2-BIP39 ->
  HKDF-SHA256(info unibus-sign-v1 / unibus-kex-v1) -> Ed25519 + X25519. Test de
  paridad contra el vector de oro (mnemonica abandon...about -> sign_pub
  34302746...b3c8) garantiza misma cuenta web<->movil byte a byte.
- Selector de salas: Session.ListMyRooms() -> JSON [{id,subject,mode,role}].
- Nombres legibles: Session.Directory() + Client.Directory()/EndpointID() nuevos
  en pkg/client (GET /directory firmado).
- HasIdentity/SignPubAt para el onboarding.

Aditivo; build/vet/test del modulo verdes (incluido TestDeriveParityWithWeb).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
agent
2026-06-18 23:38:14 +02:00
parent 4dea99a524
commit 53f8a4a3d6
5 changed files with 375 additions and 1 deletions
+49
View File
@@ -0,0 +1,49 @@
package mobile
import (
"encoding/hex"
"testing"
)
// TestDeriveParityWithWeb pins the Go wallet derivation to the TypeScript one
// (web/src/wallet/derive.ts). The canonical BIP39 test mnemonic must derive to this
// exact Ed25519 sign_pub — the same value the uniweb client showed for this phrase.
// If this fails, web and mobile would derive different identities from the same seed
// and the "same account on both devices" guarantee breaks.
func TestDeriveParityWithWeb(t *testing.T) {
const mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
const wantSignPub = "34302746268e7370d35940e1bcef8c0b1c13a857ea6209e6ecc6e9b3af06b3c8"
id, err := deriveIdentity(mnemonic)
if err != nil {
t.Fatalf("deriveIdentity: %v", err)
}
if got := hex.EncodeToString(id.SignPub); got != wantSignPub {
t.Fatalf("sign_pub mismatch:\n got %s\n want %s", got, wantSignPub)
}
if len(id.SignPriv) != 64 || len(id.KexPub) != 32 || len(id.KexPriv) != 32 {
t.Fatalf("bad key lengths: signPriv=%d kexPub=%d kexPriv=%d",
len(id.SignPriv), len(id.KexPub), len(id.KexPriv))
}
// sign_priv is Go's ed25519 layout: seed || pub, so its tail must equal sign_pub.
if got := hex.EncodeToString(id.SignPriv[32:]); got != wantSignPub {
t.Fatalf("sign_priv tail != sign_pub:\n got %s\n want %s", got, wantSignPub)
}
}
// TestMnemonicRoundTrip checks a freshly generated phrase validates and derives.
func TestMnemonicRoundTrip(t *testing.T) {
m, err := NewMnemonic()
if err != nil {
t.Fatalf("NewMnemonic: %v", err)
}
if !ValidateMnemonic(m) {
t.Fatalf("generated mnemonic failed validation: %q", m)
}
if _, err := deriveIdentity(m); err != nil {
t.Fatalf("deriveIdentity(fresh): %v", err)
}
if ValidateMnemonic("not a real mnemonic at all please") {
t.Fatal("garbage phrase validated as a mnemonic")
}
}