verification_bloc.dart 6.3 KB
Newer Older
Leon Tappe's avatar
Leon Tappe committed
1
import 'dart:convert';
Leon Tappe's avatar
Leon Tappe committed
2
3
4
import 'dart:io';

import 'package:bloc/bloc.dart';
5
import 'package:connectivity/connectivity.dart';
Leon Tappe's avatar
Leon Tappe committed
6
import 'package:digital_3g_common/verification_api.dart';
Leon Tappe's avatar
Leon Tappe committed
7
import 'package:flutter_jailbreak_detection/flutter_jailbreak_detection.dart';
Leon Tappe's avatar
Leon Tappe committed
8
import 'package:http/http.dart' as http;
Leon Tappe's avatar
Leon Tappe committed
9
import 'package:logging/logging.dart';
Leon Tappe's avatar
Leon Tappe committed
10
11
import 'package:path_provider/path_provider.dart';

12
13
14
15
16
17
enum EventType {
  check,
  revoke,
  add,
}

Leon Tappe's avatar
Leon Tappe committed
18
class VerificationBloc extends Bloc<VerificationEvent, VerificationState> {
Leon Tappe's avatar
Leon Tappe committed
19
20
  final Logger _log = Logger('VerificationBloc');

21
22
  File? _ticketStore;
  String? _ticket;
Leon Tappe's avatar
Leon Tappe committed
23

Leon Tappe's avatar
Leon Tappe committed
24
25
  File? _verificationStore;

Leon Tappe's avatar
Leon Tappe committed
26
  VerificationApi? _api;
Leon Tappe's avatar
Leon Tappe committed
27
  VerificationResponse? _verification;
28
  VerificationErrorType? _error;
Leon Tappe's avatar
Leon Tappe committed
29

30
  VerificationBloc(String url) : super(VerificationState.busy) {
Leon Tappe's avatar
Leon Tappe committed
31
    _api = VerificationApi(http.Client(), url);
Leon Tappe's avatar
Leon Tappe committed
32
33
34
35
36
37
38
39
40
    FlutterJailbreakDetection.jailbroken.then((bool jailbroken) {
      if (!jailbroken) {
        _initVerificationStore().then((_) => _initTicketFile().then((value) => _loadTicket()));
      } else {
        _log.severe('jailbroken/rooted device detected, VerificationBloc halted');
        _error = VerificationErrorType.root;
        emit(VerificationState.error);
      }
    });
Leon Tappe's avatar
Leon Tappe committed
41
42
  }

43
  VerificationApi? get api => _api;
44
  VerificationErrorType? get error => _error;
45
  String? get ticket => _ticket;
Leon Tappe's avatar
Leon Tappe committed
46
47
48
49
  VerificationResponse? get verification => _verification;

  @override
  Stream<VerificationState> mapEventToState(VerificationEvent event) async* {
Leon Tappe's avatar
Leon Tappe committed
50
    _log.fine('got event $event');
51
52
53
    switch (event.type) {
      case EventType.check:
        if (_ticket != null && _ticket!.isNotEmpty) {
Leon Tappe's avatar
Leon Tappe committed
54
55
56
          VerificationResponse? response;
          try {
            response = await _api!.check(_ticket!);
Leon Tappe's avatar
Leon Tappe committed
57
            _log.finer(response);
Leon Tappe's avatar
Leon Tappe committed
58
59
60
          } catch (e) {
            _log.severe('failed to check ticket:\n$e');
          }
61
62
          if ((response?.verified ?? false) && !(response?.hasError ?? false)) {
            _verification = response;
Leon Tappe's avatar
Leon Tappe committed
63
            await _verificationStore!.writeAsString(json.encode(_verification!.toMap));
64
            yield VerificationState.verified;
65
66
67
68
          } else if (((((await Connectivity().checkConnectivity()) == ConnectivityResult.none) ||
                  ((response?.hasError ?? false) &&
                      response!.error == VerificationErrorType.server)) &&
              (_verificationStore?.existsSync() ?? false))) {
69
            // get verification from local storage
Leon Tappe's avatar
Leon Tappe committed
70
71
            final loaded =
                VerificationResponse.fromMap(json.decode(await _verificationStore!.readAsString()));
Leon Tappe's avatar
Leon Tappe committed
72
            if (loaded.timestamp.isAfter(DateTime.now().subtract(const Duration(minutes: 30)))) {
Leon Tappe's avatar
Leon Tappe committed
73
74
75
76
77
              _verification = loaded;
              yield VerificationState.verified;
            } else {
              yield VerificationState.unverified;
            }
78
          } else if (!(response?.verified ?? false) && !(response?.hasError ?? false)) {
79
            yield VerificationState.unverified;
80
81
82
83
          } else {
            _verification = response;
            _error = _verification!.error;
            yield VerificationState.error;
84
          }
Leon Tappe's avatar
Leon Tappe committed
85
86
        }
        break;
87
      case EventType.revoke:
Leon Tappe's avatar
Leon Tappe committed
88
        _ticketStore!.writeAsString('', flush: true);
89
        _ticket = null;
90
        _error = null;
91
92
93
        yield VerificationState.noTicket;
        break;
      case EventType.add:
94
        // get ticket from backend if longer than 728 (backwards compatibility)
95
        TicketResponse? response;
96
97
        if ((event.seed?.length ?? 0) > 768) {
          try {
98
            response = await _api!.generateTicket(event.seed!);
99
100
          } catch (e) {
            _log.severe('error while generating ticket');
101
            _error = VerificationErrorType.other;
102
103
104
            yield VerificationState.error;
          }
        } else {
105
106
107
108
109
110
          response = TicketResponse(event.seed);
        }

        if (response!.hasError) {
          _error = response.error;
          yield VerificationState.error;
111
112
113
        }

        // save ticket
114
115
116
        if (!_ticketStore!.existsSync()) {
          await _ticketStore!.create(recursive: true);
        }
117
118
        if (response.ticket != null && response.ticket!.isNotEmpty) {
          _ticket = response.ticket;
119
          await _ticketStore!.writeAsString(_ticket!, flush: true);
Leon Tappe's avatar
Leon Tappe committed
120
          yield VerificationState.unverified;
121
122
          add(VerificationEvent.check());
        }
Leon Tappe's avatar
Leon Tappe committed
123
124
125
126
127
128
        break;
      default:
        return;
    }
  }

129
  void onAdd(String ticket) => add(VerificationEvent.add(ticket));
Leon Tappe's avatar
Leon Tappe committed
130

131
132
133
134
135
136
  @override
  void onChange(Change<VerificationState> change) {
    _log.finer('new state: ${change.nextState}');
    super.onChange(change);
  }

137
  void onCheck() => add(VerificationEvent.check());
Leon Tappe's avatar
Leon Tappe committed
138

139
140
141
142
  void onRevoke() => add(VerificationEvent.revoke());

  Future<void> _initTicketFile() async {
    _ticketStore = File('${(await getApplicationDocumentsDirectory()).path}/ticket');
Leon Tappe's avatar
Leon Tappe committed
143
    _log.fine('initialized ticket store in ${_ticketStore?.path}');
144
  }
Leon Tappe's avatar
Leon Tappe committed
145

Leon Tappe's avatar
Leon Tappe committed
146
147
148
149
150
  Future<void> _initVerificationStore() async {
    _verificationStore = File('${(await getApplicationDocumentsDirectory()).path}/last');
    _log.fine('initialized verification store in ${_verificationStore?.path}');
  }

151
152
  Future<void> _loadTicket() async {
    String? ticket;
Leon Tappe's avatar
Leon Tappe committed
153
    if (!_ticketStore!.existsSync()) {
Leon Tappe's avatar
Leon Tappe committed
154
      _log.fine('no ticket found, creating ticket store');
155
      _ticketStore!.create(recursive: true);
Leon Tappe's avatar
Leon Tappe committed
156
      add(VerificationEvent.revoke());
Leon Tappe's avatar
Leon Tappe committed
157
158
159
160
161
162
163
164
165
166
167
      return;
    }

    _log.fine('ticketStore found, loading and checking');
    ticket = await _ticketStore!.readAsString();
    _log.finer('ticket: "$ticket"');
    if (ticket.isNotEmpty) {
      _ticket = ticket;
      add(VerificationEvent.check());
    } else {
      _log.fine('ticket empty, going to noTicket state');
Leon Tappe's avatar
Leon Tappe committed
168
      add(VerificationEvent.revoke());
Leon Tappe's avatar
Leon Tappe committed
169
170
171
172
    }
  }
}

173
174
class VerificationEvent {
  final EventType type;
175
  final String? seed;
176

177
  VerificationEvent({required this.type, this.seed});
178
179

  factory VerificationEvent.add(String ticket) =>
180
      VerificationEvent(type: EventType.add, seed: ticket);
181
182
183
184

  factory VerificationEvent.check() => VerificationEvent(type: EventType.check);

  factory VerificationEvent.revoke() => VerificationEvent(type: EventType.revoke);
185

186
  Map get toMap => {'type': type, 'seed': seed};
187
188
189

  @override
  String toString() => '[VerificationEvent $toMap]';
Leon Tappe's avatar
Leon Tappe committed
190
191
192
}

enum VerificationState {
193
  noTicket,
Leon Tappe's avatar
Leon Tappe committed
194
195
196
  verified,
  unverified,
  error,
197
  busy,
Leon Tappe's avatar
Leon Tappe committed
198
}