Voting Platform - Authentication & Wallet Architecture

1. Core Principles

2. Registration Flow

  1. Operator creates invitation with phone, email and saves codes for verification
  2. User receives invitation link
  3. User opens app → SignUp screen (phone & email pre-filled, locked)
  4. User creates account / password, confirms phone and email
    {
        "invitation_token": "...",
        "account_name": "...",
        "password_hash": "argon2id(password + salt)",
        "phone_verification_code": "...",
        "email_verification_code": "...",
    }
    
  5. Server stores account, password_hash and marks invitation as used and sends session tokens
  6. App generates wallet key pair locally on device
  7. App encrypts private key with user's password
  8. App sends to server:
    {
        "session_token": "...",
        "public_key": "0x...",
        "encrypted_wallet": "base64(aes_gcm(privkey, password))"
    }
    
  9. Server stores public_key and encrypted_wallet
  10. App saves password to SecureStore (Keychain/Keystore)

3. Login Flow

3.1 Normal Login (same device)

  1. App retrieves password from SecureStore
  2. App computes password_hash = argon2id(password)
  3. App sends to server:
    POST /auth/login { "phone": "...", "password_hash": "..." }
  4. Server verifies password_hash matches stored hash
  5. Server returns session_token (JWT, 24h validity)
  6. App stores session_token in memory (or SecureStore)
  7. All subsequent API calls use session_token

3.2 Login on New Device (wallet import)

  1. User installs app on new device
  2. SecureStore is empty → user must enter password
  3. User enters phone + password
  4. App computes password_hash → sends to server
  5. Server verifies password_hash → returns encrypted_wallet
  6. App decrypts encrypted_wallet using the same password
  7. App gets private key → ready to vote!
  8. App saves password to SecureStore on new device

4. Session Tokens

// Typical authenticated request GET /api/voting/active Authorization: Bearer <session_token>

5. Password Change (user knows old password)

  1. User is authenticated (session_token valid)
  2. User enters old password and new password
  3. App decrypts wallet with old password
  4. App re-encrypts same private key with new password
  5. App sends to server:
    PUT /user/wallet { "new_password_hash": "argon2id(new_password)", "new_encrypted_wallet": "base64(...)" }
  6. Server updates password_hash and encrypted_wallet
  7. App updates password in SecureStore
  8. Session tokens are invalidated (user must re-login)

6. Password Recovery (lost password)

⚠ CRITICAL: Wallet is lost FOREVER. No recovery possible by design (security).
  1. User contacts community operator
  2. Operator sends new invitation (same phone/email)
  3. User registers again (creates new wallet)
  4. Old account marked as wallet_locked (archived for audit)
  5. Old wallet remains encrypted forever (cannot be recovered)
Note: No seed phrase is used. The password is the ONLY key to the wallet. This is a conscious trade-off: simplicity vs. recovery options.

7. Repeated Invitation (incomplete registration)

8. Database Schema

Table: invitations

FieldTypeDescription
invitation_uuidUUIDPrimary key
community_uuidUUIDTarget community
phoneVARCHAR(20)Plain phone (temporary, for delivery)
emailVARCHAR(255)Plain email (temporary, for delivery)
invitation_tokenVARCHAR(255)Signed JWT token
expires_atTIMESTAMPInvitation expiration
statusENUMpending, used, expired
created_byUUIDOperator who created invitation

Table: users

FieldTypeDescription
user_uuidUUIDPrimary key
community_uuidUUIDForeign key to community
phone_hashVARCHAR(64)sha256(phone + salt)
email_hashVARCHAR(64)sha256(email + salt)
password_hashVARCHAR(128)Argon2id hash of password
encrypted_walletTEXTPrivate key encrypted with password
public_keyVARCHAR(128)Wallet public key
statusENUMactive, wallet_locked, pending
created_atTIMESTAMPRegistration timestamp

Table: sessions

FieldTypeDescription
session_idUUIDPrimary key
user_uuidUUIDForeign key to users
session_tokenVARCHAR(255)JWT or random token
expires_atTIMESTAMPToken expiration
device_idVARCHAR(255)Optional device fingerprint
created_atTIMESTAMPSession creation time

9. Security Summary

ScenarioOutcome
Password lostWallet lost forever. New registration required via operator.
Invitation interceptedUseless without access to email/phone.
Server breachedAttacker gets encrypted_wallet but cannot decrypt without user's password.
Device compromisedIf password in SecureStore + device unlocked → wallet accessible. SecureStore protects against other apps.
Man-in-the-middleHTTPS + certificate pinning. Only password_hash transmitted, not password.

10. Key Design Decisions (Summary)


Ready for: API specification (OpenAPI), detailed encryption scheme.