verification_bloc.dart 7.95 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_jailbreak_detection/flutter_jailbreak_detection.dart';
Leon Tappe's avatar
Leon Tappe committed
11
import 'package:http/http.dart' as http;
Leon Tappe's avatar
Leon Tappe committed
12
import 'package:logging/logging.dart';
Leon Tappe's avatar
Leon Tappe committed
13
import 'package:path_provider/path_provider.dart';
14
15
16
import 'package:platform_device_id/platform_device_id.dart';

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

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

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

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

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

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

36
37
  Encrypter? _encrypter;

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

52
  VerificationApi? get api => _api;
53
  VerificationErrorType? get error => _error;
54
  String? get ticket => _ticket;
Leon Tappe's avatar
Leon Tappe committed
55
56
57
58
  VerificationResponse? get verification => _verification;

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

        if (response!.hasError) {
          _error = response.error;
          yield VerificationState.error;
131
132
133
        }

        // save ticket
134
135
136
        if (!_ticketStore!.existsSync()) {
          await _ticketStore!.create(recursive: true);
        }
137
138
        if (response.ticket != null && response.ticket!.isNotEmpty) {
          _ticket = response.ticket;
Leon Tappe's avatar
Leon Tappe committed
139
          _writeEncryptedTicket(_ticket!);
Leon Tappe's avatar
Leon Tappe committed
140
          yield VerificationState.unverified;
141
142
          add(VerificationEvent.check());
        }
Leon Tappe's avatar
Leon Tappe committed
143
144
145
146
147
148
        break;
      default:
        return;
    }
  }

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

151
152
153
154
155
156
  @override
  void onChange(Change<VerificationState> change) {
    _log.finer('new state: ${change.nextState}');
    super.onChange(change);
  }

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

159
160
  void onRevoke() => add(VerificationEvent.revoke());

161
162
163
164
165
166
167
  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));
  }

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

Leon Tappe's avatar
Leon Tappe committed
173
174
175
176
177
  Future<void> _initVerificationStore() async {
    _verificationStore = File('${(await getApplicationDocumentsDirectory()).path}/last');
    _log.fine('initialized verification store in ${_verificationStore?.path}');
  }

178
179
  Future<void> _loadTicket() async {
    String? ticket;
Leon Tappe's avatar
Leon Tappe committed
180
    if (!_ticketStore!.existsSync()) {
Leon Tappe's avatar
Leon Tappe committed
181
      _log.fine('no ticket found, creating ticket store');
182
      _ticketStore!.create(recursive: true);
Leon Tappe's avatar
Leon Tappe committed
183
      add(VerificationEvent.revoke());
Leon Tappe's avatar
Leon Tappe committed
184
185
186
187
188
189
190
      return;
    }

    _log.fine('ticketStore found, loading and checking');
    ticket = await _ticketStore!.readAsString();
    _log.finer('ticket: "$ticket"');
    if (ticket.isNotEmpty) {
191
192
      final iv = IV.fromBase64(ticket.substring(0, 24));
      final encrypted = Encrypted.fromBase64(ticket.substring(24));
193
194
195
196
197
198
199
200
201
202
      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
203
204
205
      add(VerificationEvent.check());
    } else {
      _log.fine('ticket empty, going to noTicket state');
Leon Tappe's avatar
Leon Tappe committed
206
      add(VerificationEvent.revoke());
Leon Tappe's avatar
Leon Tappe committed
207
208
    }
  }
209
210
211

  Future<void> _writeEncryptedTicket(String ticket) async {
    final iv = IV.fromLength(16);
212
    // write 24 characters of IV and append encrypted data to ticketStore
213
    await _ticketStore!.writeAsString(
214
      iv.base64 + _encrypter!.encrypt(ticket, iv: iv).base64,
215
216
217
      flush: true,
    );
  }
Leon Tappe's avatar
Leon Tappe committed
218
219
}

220
221
class VerificationEvent {
  final EventType type;
222
  final String? seed;
223

224
  VerificationEvent({required this.type, this.seed});
225
226

  factory VerificationEvent.add(String ticket) =>
227
      VerificationEvent(type: EventType.add, seed: ticket);
228
229
230
231

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

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

233
  Map get toMap => {'type': type, 'seed': seed};
234
235
236

  @override
  String toString() => '[VerificationEvent $toMap]';
Leon Tappe's avatar
Leon Tappe committed
237
238
239
}

enum VerificationState {
240
  noTicket,
Leon Tappe's avatar
Leon Tappe committed
241
242
243
  verified,
  unverified,
  error,
244
  busy,
Leon Tappe's avatar
Leon Tappe committed
245
}