# Što ovaj vodič pokriva#
Flutter olakšava isporuku cross-platform aplikacija, ali ta praktičnost može prikriti rupe u mobilnoj sigurnosti sve dok ne dođete do produkcije i ne susretnete prijevare, preuzimanje računa ili curenje podataka. Ovaj vodič fokusira se na ojačavanje sigurnosti Flutter aplikacije kroz obrasce koji su praktični za većinu timova: sigurna pohrana, odluke oko SSL pinninga, provjere integriteta u runtimeu i sigurno rukovanje auth tokenima.
Koristit ćemo pristup “threat model prvo” i dati primjenjivu checklistu koju možete primijeniti na većinu Flutter aplikacija. Za širu web perspektivu koja nadopunjuje mobilno ojačavanje, pogledajte naš Checklist za sigurnost web aplikacija.
# Threat modeli za koje trebate dizajnirati (i što prvo puca)#
Aplikacije se ojačavaju prema sposobnostima napadača, a ne prema generičnoj “sigurnosti”. Mobilni napadači najčešće spadaju u nekoliko kategorija, a svaka se mapira na konkretne kontrole.
Uobičajeni profili mobilnih napadača#
| Threat model | Sposobnost napadača | Tipičan cilj | Što prvo puca | Najučinkovitije mitigacije |
|---|---|---|---|---|
| Network MITM | Može presresti ili mijenjati promet na neprijateljskom Wi‑Fi-ju, rogue AP-ovima, kompromitiranim routerima | Krađa tokena, downgrade TLS-a, ubacivanje odgovora | API pozivi, auth flowovi | Ispravno konfiguriran TLS, opcionalni SSL pinning, vezanje tokena (token binding), server-side detekcija anomalija |
| Napadač na uređaju (bez roota) | Ima fizički pristup, može instalirati malware, može čitati screenshotove | Eksfiltracija osjetljivih UI podataka | Ekrani, logovi, clipboard | Minimizirati tajne u UI-ju, isključiti osjetljive logove, higijena clipboarda, sigurna pohrana |
| Rootan ili jailbroken uređaj | Može zaobići sandbox, pregledati storage aplikacije, hookati runtime | Krađa tokena, zaobilaženje provjera | Lokalna pohrana, runtime logika | Signali root/jailbreak, step-up auth, device attestation, server-side pravila |
| Manipulacija aplikacijom i repackaging | Može patchati APK/IPA, mijenjati Flutter/Dart ili native kod, ponovno potpisati | Prijevara, zaobilaženje paywalla, ubacivanje SDK-ova | Poslovna logika, integritet | Store-side potpisi, runtime provjere integriteta, app attestation, backend validacija |
| Reverse engineering | Može dekompilirati, pregledati stringove, presretati mrežu, čitati assete | Izvući ključeve, endpointove, skrivene flagove | Hardkodirane tajne | Ukloniti tajne s klijenta, rotirati ključeve, tajne samo na serveru, obfuskacija |
| Debugger i instrumentacija | Može attachati Frida, koristiti dinamičku analizu | Zaobići provjere, manipulirati odgovorima | Runtime odluke | Signali detekcije debuga, integritet, attestation, server-side provođenje |
ℹ️ Napomena: OWASP MASVS je solidna osnova za mobilne kontrole. Ne morate implementirati svaki zahtjev prvog dana, ali trebate mapirati rizike na kontrole i isporučivati iterativno.
# Praktična checklist za ojačavanje sigurnosti (prvo isporučite ovo)#
Koristite ovo kao checklistu spremnu za sprint. Prioritizira kontrole koje smanjuju stvarne incidente, a ne samo “audit findings”.
P0 checklist (većina aplikacija trebala bi imati ovo)#
- Forsirajte HTTPS svugdje i uklonite iznimke za cleartext promet.
- Pohranjujte tokene u OS-backed secure storage (Keychain i Keystore), ne u SharedPreferences ili datoteke.
- Izbjegavajte dugotrajne tajne na klijentu. Preferirajte kratkoživuće access tokene i refresh koji kontrolira server.
- Uklonite osjetljive podatke iz logova i crash reportova.
- Implementirajte server-side autorizacijske provjere za svaku osjetljivu radnju. Nikad ne vjerujte klijentu.
- Dodajte runtime “risk signale” za root/jailbreak, debug i emulator; koristite ih za step-up auth i nadzor.
- Pratite sigurnosno relevantne evente i crashove. Uparite hardening s observabilityjem od prvog dana uz Observability za Flutter aplikacije.
P1 checklist (aplikacije visokog rizika i regulirani podaci)#
- Dodajte SSL pinning sa strategijom rotacije i remote konfiguracijom.
- Dodajte device ili app attestation signale (platform-specific) i provjeravajte ih server-side.
- Dodajte anti-tamper signale (signature provjere, integrity provjere) i provodite ih na backendu.
- Dodajte kontrole za screenshot ili screen recording na osjetljivim ekranima gdje je prikladno.
- Implementirajte “kill switch” mogućnost preko remote configa za kompromitirane buildove.
P2 checklist (defense-in-depth)#
- Obfuskirajte release buildove i smanjite curenje metapodataka.
- Dodajte granularnije risk scoring uz feature flagove i progresivno ojačavanje.
- Dodajte runtime zaštite od hooking-a i instrumentacije (signal-based, ne apsolutno).
💡 Savjet: Ugradite “security gate” u release pipeline: pokrenite skeniranje ovisnosti, provjerite network security config, provjerite da su debug flagovi ugašeni i napravite brzi root/jailbreak smoke test na stvarnim uređajima.
# Sigurna pohrana u Flutteru: što spremati, a što ne#
Sigurna pohrana nije “sakriti sve”. Poanta je spremiti prave tajne na način koji odgovara tome kako ih napadači zaista kradu.
Što spada u sigurnu pohranu#
| Tip podatka | Spremati na klijentu? | Gdje | Napomene |
|---|---|---|---|
| Access token (kratkoživući) | Da | Secure storage | Držati kratko, često rotirati |
| Refresh token (dugotrajan) | Po mogućnosti ne, ponekad da | Secure storage + server kontrole | Visokovrijedna meta, zaštititi rotacijom i revokacijom |
| Profil korisnika | Da | Normalna pohrana | Tretirati kao ne-tajnu |
| API base URL | Da | Normalna pohrana | Nije tajna, ali izbjegavajte “skrivene admin” endpointove |
| Ključevi enkripcije | Po mogućnosti ne | Server ili hardware-backed | Ako treba, derivirajte ključeve, nemojte hardkodirati |
| Feature flagovi | Da | Normalna pohrana | Ne skrivajte sigurnosne kontrole iza client-only flagova |
Preporučeni plugin: flutter_secure_storage#
flutter_secure_storage koristi Keychain na iOS-u i Android Keystore-backed storage “ispod haube”. To je osnovni izbor za tokene, refresh tokene i osjetljive preference.
Obrazac implementacije: omotajte storage servisom i centralizirajte sva čitanja i upise. To smanjuje slučajna curenja u logove, analitiku ili snapshotove stanja.
// dart
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class SecureTokenStore {
static const _accessKey = 'access_token';
static const _refreshKey = 'refresh_token';
final FlutterSecureStorage _storage;
SecureTokenStore({FlutterSecureStorage? storage})
: _storage = storage ?? const FlutterSecureStorage();
Future<void> saveTokens({
required String accessToken,
required String refreshToken,
}) async {
await _storage.write(key: _accessKey, value: accessToken);
await _storage.write(key: _refreshKey, value: refreshToken);
}
Future<String?> readAccessToken() => _storage.read(key: _accessKey);
Future<String?> readRefreshToken() => _storage.read(key: _refreshKey);
Future<void> clear() async {
await _storage.delete(key: _accessKey);
await _storage.delete(key: _refreshKey);
}
}Zamke sigurne pohrane (koje uzrokuju stvarne incidente)#
1) Pisanje tokena u logove i crash reportove
To se događa kad logirate cijele HTTP headere, exceptione ili “debug printove”. Uklonite vrijednosti tokena prije logiranja.
// dart
String redactAuthHeader(String? header) {
if (header == null) return '';
if (!header.toLowerCase().startsWith('bearer ')) return '[redacted]';
return 'Bearer [redacted]';
}2) Držanje tokena u memoriji dulje nego što treba
Ako aplikacija iz praktičnosti drži tokene u globalnom stateu, povećavate izloženost runtime inspekciji na kompromitiranim uređajima. Čitajte tokene on-demand za requestove i držite ih izvan UI statea.
3) Tretiranje secure storagea kao “sigurno na rootanim uređajima”
Na rootanim ili jailbroken uređajima mnoge zaštite se mogu zaobići. Svejedno koristite secure storage, ali kompromitirane uređaje tretirajte kao viši rizik i prilagodite ponašanje server-side.
⚠️ Upozorenje: Nikad nemojte isporučiti API ključeve koji daju server-side privilegije unutar aplikacije. Ako ključ mora postojati na klijentu, pretpostavite da će biti izvučen i zloupotrijebljen.
# Sigurno rukovanje auth tokenima u Flutteru (iznad same pohrane)#
Krađa tokena jedan je od najbržih puteva do preuzimanja računa. Ojačavanje se uglavnom svodi na dizajn životnog ciklusa tokena, ne samo na pohranu.
Token strategija koja radi u produkciji#
| Preporuka | Zašto je važno | Praktičan default |
|---|---|---|
| Kratkoživući access tokeni | Ograničava “blast radius” krađe | 5 do 15 minuta trajanja |
| Rotacija refresh tokena | Sprječava replay nakon krađe | Rotirati pri svakoj upotrebi |
| Revokacija refresh tokena | Zaustavlja ponovnu upotrebu | Revocirati na promjenu lozinke, logout, sumnjive evente |
| Minimizacija scopeova | Ograničava štetu kompromitacije | Uski scopeovi po featureu |
| Server-side praćenje sesija | Provodi politike | Pratiti device ID, IP signale, risk flagove |
Primjer: HTTP klijent s ubacivanjem tokena i refreshom#
Neka kod bude mali i predvidljiv. Možete koristiti dio ili http; ključno je centralizirati injection i refresh.
// dart
import 'package:dio/dio.dart';
class ApiClient {
final Dio _dio;
final SecureTokenStore _store;
ApiClient(this._dio, this._store);
Future<Response<T>> get<T>(String path) async {
final token = await _store.readAccessToken();
return _dio.get<T>(
path,
options: Options(headers: {
if (token != null) 'Authorization': 'Bearer $token',
}),
);
}
}Napomene za implementaciju:
- Dodajte refresh interceptor, ali izbjegnite beskonačne petlje. Prestanite retryati nakon jednog pokušaja refresha.
- Ako refresh ne uspije, obrišite tokene i tražite ponovnu prijavu.
- Izbjegavajte stavljati tokene u query parametre. Cure kroz logove, proxyje i analitiku.
Firebase specifična napomena#
Ako koristite Firebase Auth, tretirajte ID token kao kratkoživući i izbjegavajte pohranu custom admin credsa u aplikaciji. Uparite autentikaciju sa strogim Firestore pravilima i server verifikacijom gdje treba. Ako želite praktičan baseline, pogledajte naš Flutter Firebase Tutorial i dodajte hardening korake iz ovog vodiča.
# SSL Pinning u Flutteru: kada pomaže, a kada škodi#
SSL pinning smanjuje rizik da korisnički instaliran root certifikat omogući MITM. Ne štiti vas od potpuno kompromitiranog uređaja i može uzrokovati prekide rada ako pogrešno odradite rotaciju certifikata.
Kompromisi pinninga koje trebate odlučiti unaprijed#
| Točka odluke | Opcija A | Opcija B | Preporuka |
|---|---|---|---|
| Što pinati | Leaf cert | Hash javnog ključa | Preferirajte public key pinning kako biste tolerirali obnove certifikata |
| Ponašanje na grešku | Hard fail | Soft fail s telemetrijom | Aplikacije visokog rizika hard fail, ostale mogu soft fail ograničeno vrijeme |
| Rotacija | Update aplikacije | Remote config skup pinova | Planirajte barem dva važeća pina i prozor rotacije |
| Opseg | Sve domene | Samo auth i osjetljivi endpointovi | Krenite s auth domenom, zatim širite |
| Debug buildovi | Pin uključen | Pin isključen | Pin isključen u debugu kako ne biste blokirali developer tooling |
🎯 Ključna poruka: SSL pinning je operativna obveza. Ako ne možete održavati rotacije i observability, možete zaključati legitimne korisnike.
Napomene za implementaciju u Flutteru#
Pure Dart pinning kroz HttpClient je moguć, ali produkcijski pinning obično treba platformsku podršku kako bi se izbjegli bypassi i kako bi se došlo do boljih TLS detalja. Mnogi timovi implementiraju pinning uz:
- Native networking stack na iOS-u i Androidu s pinned trust managerom, ili
- Provjeren plugin koji ispravno podržava pinning na obje platforme.
Minimalni Dart-level pristup koristi badCertificateCallback, ali tretirajte ga edukativno, ne kao kompletno rješenje.
// dart
import 'dart:io';
HttpClient createPinnedHttpClient() {
final client = HttpClient();
client.badCertificateCallback =
(X509Certificate cert, String host, int port) {
// Compare cert.pem or SPKI hash here.
// Return true only when it matches expected pins.
return false;
};
return client;
}Operativna checklist za pinning:
- Održavajte barem dva pina: trenutni i sljedeći.
- Dodajte telemetriju za pin failuree kako biste brzo uočili probleme u stvarnom svijetu.
- Planirajte rollback put ako pinning pukne zbog promjena certifikata ili CDN-a.
# Detekcija roota i jailbreaka: koristite to kao signal rizika#
Detekcija roota i jailbreaka može smanjiti prijevare omogućavanjem step-up autentikacije i ograničavanjem featurea. Neće zaustaviti odlučnog napadača i može uzrokovati false positive na nekim uređajima.
Što napraviti sa signalom “kompromitiran uređaj”#
| Radnja aplikacije | Kada primijeniti | Utjecaj na korisnika | Sigurnosna korist |
|---|---|---|---|
| Step-up autentikacija | Prije osjetljivih radnji | Umjeren | Blokira automatiziranu zloupotrebu |
| Isključiti lokalno cachiranje osjetljivih podataka | Uvijek za visok rizik | Nizak | Smanjuje izloženost data-at-rest |
| Ograničiti visokorizične featuree | Samo kad je potrebno | Visok | Smanjuje financijski gubitak ili curenje podataka |
| Tražiti češću ponovnu prijavu | Aplikacije srednjeg rizika | Umjeren | Smanjuje prozor krađe tokena |
| Logirati i pratiti | Uvijek | Nema | Poboljšava detekciju i incident response |
Flutter pristup implementaciji#
Postoje pluginovi koji provjeravaju uobičajene indikatore roota i jailbreaka. Rezultat tretirajte kao probabilistički i izbjegavajte previše tehnička upozorenja.
Napomene za implementaciju:
- Pokrenite provjere na startupu i prije osjetljivih radnji.
- Keširajte rezultat kratko vrijeme kako biste izbjegli performansne udarce.
- Pošaljite risk flag backendu, ali ne šaljite sirove detalje uređaja koji mogu biti osjetljivi.
// dart
class RiskSignals {
final bool isCompromised;
final bool isDebug;
final bool isEmulator;
RiskSignals({
required this.isCompromised,
required this.isDebug,
required this.isEmulator,
});
Map<String, dynamic> toJson() => {
'compromised': isCompromised,
'debug': isDebug,
'emulator': isEmulator,
};
}ℹ️ Napomena: Izbjegavajte “blokiranje cijele aplikacije” samo na temelju detekcije roota ili jailbreaka, osim ako ste u reguliranom okruženju. Bolji pristup je step-up auth, stroža ograničenja i backend enforcement.
# Provjere integriteta u runtimeu: detekcija debugginga, hookinga i manipulacije#
Integrity provjere nisu jedna značajka. To je slojeviti set signala koji napade čini skupljima i lakše uočljivima.
Signali koje realno možete prikupljati u Flutter aplikacijama#
| Signal | Što detektira | Rizik false positivea | Preporučena upotreba |
|---|---|---|---|
| Debugger attached | Aktivno debugiranje | Nizak | Isključiti debug-only endpointove, step-up auth |
| Detekcija emulatora | Automatizirane farme | Srednji | Rate limit, CAPTCHA-like izazovi, step-up auth |
| Neusklađen potpis aplikacije | Repackaging | Nizak do srednji | Blokirati osjetljive operacije, server alerti |
| Indikatori hookinga | Frida, instrumentacija | Srednji | Povećati “friction” i logiranje |
| Integrity ili attestation | Autentičan uređaj i aplikacija | Nizak kad je ispravno implementirano | Server-side allow/deny za visokorizične operacije |
Praktičan obrazac provođenja#
- 1Prikupite signale client-side na startupu i prije osjetljivih flowova.
- 2Pošaljite sažeti risk payload backendu uz svaki auth i svaki high-value transaction request.
- 3Donesite server-side odluke: dopusti, step-up ili blokiraj.
- 4Logirajte odluku i signale za audit i analizu prijevara.
Ovaj obrazac skalira bolje od hard-blockanja u aplikaciji, jer server vidi puni kontekst: povijest korisnika, reputaciju IP-a, povijest uređaja i rate limite.
# Kontrole osjetljivog UI-ja i izloženosti podataka#
Čak i uz savršenu pohranu i mrežnu sigurnost, osjetljivi podaci mogu procuriti kroz UI sloj.
Kontrole koje se brzo isplate#
| Rizik | Kontrola | Flutter-specifična napomena |
|---|---|---|
| Screenshotovi i screen recording | Onemogućiti na osjetljivim ekranima gdje je prikladno | Koristiti platform flagove, ograničiti na određene rute |
| Eksfiltracija preko clipboarda | Očistiti clipboard nakon kopiranja osjetljivih vrijednosti | Postaviti kratki TTL u UI flowu |
| Autofill i prijedlozi tipkovnice | Isključiti za tajne | Izbjegavati spremanje lozinki u polja koja nisu password |
| Snapshotovi u pozadini | Sakriti osjetljivi UI kad app ode u background | Renderirati prazan ili brendirani ekran na lifecycle promjenama |
Držite ove kontrole ciljano. Globalno blokiranje screenshotova može narušiti upotrebljivost i pristupačnost.
# Logiranje i observability za sigurnosne evente#
Sigurnosno ojačavanje bez telemetrije je pogađanje. Trebate vidjeti kada se zaštite aktiviraju i koreliraju li s prijevarama, churnom ili crashovima.
Logirajte ove evente:
- Pinning failuree po domeni i verziji aplikacije.
- Promjene root ili jailbreak signala kroz vrijeme.
- Neuspjehe refreshanja tokena i razloge logouta.
- Neuspjehe integriteta ili attestacije.
- Rate-limitane ili blokirane radnje po endpointu i segmentu korisnika.
Zahtjev implementacije: nikad ne logirajte tokene, lozinke ili osobne podatke. Logirajte hashove ili redaktirane identifikatore.
Za praktičan setup kroz Crashlytics, Sentry, strukturirane logove i metrike, slijedite Observability za Flutter aplikacije i definirajte taksonomiju security eventa rano.
# Napomene za implementaciju za česte mobilne threat scenarije (checklist)#
Koristite ovo kao mapiranje između “što može poći po zlu” i “što treba izgraditi”.
Checklist po scenarijima#
| Scenarij | Vjerojatna akcija napadača | Što implementirate u Flutteru | Što provodite server-side |
|---|---|---|---|
| MITM na javnom Wi‑Fi-ju | Instalira root cert, proxyja promet | Opcionalni pinning za auth endpointove, strogi TLS | Detektirati neobične IP-e, token binding signale, revocirati sesije |
| Ukraden uređaj | Izvlači lokalne podatke | Secure storage za tokene, minimizirati cacheani PII | Remote revokacija sesije, step-up auth na novom uređaju |
| Prijevare na rootanom uređaju | Hooka app, replaya pozive | Root/jailbreak signali, integrity signali | Risk scoring, stroža ograničenja, blokirati visokorizične endpointove |
| Token replay | Ponovno koristi ukradeni refresh token | Rotacija i kratkoživući access tokeni | Revocirati refresh “obitelj”, pratiti device sesije |
| Repackaged app | Manipulira logikom, ponovno potpisuje | Signature i integrity signali | Allowlist službenih signing certifikata, blokirati nepoznate buildove |
| Curenje debug builda | Dodatno logiranje i test endpointovi | Isključiti debug logove u releaseu, build-time config | Odbiti debug build identifikatore, rate limit |
💡 Savjet: Ako možete priuštiti samo jednu backend promjenu, implementirajte refresh-token rotaciju s “family revocation”. Drastično smanjuje vrijednost ukradenih refresh tokena.
# Ključne poruke#
- Koristite sigurnu pohranu za tokene, ali dizajnirajte životni ciklus tokena za kratkoživući access i rotirane refresh tokene s revokacijom.
- Detekciju roota i jailbreaka tretirajte kao signal rizika, ne kao čvrstu garanciju; odluke provodite na backendu.
- SSL pinning koristite selektivno i samo ako možete podržati rotaciju certifikata, telemetriju i rollback.
- Centralizirajte token injection i refresh logiku te nikad ne logirajte headere ili tajne u crashovima ili analitici.
- Prikupljajte runtime integrity signale i kombinirajte ih sa server-side risk scoringom za osjetljive radnje.
# Zaključak#
Ojačavanje sigurnosti Flutter aplikacije kombinacija je ispravne pohrane, dobrog dizajna tokena, selektivnog ojačavanja mreže i server-side odluka vođenih rizikom. Krenite s P0 checklistom, dodajte telemetriju, a zatim slojevito uvedite pinning i integrity kontrole ondje gdje to traži vaš threat model.
Ako želite da Samioda pregleda vaš trenutni Flutter security posture, implementira pinning i token strategiju sa sigurnim rotacijama ili doda runtime risk signale uz server-side enforcement, kontaktirajte nas preko samioda.com i predložit ćemo plan ojačavanja koji možete isporučiti u sljedećem release ciklusu.
FAQ
Osnivač i senior developer u Samiodi. 8+ godina iskustva u izradi React, Next.js, Flutter i n8n rješenja za klijente diljem Europe.
Više iz kategorije Mobilni razvoj
Sve →Flutter zadaci u pozadini: raspoređivanje, pouzdanost i ograničenja platformi (iOS + Android) u 2026.
Praktičan vodič za raspoređivanje Flutter zadataka u pozadini: ograničenja platformi na iOS-u i Androidu, kompromisi pouzdanosti te kada koristiti workmanager, background_fetch ili izvorni kod za sinkronizaciju, obavijesti i raspoređivanje koje čuva bateriju.
Flutter dizajnerski sustav i tematizacija u 2026.: Material 3, dinamičke boje, tipografija i tamni način rada
Praktičan vodič za tematizaciju Flutter dizajnerskog sustava uz Material 3: skalabilna arhitektura teme, dizajnerski tokeni, dinamičke boje, tipografija, razmaci, tematizacija komponenti i konzistentan tamni način rada kroz sve funkcionalnosti.
Observabilnost Fluttera u produkciji: izvještavanje o rušenjima, logiranje i praćenje performansi
Praktičan vodič za 2026. o observabilnosti Flutter aplikacija u produkciji: instrumentirajte rušenja, nefatalne pogreške, latenciju API-ja, start aplikacije i frame timinge. Uključuje usporedbu Crashlytics vs Sentry, korake integracije i checklistu za izdanje.
Trebate pomoć s projektom?
Gradimo prilagođena rješenja koristeći tehnologije iz ovog članka. Senior tim, fiksne cijene.
Povezani članci
Flutter zadaci u pozadini: raspoređivanje, pouzdanost i ograničenja platformi (iOS + Android) u 2026.
Praktičan vodič za raspoređivanje Flutter zadataka u pozadini: ograničenja platformi na iOS-u i Androidu, kompromisi pouzdanosti te kada koristiti workmanager, background_fetch ili izvorni kod za sinkronizaciju, obavijesti i raspoređivanje koje čuva bateriju.
Flutter dizajnerski sustav i tematizacija u 2026.: Material 3, dinamičke boje, tipografija i tamni način rada
Praktičan vodič za tematizaciju Flutter dizajnerskog sustava uz Material 3: skalabilna arhitektura teme, dizajnerski tokeni, dinamičke boje, tipografija, razmaci, tematizacija komponenti i konzistentan tamni način rada kroz sve funkcionalnosti.
Observabilnost Fluttera u produkciji: izvještavanje o rušenjima, logiranje i praćenje performansi
Praktičan vodič za 2026. o observabilnosti Flutter aplikacija u produkciji: instrumentirajte rušenja, nefatalne pogreške, latenciju API-ja, start aplikacije i frame timinge. Uključuje usporedbu Crashlytics vs Sentry, korake integracije i checklistu za izdanje.