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

import 'package:bloc/bloc.dart';
6
import 'package:connectivity/connectivity.dart';
7
import 'package:crypto/crypto.dart';
Leon Tappe's avatar
Leon Tappe committed
8
import 'package:digital_3g_common/verification_api.dart';
9
import 'package:encrypt/encrypt.dart';
Leon Tappe's avatar
Leon Tappe committed
10
import 'package:flutter/foundation.dart' show kIsWeb;
Leon Tappe's avatar
Leon Tappe committed
11
import 'package:flutter_jailbreak_detection/flutter_jailbreak_detection.dart';
Leon Tappe's avatar
Leon Tappe committed
12
import 'package:http/http.dart' as http;
Leon Tappe's avatar
Leon Tappe committed
13
import 'package:logging/logging.dart';
Leon Tappe's avatar
Leon Tappe committed
14
import 'package:path_provider/path_provider.dart';
15
16
17
import 'package:platform_device_id/platform_device_id.dart';

const String iv = 'c3eb69edb728ff3c';
Leon Tappe's avatar
Leon Tappe committed
18

19
20
21
22
23
24
enum EventType {
  check,
  revoke,
  add,
}

Leon Tappe's avatar
Leon Tappe committed
25
class VerificationBloc extends Bloc<VerificationEvent, VerificationState> {
Leon Tappe's avatar
Leon Tappe committed
26
27
  final Logger _log = Logger('VerificationBloc');

28
29
  File? _ticketStore;
  String? _ticket;
Leon Tappe's avatar
Leon Tappe committed
30

Leon Tappe's avatar
Leon Tappe committed
31
32
  File? _verificationStore;

Leon Tappe's avatar
Leon Tappe committed
33
  VerificationApi? _api;
Leon Tappe's avatar
Leon Tappe committed
34
  VerificationResponse? _verification;
35
  VerificationErrorType? _error;
Leon Tappe's avatar
Leon Tappe committed
36

37
38
  Encrypter? _encrypter;

39
  VerificationBloc(String url) : super(VerificationState.busy) {
Leon Tappe's avatar
Leon Tappe committed
40
    _api = VerificationApi(http.Client(), url);
Leon Tappe's avatar
Leon Tappe committed
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
    if (!kIsWeb) {
      FlutterJailbreakDetection.jailbroken.then((bool jailbroken) {
        if (!jailbroken) {
          _initVerificationStore().then(
              (_) => _initTicketFile().then((_) => _getDeviceId().then((_) => _loadTicket())));
        } else {
          _log.severe('jailbroken/rooted device detected, VerificationBloc halted');
          _error = VerificationErrorType.root;
          emit(VerificationState.error);
        }
      });
    } else {
      _initVerificationStore().then((_) => _initTicketFile()
          .then((_) => _getDeviceId().then((_) => add(VerificationEvent.revoke()))));
    }
Leon Tappe's avatar
Leon Tappe committed
56
57
  }

58
  VerificationApi? get api => _api;
59
  VerificationErrorType? get error => _error;
60
  String? get ticket => _ticket;
Leon Tappe's avatar
Leon Tappe committed
61
62
63
64
  VerificationResponse? get verification => _verification;

  @override
  Stream<VerificationState> mapEventToState(VerificationEvent event) async* {
Leon Tappe's avatar
Leon Tappe committed
65
    _log.fine('got event $event');
66
67
68
    switch (event.type) {
      case EventType.check:
        if (_ticket != null && _ticket!.isNotEmpty) {
Leon Tappe's avatar
Leon Tappe committed
69
70
71
          VerificationResponse? response;
          try {
            response = await _api!.check(_ticket!);
Leon Tappe's avatar
Leon Tappe committed
72
            _log.finer(response);
Leon Tappe's avatar
Leon Tappe committed
73
74
75
          } catch (e) {
            _log.severe('failed to check ticket:\n$e');
          }
76
77
          if ((response?.verified ?? false) && !(response?.hasError ?? false)) {
            _verification = response;
78
79
80
            final iv = IV.fromLength(16);
            await _verificationStore!.writeAsString(
                iv.base64 + _encrypter!.encrypt(json.encode(_verification!.toMap), iv: iv).base64);
81
            yield VerificationState.verified;
82
83
84
85
          } else if (((((await Connectivity().checkConnectivity()) == ConnectivityResult.none) ||
                  ((response?.hasError ?? false) &&
                      response!.error == VerificationErrorType.server)) &&
              (_verificationStore?.existsSync() ?? false))) {
86
            // get verification from local storage
87
88
89
90
91
92
93
94
95
96
97
            String rawVerification;
            final storeData = await _verificationStore!.readAsString();
            final iv = IV.fromBase64(storeData.substring(0, 24));
            final encrypted = Encrypted.fromBase64(storeData.substring(24));
            try {
              rawVerification = _encrypter!.decrypt(encrypted, iv: iv);
            } catch (e) {
              yield VerificationState.unverified;
              return;
            }
            final loaded = VerificationResponse.fromMap(json.decode(rawVerification));
Leon Tappe's avatar
Leon Tappe committed
98
            if (loaded.timestamp.isAfter(DateTime.now().subtract(const Duration(minutes: 30)))) {
Leon Tappe's avatar
Leon Tappe committed
99
100
101
102
103
              _verification = loaded;
              yield VerificationState.verified;
            } else {
              yield VerificationState.unverified;
            }
104
          } else if (!(response?.verified ?? false) && !(response?.hasError ?? false)) {
105
            yield VerificationState.unverified;
106
107
108
109
          } else {
            _verification = response;
            _error = _verification!.error;
            yield VerificationState.error;
110
          }
Leon Tappe's avatar
Leon Tappe committed
111
112
        }
        break;
113
      case EventType.revoke:
Leon Tappe's avatar
Leon Tappe committed
114
115
116
        if (!kIsWeb) {
          _ticketStore!.writeAsString('', flush: true);
        }
117
        _ticket = null;
118
        _error = null;
119
120
121
        yield VerificationState.noTicket;
        break;
      case EventType.add:
122
        // get ticket from backend if longer than 728 (backwards compatibility)
123
        TicketResponse? response;
124
125
        if ((event.seed?.length ?? 0) > 768) {
          try {
126
            response = await _api!.generateTicket(event.seed!);
127
128
          } catch (e) {
            _log.severe('error while generating ticket');
129
            _error = VerificationErrorType.other;
130
131
132
            yield VerificationState.error;
          }
        } else {
133
134
135
136
137
138
          response = TicketResponse(event.seed);
        }

        if (response!.hasError) {
          _error = response.error;
          yield VerificationState.error;
139
140
141
        }

        // save ticket
142
143
144
        if (!_ticketStore!.existsSync()) {
          await _ticketStore!.create(recursive: true);
        }
145
146
        if (response.ticket != null && response.ticket!.isNotEmpty) {
          _ticket = response.ticket;
Leon Tappe's avatar
Leon Tappe committed
147
          _writeEncryptedTicket(_ticket!);
Leon Tappe's avatar
Leon Tappe committed
148
          yield VerificationState.unverified;
149
150
          add(VerificationEvent.check());
        }
Leon Tappe's avatar
Leon Tappe committed
151
152
153
154
155
156
        break;
      default:
        return;
    }
  }

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

159
160
161
162
163
164
  @override
  void onChange(Change<VerificationState> change) {
    _log.finer('new state: ${change.nextState}');
    super.onChange(change);
  }

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

167
168
  void onRevoke() => add(VerificationEvent.revoke());

169
170
171
172
173
174
175
  Future<void> _getDeviceId() async {
    final deviceId = await PlatformDeviceId.getDeviceId;
    final key = Key(sha256.convert(utf8.encode(deviceId!)).bytes as Uint8List);

    _encrypter = Encrypter(AES(key, mode: AESMode.cbc));
  }

176
  Future<void> _initTicketFile() async {
Leon Tappe's avatar
Leon Tappe committed
177
    if (kIsWeb) return;
178
    _ticketStore = File('${(await getApplicationDocumentsDirectory()).path}/ticket');
Leon Tappe's avatar
Leon Tappe committed
179
    _log.fine('initialized ticket store in ${_ticketStore?.path}');
180
  }
Leon Tappe's avatar
Leon Tappe committed
181

Leon Tappe's avatar
Leon Tappe committed
182
  Future<void> _initVerificationStore() async {
Leon Tappe's avatar
Leon Tappe committed
183
    if (kIsWeb) return;
Leon Tappe's avatar
Leon Tappe committed
184
185
186
187
    _verificationStore = File('${(await getApplicationDocumentsDirectory()).path}/last');
    _log.fine('initialized verification store in ${_verificationStore?.path}');
  }

188
  Future<void> _loadTicket() async {
Leon Tappe's avatar
Leon Tappe committed
189
    if (kIsWeb) return;
190
    String? ticket;
Leon Tappe's avatar
Leon Tappe committed
191
    if (!_ticketStore!.existsSync()) {
Leon Tappe's avatar
Leon Tappe committed
192
      _log.fine('no ticket found, creating ticket store');
193
      _ticketStore!.create(recursive: true);
Leon Tappe's avatar
Leon Tappe committed
194
      add(VerificationEvent.revoke());
Leon Tappe's avatar
Leon Tappe committed
195
196
197
198
199
200
201
      return;
    }

    _log.fine('ticketStore found, loading and checking');
    ticket = await _ticketStore!.readAsString();
    _log.finer('ticket: "$ticket"');
    if (ticket.isNotEmpty) {
202
203
      final iv = IV.fromBase64(ticket.substring(0, 24));
      final encrypted = Encrypted.fromBase64(ticket.substring(24));
204
205
206
207
208
209
210
211
212
213
      try {
        _ticket = _encrypter!.decrypt(encrypted, iv: iv);
      } catch (e) {
        // convert old ticket into encrypted
        _writeEncryptedTicket(ticket);
        _log.info('restarting loadTicket with migrated ticket');
        _loadTicket();
        return;
      }

Leon Tappe's avatar
Leon Tappe committed
214
215
216
      add(VerificationEvent.check());
    } else {
      _log.fine('ticket empty, going to noTicket state');
Leon Tappe's avatar
Leon Tappe committed
217
      add(VerificationEvent.revoke());
Leon Tappe's avatar
Leon Tappe committed
218
219
    }
  }
220
221
222

  Future<void> _writeEncryptedTicket(String ticket) async {
    final iv = IV.fromLength(16);
223
    // write 24 characters of IV and append encrypted data to ticketStore
224
    await _ticketStore!.writeAsString(
225
      iv.base64 + _encrypter!.encrypt(ticket, iv: iv).base64,
226
227
228
      flush: true,
    );
  }
Leon Tappe's avatar
Leon Tappe committed
229
230
}

231
232
class VerificationEvent {
  final EventType type;
233
  final String? seed;
234

235
  VerificationEvent({required this.type, this.seed});
236
237

  factory VerificationEvent.add(String ticket) =>
238
      VerificationEvent(type: EventType.add, seed: ticket);
239
240
241
242

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

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

244
  Map get toMap => {'type': type, 'seed': seed};
245
246
247

  @override
  String toString() => '[VerificationEvent $toMap]';
Leon Tappe's avatar
Leon Tappe committed
248
249
250
}

enum VerificationState {
251
  noTicket,
Leon Tappe's avatar
Leon Tappe committed
252
253
254
  verified,
  unverified,
  error,
255
  busy,
Leon Tappe's avatar
Leon Tappe committed
256
}