← OVIQ — etusivu

Security & Architecture Audit Reference

OVIQ Delegation

How an owner grants another person the right to open a lock — the components, data model, message flows, key custody, and the trust boundaries an auditor needs to verify.

Model: owner-signed capabilities Trust: server = post office Unlock: offline BLE Store: canonical asset object

00 What this system is

A smart-lock access product: an ESP32 relay on the door, a phone app, and the Honkio backend. The lock verifies everything itself and never trusts the network.

A door is a Honkio asset owned by a user. The lock enrolls the owner's Ed25519 public key on first pairing (trust-on-first-use). To let someone else in, the owner signs a capability bound to that person's public key; the capability travels through the server and is presented to the lock over Bluetooth. The lock checks the owner's signature and the grantee's proof-of-possession locally — so an unlock works with no internet, and the server can never forge one.

Core invariant
Only a key enrolled on the lock as ROLE_OWNER can authorize access. The server stores and forwards opaque bytes; it holds no key that opens any door.

01 Architecture

Five parties. The two phones and the lock hold keys; the server and Firebase only move bytes between them.

☁️ server2 — canonical APIs only

🔒 ESP32 lock (offline-capable)

📱 Grantee phone

📱 Owner phone

enroll pubkey (TOFU)

sign capability

publish identity QR

read shared asset (reader ACL)

backup seed

backup seed

push: access_granted

present capability (BLE)

notify

Owner key
(per lock, on device)

Grantee key
(per user, on device)

Enrolled owner pubkey
Ed25519 verify · relay

/merchant/assets
(asset + reader ACL)

user_merchant_private_data
(user-only secrets)

asset-share/notify

Firebase Cloud Messaging
(per-app project)

Component & data flow. Keys live only on the phones and the lock. The server exposes three canonical surfaces — the asset (with its read ACL), the user-only private store, and a push trigger — and never sees a usable private key.

02 Data model

Everything hangs off the asset object in the app → company → user hierarchy. There is no bespoke "device" object.

contains

owns

is owner of

data.authorizations[]

data.reader_user_ids[]

user-only

APPLICATION

COMPANY

ASSET

string

asset_type

honkio_access_device

string

merchant_id

always present

string

owner_id

the owning USER

string

device_id

BLE name (public)

string

device_pubkey

lock key, pinned (public)

string

lock_device_id

16B lock id (public)

int

key_id

owner key id on lock (public)

USER

AUTHORIZATION

string

grantee_user_id

string

capability

owner-signed, 145B (public-safe)

READER

string

user_id

may READ this asset

PRIVATE_DATA

string

collection

user_merchant_private_data

string

access_seed

grantee signing seed

object

owner_seeds

per-lock owner seeds

string

access

ONLY the user themselves

Asset-centric model. The asset carries only public material (lock pubkey, ids, owner-signed capabilities). Every private signing seed lives in user_merchant_private_data, readable by the owning user alone — never in the shared asset, which grantees can read.

03 Provisioning — trust on first use

The first key enrolled on an unowned lock becomes its owner. No pairing secret is needed because an unowned lock has nothing to protect yet.

private-data storeserver2 /merchant/assetsLock (ESP32)Owner phoneprivate-data storeserver2 /merchant/assetsLock (ESP32)Owner phoneunowned, accept (TOFU)role = ROLE_OWNERonce owned, further enroll needsthe pairing window (serial button)generate per-lock Ed25519 seed1BLE enroll(owner_pubkey)2key_id3read device_pubkey and lock_device_id (challenge)4Wi-Fi credentials (SSID and password)5create asset — owner=user, device_id, device_pubkey, lock_device_id, key_id6backup owner seed (owner_seeds per lock)7
Provisioning. After the first enrollment the lock refuses new keys unless an owner opens the pairing window — so a stranger cannot silently add themselves. The owner seed is backed up to the user-only store, not the asset.

04 Delegation — owner shares a door

The grantee shows an identity QR; the owner scans it, signs a capability for that exact public key, and writes it onto the asset. A push tells the grantee at once.

FCMasset-share notifyserver2 assets APIOwner phoneGrantee phoneFCMasset-share notifyserver2 assets APIOwner phoneGrantee phoneverify caller owns asset and grantee is a readerdoor appears automaticallyload grantee key (from device, else private-data on new phone)1show QR identity — user_id plus public key2scan QR, sign capability with owner seed (bound to lock and grantee key)3PUT asset data — add grantee to reader_user_ids and authorizations4notify(asset_id, grantee_user_id)5push access_granted6notification7refresh, GET assets via reader ACL8shared lock and capability9
QR-identity delegation. No consent round-trip and no server-side authority: the owner signs locally and the server only stores the bytes and fires a push it has verified the owner is entitled to send.

05 Guest unlock — verified on the lock, offline

The grantee proves possession of the granted key; the lock checks the owner's signature on the capability and the grantee's signature on the challenge. No network involved.

Lock (ESP32)Grantee phoneLock (ESP32)Grantee phone1 capability bound to this lock2 grant signed by an enrolled ROLE_OWNER key3 within the validity window4 grantee sig matches the grant public key5 nonce matches and is consumed (anti-replay)BLE connect and read challenge1nonce plus device_id plus device_sig2verify device_sig (correct lock)3build M and sign it with the grantee seed4write capability plus M plus S (280 bytes)5status OK, relay pulses6
Capability presentation. Five independent checks run on the lock. Any failure returns a status code and the relay stays shut. The single nonce is consumed per attempt, so a captured message cannot be replayed.

06 Lock ownership lifecycle

A lock is unowned, owned, or reset. Losing the owner key is recoverable only by a physical factory reset — the safe failure direction.

first enroll (TOFU) becomes ROLE_OWNER

owner opens pairing window, add member

serial wipe (factory reset)

Unowned

Owned

Owner key lost and asset lost
means no one can sign, wipe required

Enroll accepted without a
pairing secret (nothing to protect)

Ownership states. Once owned, the enroll characteristic rejects writes (GATT_WRITE_NOT_PERMITTED) unless the pairing window is open — preventing silent takeover. wipe erases enrolled keys so the next enrollment re-establishes ownership.

07 Key custody & recovery

Keys are kept on the phone and fetched from the user-only server store only when a device doesn't have them. Recovery depends on what was lost.

KeySignsOn deviceServer backupWho can read it
Owner per-lock seedcapabilities (grants)secure storage private_data.owner_seeds[]owner only
Grantee per-user seedguest unlock (M)secure storage private_data.access_seedgrantee only
Lock device keychallenge (identity)lock NVS pubkey pinned in assetpubkey public
Owner-signed capability—— asset.data.authorizationsreaders (public-safe)

asset only
(key on phone)

phone key only
(backup exists)

grantee key
(new phone)

owner key AND asset
(both gone)

What was lost?

app recreates asset
from local enrollment

restore seed from
user private-data

fetch seed from
private-data on login

serial `wipe` +
re-provision (last resort)

no new keys

new owner key (last resort)

Recovery matrix. As long as either the phone-local key or the user's server backup survives, ownership is recovered without touching the lock. Only losing both forces a physical reset.

08 Trust boundaries

The central audit claim: no server-side actor can open a door. Draw the line at what each party can and cannot do.

Server / admin CANNOT

Server / admin CAN

store & forward opaque bytes

deliver a push it verified

see PUBLIC data
(pubkeys, capabilities)

read a user's private seed

forge a capability
(no owner key)

produce a valid unlock

DB dump +
broker access

Server as post office. A full database dump plus message-broker access yields no working unlock — the private seeds sit in the user-only store and the owner key never leaves the owner's device. This is the property auditors should attempt to break.
Why private seeds are not in the asset
The reader ACL returns the whole asset to every grantee of that door. A private seed placed there would leak to co-grantees. Hence seeds live in user_merchant_private_data, scoped to the token's own user_id with no admin read path.

09 Access-control matrix

Who may do what, and where it is enforced.

ActionOwnerGranteeCompany adminEnforced by
Open the dooryesyesno*lock (Ed25519)
Grant / revoke accessyesnonolock owner-role gate
Read the shared assetyesif readeryesasset reader ACL
Write the assetyesnoyesassets:* perm
Read another's private seednononotoken-scoped store
Factory-reset the lockphysicalnonoserial console access

* the admin owns/can-read the asset but holds no key enrolled on the lock, so cannot produce a valid unlock.

10 Security properties to verify

The checklist an audit should actively run, not just read.

#PropertyHow to test
1DB dump + broker access opens no doorattempt to derive a valid signature from stored data — must fail
2Only ROLE_OWNER may sign a grantcapability signed by a member key → lock rejects
3Capability bound to one lockpresent a grant to a different lock_device_id → rejected
4Time-boundedpresent outside [valid_from, valid_until] → rejected
5Anti-replayreplay a captured 280-byte unlock → nonce consumed, rejected
6Private seed is user-onlyadmin API for another user's private-data → no such path
7No silent takeoverenroll on an owned lock without pairing window → WRITE_NOT_PERMITTED
8Notify is authorizedpush a user not in the asset's readers → 403
Auditor's one-line summary
Compromising the backend yields metadata and public keys, never access. The only paths to opening a door are holding an enrolled key (owner/grantee) or physical possession of the lock's serial console.