Abbinamento: come funziona
Codice a 6 cifre, cavo, canale cifrato: cosa protegge l'abbinamento e da cosa.
Una scheda vera parla con il cervello solo dopo essersi abbinata, e da lì sempre su un canale cifrato. Chi sta nella stessa rete (o in mezzo al cavo) non legge cosa passa, non si finge cervello né scheda e non può comandare la scheda. Il codice è in due posti che fanno la stessa cosa:
| dove | cosa | |
|---|---|---|
| scheda | ui/include/jz/pair.h, ui/src/pair.c | lo strato tra trasporto e UI, l'archivio dei cervelli |
| scheda | ui/include/jz/cpace.h, ui/src/pair_cpace.c | CPace su Monocypher |
| cervello | packages/pairing | Abbinamenti, Sessione, ArchivioSchede, CPace su @noble |
| prove | tests/pairtest, packages/pairing/src/*.test.ts | vettori della bozza, prova vera tra C e TypeScript |
In breve
- Con il codice (Wi-Fi, e qualunque collegamento): la scheda mostra 6 cifre, l'utente le scrive nel configuratore. Scheda e cervello fanno CPace (draft-irtf-cfrg-cpace-21, suite CPACE-X25519-SHA512) usando il codice come password: chi ascolta o si mette in mezzo ha un tentativo solo per indovinarlo.
- Via cavo (USB): il cavo dimostra già che sei lì. X25519 senza codice e un tocco sulla scheda ("Abbinare a MacBook Air di Jacopo?").
- Tutti e due finiscono con una chiave di abbinamento di 32 byte salvata dalle due parti, con
le identità: la scheda tiene fino a 4 cervelli (id, nome, chiave), il cervello le schede in
<cartella dati>/schede.json(0600). - Ricollegamento: a ogni collegamento una stretta di mano con la chiave di abbinamento e X25519 effimero. Ne escono due chiavi nuove per il canale, una per verso: chi ruba la chiave di abbinamento domani non legge le sessioni di oggi.
- Canale: ChaCha20-Poly1305 con un contatore per verso; frame ripetuti, vecchi o toccati si scartano, e fuori dal cavo anche un frame tolto si nota (il contatore dopo salta). Dopo 2^20 frame con le stesse chiavi se ne fanno di nuove senza interrompere niente.
- Revoca da tutte e due le parti: chi revoca, se il canale è aperto, lo dice all'altro.
Il browser (simulatore, configuratore, telefono) non passa di qui: i suoi WebSocket hanno un Origin e
li autentica il cervello (vedi apps/brain/API.md). Un client senza Origin è una scheda.
Chi è chi
| id | dove sta | |
|---|---|---|
| scheda | jz-<MAC di fabbrica> (sim-… nel simulatore), jz_platform_device_id() | nel CIAO |
| cervello | cv- + 16 cifre esadecimali casuali, creato al primo avvio | schede.json, campo cervello.id |
Il cervello ha anche un nome ("MacBook Air di Jacopo"), che la scheda mostra: è dentro le strette di mano (autenticato), quindi la scheda lo aggiorna a ogni ricollegamento. Gli id stanno in 39 byte e i nomi in 47 (UTF-8): il cervello taglia il nome prima di usarlo, perché i due lati devono vedere gli stessi byte.
Messaggi
Tutto sopra il framing di sempre (protocollo.md), widget 0xFF, due tipi nuovi:
PAIR (17) per le strette di mano e SEALED (18) per il canale. Numeri e nomi vengono da
packages/protocol/src/spec.ts (bun run gen aggiorna ui/include/jz/proto_gen.h e le tabelle qui sotto).
| passo | nome | significato |
|---|---|---|
| 1 | CIAO | scheda → cervello: chi è la scheda, il nonce per la prossima stretta di mano e se mostra un codice. In chiaro al collegamento, a ogni cambio di stato e quando la scheda chiude il canale (o riceve frame cifrati senza averlo): il cervello chiude il suo lato e ricollega. Cifrato quando la scheda vuole chiavi nuove o risponde a RINNOVA |
| 2 | INIZIO | cervello → scheda: comincia una stretta di mano nel modo indicato (codice, cavo, ricollega). In chiaro a canale aperto vuol dire che il cervello l'ha chiuso: la scheda chiude anche lei e risponde |
| 3 | RISPOSTA | scheda → cervello: il punto della scheda e la sua conferma |
| 4 | CONFERMA | cervello → scheda: la conferma del cervello; con il modo ricollega apre il canale |
| 5 | ESITO | scheda → cervello: la stretta di mano è fallita, con il motivo; segue un CIAO nuovo. In chiaro, o cifrato se il canale resta aperto (rinnovo col nonce sbagliato) |
| 6 | REVOCA | entrambe, solo cifrato: chi lo manda ha cancellato l'abbinamento; chi lo riceve fa lo stesso e chiude |
| 7 | RINNOVA | cervello → scheda, solo cifrato: il cervello vuole chiavi nuove per il canale; la scheda risponde con un CIAO cifrato |
| modo | nome | significato |
|---|---|---|
| 1 | CODICE | abbinamento con il codice a 6 cifre mostrato dalla scheda (CPace) |
| 2 | CAVO | abbinamento via cavo: X25519 senza codice, la scheda chiede una conferma con un tocco |
| 3 | RICOLLEGA | stretta di mano con la chiave di abbinamento e X25519 effimero: apre il canale (o rinnova le chiavi) |
| esito | nome | significato |
|---|---|---|
| 1 | CODICE | codice sbagliato (conta come tentativo) |
| 2 | SCONOSCIUTO | la scheda non conosce questo cervello: va abbinata di nuovo |
| 3 | CHIAVE | la conferma con la chiave di abbinamento non torna |
| 4 | CHIUSO | la scheda non sta mostrando un codice |
| 5 | ATTESA | troppi tentativi sbagliati: il codice nuovo accetta tentativi tra poco |
| 6 | NONCE | il nonce dell'INIZIO non è quello dell'ultimo CIAO |
| 7 | RIFIUTATO | abbinamento via cavo rifiutato sulla scheda, o nessuna risposta in tempo |
| 8 | NON_PERMESSO | abbinamento via cavo chiesto su un collegamento che non è il cavo |
| 9 | PROTOCOLLO | messaggio malformato, fuori sequenza o di una versione diversa |
| 10 | TEMPO | la stretta di mano non si è conclusa in tempo |
Versione attuale dell'abbinamento: 1.
I campi (chiave 0 int è sempre il passo):
| passo | campi |
|---|---|
CIAO | 1 int versione dell'abbinamento, 2 str id scheda, 3 bytes nS (16), 4 int 1 se mostra un codice, 5 int ms di vita del codice, 6 int tentativi rimasti, 7 int ms prima del prossimo tentativo, 8 int 1 se è sul cavo |
INIZIO | 1 int modo, 2 str id cervello, 3 str nome cervello, 4 bytes nC (16), 5 bytes nS (l'ultimo del CIAO), 6 bytes punto (32: Ya, o eC) |
RISPOSTA | 1 bytes punto (32: Yb, o eS), 2 bytes conferma (64 con il codice, 32 negli altri modi) |
CONFERMA | 1 bytes conferma |
ESITO | 1 int motivo |
REVOCA, RINNOVA | nessuno |
SEALED: chiave 0 bytes = contatore u64 LE (8) ‖ cifrato ‖ tag (16). Dentro c'è il corpo di un frame
qualunque (da widget all'ultimo campo), fino a 2048 byte: la busta supera di al più 30 byte il massimo
dei frame in chiaro, e chi legge un collegamento cifrato accetta frame fino a 2078 byte.
Le strette di mano
Tutte cominciano da un CIAO della scheda, che porta il nonce nS. Il nonce cambia ogni volta che un
INIZIO lo usa: ogni stretta di mano ha nonce nuovi da tutte e due le parti.
scheda cervello
CIAO (id, nS, codice?, cavo?) ───────────▶
◀─────────── INIZIO (modo, idC, nomeC, nC, nS, punto)
RISPOSTA (punto, conferma) ───────────▶ controlla la conferma della scheda
◀─────────── CONFERMA
codice, cavo: CIAO (nS nuovo) ───────────▶ subito un INIZIO ricollega con la chiave nuova
ricollega: canale aperto ◀──────────▶ SEALED…
qualcosa va storto: ESITO (motivo), poi CIAOCodice (CPace)
| ingresso CPace | valore |
|---|---|
| PRS | le 6 cifre in ASCII |
| CI | lv_cat("jz.land desk/codice/1", idC, idS): le identità, con il ruolo dato dalla posizione (sezione 10.1.1 della bozza) |
| sid | nC ‖ nS (32 byte, metà a testa: sezione 10.9) |
| ADa | lv_cat("cervello", idC, nomeC) |
| ADb | lv_cat("scheda", idS) |
| ruoli | il cervello è A (iniziatore), la scheda B; trascrizione transcript_ir |
Il cervello manda Ya, la scheda risponde con Yb e la sua conferma Tb, il cervello con Ta: è la conferma
delle chiavi della sezione 10.4 (mac_key = H("CPaceMac" ‖ sid ‖ ISK), T = HMAC-SHA-512(mac_key, lv_cat(Y, AD))).
Un punto che dà il neutro (ordine piccolo) fa abortire, come chiede la bozza.
Chiave di abbinamento = HKDF-SHA-512(ISK, sale = sid, info = "jz.land desk/codice/chiave"), 32 byte.
Elligator 2 in C. CPace mappa il generatore con map_to_curve_elligator2 di RFC 9380 su un numero di
255 bit. crypto_elligator_map di Monocypher fa la stessa mappa (Z = 2) ma legge solo 254 bit. Siccome
la mappa dipende solo da r², jz_cpace_map riduce r modulo p e, se r ≥ 2^254, passa p − r (che sta
sotto 2^254 e dà lo stesso punto). Il vettore della bozza ha proprio il bit 254 acceso, e una prova
confronta il C con @noble/curves su 1024 casi (bordi compresi: 0, p − 1, p … 2^255 − 1, 2^254 ± 1).
Cavo
th = SHA-512(lv_cat("jz.land desk/cavo/1", idC, idS, nomeC, nC, nS, eC, eS)), dh = X25519(e, e dell'altro)
(neutro → abort), HKDF-SHA-512(dh, sale th, "jz.land desk/cavo/chiavi") → chiave di abbinamento ‖ chiave di
conferma della scheda ‖ del cervello; conferma = HMAC-SHA-512(chiave di conferma, th), primi 32 byte.
La scheda risponde solo dopo il sì sullo schermo e solo se il trasporto è il cavo
(jz_pair_transport(…, cable = true)); il cervello lo chiede solo su via: "usb".
Ricollega
Stessa trascrizione con l'etichetta "jz.land desk/ricollega/1"; HKDF-SHA-512(dh ‖ chiave di abbinamento, sale th, "jz.land desk/ricollega/chiavi") → conferma scheda ‖ conferma cervello ‖ chiave cervello→scheda
‖ chiave scheda→cervello. Serve la chiave di abbinamento (autentica tutte e due le parti) e il segreto
effimero (segretezza in avanti). La scheda apre il canale quando la conferma del cervello torna; il
cervello quando torna quella della scheda.
Il canale
- ChaCha20-Poly1305 di RFC 8439, nonce = 4 byte a zero ‖ contatore u64 little-endian (come WireGuard), dati associati vuoti. Una chiave e un contatore per verso.
- Il contatore viaggia nel frame: chi riceve accetta solo contatori più grandi dell'ultimo accettato. Un frame ripetuto o vecchio si scarta senza decifrarlo, uno toccato fallisce il tag.
- Frame che mancano. Fuori dal cavo (WebSocket su TCP) il trasporto consegna tutto e in ordine:
il contatore deve essere proprio il prossimo, e un buco vuol dire che qualcuno ha tolto dei frame. Chi
lo vede (dopo aver controllato il tag del frame che arriva) chiude il canale e i due lati si ricollegano
(sotto). Sul cavo un frame può perdersi davvero, perché
usb_link.cbutta i byte quando i buffer sono pieni: lì il buco si accetta e finisce nel log (persi N frame). Il cervello sceglie convia("usb"è il cavo), la scheda conjz_pair_transport(…, cable). - Rinnovo: dopo
JZ_PAIR_REKEY_AFTER(2^20) frame in un verso, la scheda manda unCIAOcifrato (o il cervello unRINNOVAe la scheda risponde colCIAO), poi un ricollega dentro il canale. L'ordine dei cambi di chiave tiene i frame in viaggio: la scheda scrive con le chiavi nuove dopo laRISPOSTA, il cervello legge con le nuove appena la controlla, scrive con le nuove dopo laCONFERMA, e la scheda legge con le nuove quando la controlla. A 2 × 2^20 frame senza rinnovo ci si ferma. - Prima del canale passano solo i
PAIR, in tutte e due le direzioni; dopo solo iSEALED, tranne i messaggi qui sotto. - Revoca:
REVOCAcifrato, poi il canale si chiude e l'abbinamento sparisce dalle due parti. Se l'altro non è collegato lo scopre al ricollegamento (ESITO SCONOSCIUTO, o nessun tentativo dal cervello): non cancella da solo la sua copia, perché un messaggio in chiaro non basta per cancellare.
Quando un lato chiude
Ogni lato può chiudere il suo canale mentre il trasporto resta su: un rinnovo andato male (conferma sbagliata, tempo scaduto), un buco nei contatori. L'altro non lo sa, e senza fare niente resterebbero disallineati (uno manda cifrato, l'altro scarta). Per questo chi chiude lo dice, in chiaro:
| chi chiude | cosa manda | cosa fa l'altro |
|---|---|---|
| la scheda | CIAO in chiaro (dopo un ESITO, se c'era una stretta di mano) | il cervello chiude il suo lato e ricollega con quel nonce |
| il cervello | INIZIO ricollega in chiaro, con il nonce dell'ultimo CIAO | la scheda chiude il suo lato e lo tratta come al collegamento: col nonce ormai usato risponde ESITO NONCE e un CIAO nuovo, e il secondo tentativo va |
Se il CIAO della scheda si perde, il cervello continua a mandare cifrato: la scheda, che il canale non
ce l'ha più, a ogni frame cifrato ripete il CIAO (al più uno al secondo). Un ESITO in chiaro a canale
aperto il cervello lo scarta: da solo non chiude niente (se è vero, il CIAO arriva subito dopo). Il
cervello considera solo un CIAO della stessa scheda e della stessa versione.
Questi messaggi in chiaro non sono autenticati: chi sta in mezzo può fingerli, e ottiene un ricollegamento (il canale si chiude e si riapre, la UI rivede il cervello e manda l'HELLO). È un disservizio, che chi sta in mezzo può comunque fare buttando via i frame.
Il codice
- 6 cifre uniformi dal generatore crittografico (
jz_platform_random, rifiuto sotto 4294 × 10^6). - Vale 2 minuti, poi ce n'è uno nuovo. 5 tentativi: ogni
INIZIOcol codice ne consuma uno, comunque vada (anche se poi il cervello sparisce). Dopo 5 errori codice nuovo e 30 secondi di attesa. - Dopo 5 codici di fila senza abbinamento (scaduti o finiti con 5 errori) il modo si chiude e si riapre dallo schermo: al più 10 minuti, al più 25 tentativi.
- La scheda annuncia lo stato nel
CIAO(vita, tentativi, attesa): il configuratore mostra il conto alla rovescia. Il codice non viaggia mai.
Cosa protegge, e da cosa
| chi | cosa può fare | cosa lo ferma |
|---|---|---|
| qualcuno nella stessa rete che ascolta | leggere il traffico | canale cifrato; il codice non viaggia e CPace non lo rivela |
| qualcuno in mezzo durante l'abbinamento | indovinare il codice, sostituirsi | CPace: un tentativo per stretta di mano, 5 per codice, poi 30 s di attesa e codice nuovo; dopo 25 errori il modo si chiude. Al più 25 tentativi su un milione: 1 probabilità su 40 000 |
| una scheda finta | abbinarsi al cervello | serve il codice di una scheda vera, scritto dall'utente per quella scheda |
| un cervello finto | comandare una scheda | la scheda accetta solo cervelli nel suo archivio (chiave di abbinamento) o chi conosce il codice mostrato |
| chi registra il traffico e un giorno ruba la chiave di abbinamento | leggere le sessioni passate | X25519 effimero a ogni ricollegamento: le chiavi del canale non si ricavano dalla chiave di abbinamento |
| chi ripete, riordina o tocca i frame | rifare un'azione (muto, apri chat), cambiare dati | contatore e tag: scartati |
| chi toglie un frame (un comando di blocco, di silenzio), riconoscendolo da dimensione e tempi | far sparire un'azione senza che nessuno se ne accorga | fuori dal cavo il contatore del frame dopo salta: chi riceve chiude e si ricollega (se ne accorge con il frame dopo, non prima). Sul cavo no (vedi i limiti) |
chi taglia la connessione o inietta un CIAO, un INIZIO o un ESITO in chiaro | far chiudere il canale | niente: è un disservizio; i due lati si ritrovano da soli ("Quando un lato chiude") |
Limiti (cose che questo schema non copre):
- Chiavi a riposo. Sulla scheda l'archivio sta in NVS in chiaro finché non ci sono flash e NVS
cifrate e il firmware firmato (fase 1 del piano): chi ha la scheda in mano e un cavo può leggere le
chiavi. Sul computer
schede.jsonè 0600: un programma che gira già come l'utente la legge. - Il cavo si fida del computer. Un programma qualunque sul computer collegato può chiedere l'abbinamento via cavo: lo ferma solo il sì sullo schermo.
- Frame tolti sul cavo. Sul cavo i buchi nei contatori si accettano (i buffer USB possono perdere byte) e vanno solo nel log: chi sta in mezzo al cavo, o un programma sul computer che si mette tra la seriale e il cervello, può togliere un frame. Ripeterlo, cambiarlo o leggerlo no.
- Codice e utente. Se l'utente scrive il codice nella scheda sbagliata del configuratore (un attaccante che finge di mostrare un codice con lo stesso id), l'attaccante ha comunque un solo tentativo; ma il configuratore deve mostrare chiaramente quale scheda si sta abbinando.
- Tempo costante. In C tutto è Monocypher (tempo costante, mappa compresa). Nel cervello l'aritmetica è di @noble/curves in JavaScript: non è a tempo costante garantito; i segreti lunghi (chiavi di abbinamento) passano solo da HMAC e HKDF, i segreti effimeri si usano una volta.
- Disservizio. Chi controlla la rete può sempre impedire il collegamento.
- Un trasporto alla volta sulla scheda. Lo strato C ha uno stato solo (variabili globali, come
jz_platform_sendejz_ui_link_changed): con USB e Wi-Fi accesi insieme la piattaforma ne collega uno solo, ejz_pair_transportdell'uno chiude il canale dell'altro. Più sessioni insieme vorrebbero un contesto per trasporto (jz_pair_t *). Il cervello invece ha unaSessioneper collegamento. - Il generatore della scheda. Il firmware prende un seme dalla sorgente vera (SAR ADC) all'avvio e lo
mescola all'RNG hardware (
platform_esp.c): se un giorno si usa l'ADC prima diplatform_random_init, il seme non è più garantito.
Nel cervello
packages/pairing (dipende da @noble/curves, @noble/hashes e @noble/ciphers, versioni fisse; niente
nativo). L'aggancio al WebSocket /ws di Bun.serve, per i client senza Origin:
import { Abbinamenti } from "@jz/pairing";
import { parser } from "@jz/protocol";
const abb = new Abbinamenti({ cartella: DIR, nome: await machineName(), log: (l, t, c) => logger("abbinamento")[l](t, c) });
// fetch: un upgrade senza Origin è una scheda; con Origin si controlla come oggi
server.upgrade(req, { data: { scheda: !req.headers.get("origin") } });
websocket: {
open(ws) {
if (!ws.data.scheda) return; // browser: come prima
const board = nuovaScheda("wifi"); // il Board di hub.ts
const feed = parser((m) => suMessaggio(board, m)); // il parser di sempre, sui frame in chiaro
const s = abb.sessione({
via: "wifi",
invia: (b) => ws.sendBinary(b),
suFrame: (f) => feed(f),
suEvento: (e) => {
if (e.tipo === "aperto") hub.attach(board); // da qui arriva l'HELLO, cifrato
if (e.tipo === "chiuso" || e.tipo === "revocata") hub.detach(board);
},
});
board.send = (frame) => s.invia(frame); // tutto quello che il hub manda passa cifrato
ws.data.sessione = s;
},
message(ws, data) { ws.data.sessione?.ricevi(data as Uint8Array); },
close(ws) { ws.data.sessione?.chiudi(); },
}La seriale USB è uguale con via: "usb" (è il cavo: permette abbinaViaCavo() e tollera i frame persi;
gli altri via vogliono un trasporto che consegna tutto e in ordine). new Abbinamenti lancia un errore
se schede.json c'è ma non si legge (permessi, I/O) o è di una versione più nuova: non lo sovrascrive mai.
Un file rovinato finisce in schede.json.rotto-<data> (abb.archivio.messoDaParte, e nel log) e si
riparte da vuoto. Per il configuratore:
| cosa | chiamata |
|---|---|
| schede abbinate (e se sono collegate) | abb.schede() |
| schede che mostrano un codice, con scadenza e tentativi | abb.inAttesa() |
| abbinare con il codice | await abb.inserisciCodice(idScheda, "123456") → { ok: true } o { ok: false, motivo, testo } |
| abbinare via cavo | await abb.perScheda(id)?.abbinaViaCavo() |
| revocare | abb.revoca(idScheda) |
Sessione emette presentata (ogni CIAO), abbinata, aperto, rifiutata, revocata, rinnovato,
scartato, chiuso; inserisciCodice e abbinaViaCavo si risolvono quando il canale è aperto con la
chiave nuova (o con il motivo del rifiuto). Le API HTTP (/api/schede…) le definisce chi scrive il cervello.
Nella scheda
jz_pair_init(&(jz_pair_cfg_t){
.send_raw = usb_link_send, .deliver = jz_ui_feed, .link = jz_ui_link_changed,
.on_code = …, .on_confirm = …, .on_event = …, /* le schermate */
});
jz_pair_transport(collegata, cavo); /* al cambio del collegamento */
jz_pair_rx(byte, n); /* byte dal trasporto */
jz_pair_tick(); /* scadenze, ogni 10–100 ms */
/* jz_platform_send() → jz_pair_send(): cifrato, o scartato se il canale non c'è */Le schermate (altro lavoro) chiamano jz_pair_code_start() / jz_pair_code_stop() e ricevono il codice
con on_code; alla domanda via cavo (on_confirm(nome)) rispondono con jz_pair_confirm(sì);
jz_pair_list() e jz_pair_revoke(id) per l'elenco dei cervelli. Un trasporto alla volta (vedi i limiti).
Nel firmware lo strato è spento per ora (CONFIG_JZ_PAIRING, menu "JZ Companion" di idf.py menuconfig, predefinito no): finché il cervello non aggancia packages/pairing, una scheda con lo strato
acceso non parlerebbe più con il companion di oggi. Spento, il collegamento resta in chiaro come prima.
Acceso, firmware/main/main.c è collegato come sopra e, finché le schermate non ci sono:
- senza cervelli nell'archivio, a ogni collegamento della USB la scheda apre un codice e lo scrive nel log
(la porta UART,
idf.py monitor):codice di abbinamento 123456 (120 s, 5 tentativi); - alla domanda via cavo la scheda mostra un avviso breve e scrive la domanda nel log; si risponde con il tasto BOOT (breve sì, tenuto premuto no), entro un minuto.
I simulatori compilano lo strato ma non lo usano: parlano con il cervello come un browser.
Prove
bun test packages/pairing # vettori in TypeScript, canale, archivio, e la prova con il C
cmake -G Ninja -B tests/pairtest/build tests/pairtest && ninja -C tests/pairtest/build
./tests/pairtest/build/jz_pairtest --vettori- I vettori della bozza (appendice B.1: generatore, messaggi, K, ISK nei due modi, sid_output, punti di ordine piccolo e coordinate non canoniche) e di RFC 8439 passano in TypeScript e in C.
interop.test.tscompilatests/pairtest(la scheda senza schermo, stessi sorgenti del firmware) e la fa parlare con il cervello vero: codice giusto e sbagliato, 5 tentativi e attesa, codice scaduto, cavo sì/no/non permesso, ricollegamento dopo il riavvio di tutti e due, scheda azzerata, scambio cifrato nei due versi, frame manomessi, ripetuti e in chiaro, rinnovo delle chiavi ogni 5 frame, revoca da tutte e due le parti. E i casi in cui un lato chiude:CIAO,ESITOeINIZIOin chiaro finti a canale aperto, un frame tolto nei due versi (fuori dal cavo si chiude e si ricollega, sul cavo si va avanti), ilCIAOdella scheda che si perde, un rinnovo che non arriva.archivio.test.ts:schede.jsonche non si legge, di una versione più nuova o rovinato.