diff --git a/lib/oath/models.dart b/lib/oath/models.dart index 2a9ba7cc..2f389fb9 100755 --- a/lib/oath/models.dart +++ b/lib/oath/models.dart @@ -14,6 +14,10 @@ * limitations under the License. */ +import 'dart:typed_data'; +import 'dart:convert'; +import 'package:base32/base32.dart'; +import 'package:convert/convert.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -121,9 +125,96 @@ class CredentialData with _$CredentialData { _$CredentialDataFromJson(json); factory CredentialData.fromUri(Uri uri) { - if (uri.scheme.toLowerCase() != 'otpauth') { + List read(Uint8List bytes) { + final index = bytes[0]; + final sublist1 = bytes.sublist(1, index + 1); + final sublist2 = bytes.sublist(index + 1); + return [sublist1, sublist2]; + } + + String b32Encode(Uint8List data) { + final encodedData = base32.encode(data); + return utf8.decode(encodedData.runes.toList()); + } + + if (uri.scheme.toLowerCase() == 'otpauth-migration') { + final uriString = uri.toString(); + var data = Uint8List.fromList( + base64.decode(Uri.decodeComponent(uriString.split('=')[1]))); + + var credentials = []; + + var tag = data[0]; + + /* + Assuming the credential(s) follow the format: + cred = 0aLENGTH0aSECRET12NAME1aISSUER200128013002xxx + where xxx can be another cred. + */ + while (tag == 10) { + // 0a tag means new credential. + + var length = data[1]; // The length of this credential + var secretTag = data[2]; + if (secretTag != 10) { + // tag before secret is 0a hex + throw ArgumentError('Invalid scheme, no secret tag'); + } + data = data.sublist(3); + final result1 = read(data); + final secret = result1[0]; + data = result1[1]; + final decodedSecret = b32Encode(secret); + + var nameTag = data[0]; + if (nameTag != 18) { + // tag before name is 12 hex + throw ArgumentError('Invalid scheme, no name tag'); + } + data = data.sublist(1); + final result2 = read(data); + final name = result2[0]; + data = result2[1]; + + var issuerTag = data[0]; + var issuer; + if (issuerTag == 26) { + // tag before issuer is 1a hex, but issuer is optional. + data = data.sublist(1); + final result3 = read(data); + issuer = result3[0]; + data = result3[1]; + } + + final credential = CredentialData( + issuer: issuerTag != 26 + ? null + : utf8.decode(issuer, allowMalformed: true), + name: utf8.decode(name, allowMalformed: true), + secret: decodedSecret, + ); + + credentials.add(credential); + + var endTag = data.sublist(0, 6); + if (hex.encode(endTag) != '200128013002') { + // At the end of every credential there is 200128013002 + throw ArgumentError('Invalid scheme, no end tag'); + } + data = data.sublist(6); + tag = data[0]; + } + + // Print all the extracted credentials + for (var credential in credentials) { + print('${credential.issuer} (${credential.name}) ${credential.secret}'); + } + + return credentials[0]; // For now, return only the first credential. + } else if (uri.scheme.toLowerCase() != 'otpauth') { throw ArgumentError('Invalid scheme, must be "otpauth://"'); } + final oathType = OathType.values.byName(uri.host.toLowerCase()); final params = uri.queryParameters; String? issuer; diff --git a/pubspec.lock b/pubspec.lock index 0291513d..4dac2184 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -41,6 +41,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + base32: + dependency: "direct main" + description: + name: base32 + sha256: ddad4ebfedf93d4500818ed8e61443b734ffe7cf8a45c668c9b34ef6adde02e2 + url: "https://pub.dev" + source: hosted + version: "2.1.3" boolean_selector: dependency: transitive description: @@ -154,7 +162,7 @@ packages: source: hosted version: "1.17.1" convert: - dependency: transitive + dependency: "direct main" description: name: convert sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" diff --git a/pubspec.yaml b/pubspec.yaml index f1801cea..16cb8e57 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -64,6 +64,8 @@ dependencies: tray_manager: ^0.2.0 local_notifier: ^0.1.5 io: ^1.0.4 + base32: ^2.1.3 + convert: ^3.1.1 dev_dependencies: integration_test: