code.dart 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. import 'package:ente_auth/utils/totp_util.dart';
  2. class Code {
  3. static const defaultDigits = 6;
  4. static const steamDigits = 5;
  5. static const defaultPeriod = 30;
  6. int? generatedID;
  7. final String account;
  8. final String issuer;
  9. final int digits;
  10. final int period;
  11. final String secret;
  12. final Algorithm algorithm;
  13. final Type type;
  14. final String rawData;
  15. final int counter;
  16. bool? hasSynced;
  17. Code(
  18. this.account,
  19. this.issuer,
  20. this.digits,
  21. this.period,
  22. this.secret,
  23. this.algorithm,
  24. this.type,
  25. this.counter,
  26. this.rawData, {
  27. this.generatedID,
  28. });
  29. Code copyWith({
  30. String? account,
  31. String? issuer,
  32. int? digits,
  33. int? period,
  34. String? secret,
  35. Algorithm? algorithm,
  36. Type? type,
  37. int? counter,
  38. }) {
  39. final String updateAccount = account ?? this.account;
  40. final String updateIssuer = issuer ?? this.issuer;
  41. final int updatedDigits = digits ?? this.digits;
  42. final int updatePeriod = period ?? this.period;
  43. final String updatedSecret = secret ?? this.secret;
  44. final Algorithm updatedAlgo = algorithm ?? this.algorithm;
  45. final Type updatedType = type ?? this.type;
  46. final int updatedCounter = counter ?? this.counter;
  47. return Code(
  48. updateAccount,
  49. updateIssuer,
  50. updatedDigits,
  51. updatePeriod,
  52. updatedSecret,
  53. updatedAlgo,
  54. updatedType,
  55. updatedCounter,
  56. "otpauth://${updatedType.name}/$updateIssuer:$updateAccount?algorithm=${updatedAlgo.name}&digits=$updatedDigits&issuer=$updateIssuer&period=$updatePeriod&secret=$updatedSecret${updatedType == Type.hotp ? "&counter=$updatedCounter" : ""}",
  57. generatedID: generatedID,
  58. );
  59. }
  60. static Code fromAccountAndSecret(
  61. String account,
  62. String issuer,
  63. String secret,
  64. ) {
  65. final digits =
  66. issuer.toLowerCase() == "steam" ? steamDigits : defaultDigits;
  67. return Code(
  68. account,
  69. issuer,
  70. digits,
  71. defaultPeriod,
  72. secret,
  73. Algorithm.sha1,
  74. Type.totp,
  75. 0,
  76. "otpauth://totp/$issuer:$account?algorithm=SHA1&digits=6&issuer=$issuer&period=30&secret=$secret",
  77. );
  78. }
  79. static Code fromRawData(String rawData) {
  80. Uri uri = Uri.parse(rawData);
  81. final issuer = _getIssuer(uri);
  82. final digits = issuer.toLowerCase() == "steam" ? 5 : _getDigits(uri);
  83. try {
  84. return Code(
  85. _getAccount(uri),
  86. issuer,
  87. digits,
  88. _getPeriod(uri),
  89. getSanitizedSecret(uri.queryParameters['secret']!),
  90. _getAlgorithm(uri),
  91. _getType(uri),
  92. _getCounter(uri),
  93. rawData,
  94. );
  95. } catch (e) {
  96. // if account name contains # without encoding,
  97. // rest of the url are treated as url fragment
  98. if (rawData.contains("#")) {
  99. return Code.fromRawData(rawData.replaceAll("#", '%23'));
  100. } else {
  101. rethrow;
  102. }
  103. }
  104. }
  105. static String _getAccount(Uri uri) {
  106. try {
  107. String path = Uri.decodeComponent(uri.path);
  108. if (path.startsWith("/")) {
  109. path = path.substring(1, path.length);
  110. }
  111. // Parse account name from documented auth URI
  112. // otpauth://totp/ACCOUNT?secret=SUPERSECRET&issuer=SERVICE
  113. if (uri.queryParameters.containsKey("issuer") && !path.contains(":")) {
  114. return path;
  115. }
  116. return path.split(':')[1];
  117. } catch (e) {
  118. return "";
  119. }
  120. }
  121. static String _getIssuer(Uri uri) {
  122. try {
  123. if (uri.queryParameters.containsKey("issuer")) {
  124. String issuerName = uri.queryParameters['issuer']!;
  125. // Handle issuer name with period
  126. // See https://github.com/ente-io/ente/pull/77
  127. if (issuerName.contains("period=")) {
  128. return issuerName.substring(0, issuerName.indexOf("period="));
  129. }
  130. return issuerName;
  131. }
  132. final String path = Uri.decodeComponent(uri.path);
  133. return path.split(':')[0].substring(1);
  134. } catch (e) {
  135. return "";
  136. }
  137. }
  138. static int _getDigits(Uri uri) {
  139. try {
  140. return int.parse(uri.queryParameters['digits']!);
  141. } catch (e) {
  142. return defaultDigits;
  143. }
  144. }
  145. static int _getPeriod(Uri uri) {
  146. try {
  147. return int.parse(uri.queryParameters['period']!);
  148. } catch (e) {
  149. return defaultPeriod;
  150. }
  151. }
  152. static int _getCounter(Uri uri) {
  153. try {
  154. final bool hasCounterKey = uri.queryParameters.containsKey('counter');
  155. if (!hasCounterKey) {
  156. return 0;
  157. }
  158. return int.parse(uri.queryParameters['counter']!);
  159. } catch (e) {
  160. return defaultPeriod;
  161. }
  162. }
  163. static Algorithm _getAlgorithm(Uri uri) {
  164. try {
  165. final algorithm =
  166. uri.queryParameters['algorithm'].toString().toLowerCase();
  167. if (algorithm == "sha256") {
  168. return Algorithm.sha256;
  169. } else if (algorithm == "sha512") {
  170. return Algorithm.sha512;
  171. }
  172. } catch (e) {
  173. // nothing
  174. }
  175. return Algorithm.sha1;
  176. }
  177. static Type _getType(Uri uri) {
  178. if (uri.host == "totp" || uri.host == "steam") {
  179. return Type.totp;
  180. } else if (uri.host == "hotp") {
  181. return Type.hotp;
  182. }
  183. throw UnsupportedError("Unsupported format with host ${uri.host}");
  184. }
  185. @override
  186. bool operator ==(Object other) {
  187. if (identical(this, other)) return true;
  188. return other is Code &&
  189. other.account == account &&
  190. other.issuer == issuer &&
  191. other.digits == digits &&
  192. other.period == period &&
  193. other.secret == secret &&
  194. other.counter == counter &&
  195. other.type == type &&
  196. other.rawData == rawData;
  197. }
  198. @override
  199. int get hashCode {
  200. return account.hashCode ^
  201. issuer.hashCode ^
  202. digits.hashCode ^
  203. period.hashCode ^
  204. secret.hashCode ^
  205. type.hashCode ^
  206. counter.hashCode ^
  207. rawData.hashCode;
  208. }
  209. }
  210. enum Type {
  211. totp,
  212. hotp,
  213. }
  214. enum Algorithm {
  215. sha1,
  216. sha256,
  217. sha512,
  218. }