code.dart 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  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}"
  57. "&digits=$updatedDigits&issuer=$updateIssuer"
  58. "&period=$updatePeriod&secret=$updatedSecret${updatedType == Type.hotp ? "&counter=$updatedCounter" : ""}",
  59. generatedID: generatedID,
  60. );
  61. }
  62. static Code fromAccountAndSecret(
  63. Type type,
  64. String account,
  65. String issuer,
  66. String secret,
  67. int digits,
  68. ) {
  69. return Code(
  70. account,
  71. issuer,
  72. digits,
  73. defaultPeriod,
  74. secret,
  75. Algorithm.sha1,
  76. type,
  77. 0,
  78. "otpauth://${type.name}/$issuer:$account?algorithm=SHA1&digits=$digits&issuer=$issuer&period=30&secret=$secret",
  79. );
  80. }
  81. static Code fromRawData(String rawData) {
  82. Uri uri = Uri.parse(rawData);
  83. final issuer = _getIssuer(uri);
  84. try {
  85. return Code(
  86. _getAccount(uri),
  87. issuer,
  88. _getDigits(uri, issuer),
  89. _getPeriod(uri),
  90. getSanitizedSecret(uri.queryParameters['secret']!),
  91. _getAlgorithm(uri),
  92. _getType(uri),
  93. _getCounter(uri),
  94. rawData,
  95. );
  96. } catch (e) {
  97. // if account name contains # without encoding,
  98. // rest of the url are treated as url fragment
  99. if (rawData.contains("#")) {
  100. return Code.fromRawData(rawData.replaceAll("#", '%23'));
  101. } else {
  102. rethrow;
  103. }
  104. }
  105. }
  106. static String _getAccount(Uri uri) {
  107. try {
  108. String path = Uri.decodeComponent(uri.path);
  109. if (path.startsWith("/")) {
  110. path = path.substring(1, path.length);
  111. }
  112. // Parse account name from documented auth URI
  113. // otpauth://totp/ACCOUNT?secret=SUPERSECRET&issuer=SERVICE
  114. if (uri.queryParameters.containsKey("issuer") && !path.contains(":")) {
  115. return path;
  116. }
  117. return path.split(':')[1];
  118. } catch (e) {
  119. return "";
  120. }
  121. }
  122. static String _getIssuer(Uri uri) {
  123. try {
  124. if (uri.queryParameters.containsKey("issuer")) {
  125. String issuerName = uri.queryParameters['issuer']!;
  126. // Handle issuer name with period
  127. // See https://github.com/ente-io/ente/pull/77
  128. if (issuerName.contains("period=")) {
  129. return issuerName.substring(0, issuerName.indexOf("period="));
  130. }
  131. return issuerName;
  132. }
  133. final String path = Uri.decodeComponent(uri.path);
  134. return path.split(':')[0].substring(1);
  135. } catch (e) {
  136. return "";
  137. }
  138. }
  139. static int _getDigits(Uri uri, String issuer) {
  140. try {
  141. return int.parse(uri.queryParameters['digits']!);
  142. } catch (e) {
  143. if (issuer.toLowerCase() == "steam") {
  144. return steamDigits;
  145. }
  146. return defaultDigits;
  147. }
  148. }
  149. static int _getPeriod(Uri uri) {
  150. try {
  151. return int.parse(uri.queryParameters['period']!);
  152. } catch (e) {
  153. return defaultPeriod;
  154. }
  155. }
  156. static int _getCounter(Uri uri) {
  157. try {
  158. final bool hasCounterKey = uri.queryParameters.containsKey('counter');
  159. if (!hasCounterKey) {
  160. return 0;
  161. }
  162. return int.parse(uri.queryParameters['counter']!);
  163. } catch (e) {
  164. return defaultPeriod;
  165. }
  166. }
  167. static Algorithm _getAlgorithm(Uri uri) {
  168. try {
  169. final algorithm =
  170. uri.queryParameters['algorithm'].toString().toLowerCase();
  171. if (algorithm == "sha256") {
  172. return Algorithm.sha256;
  173. } else if (algorithm == "sha512") {
  174. return Algorithm.sha512;
  175. }
  176. } catch (e) {
  177. // nothing
  178. }
  179. return Algorithm.sha1;
  180. }
  181. static Type _getType(Uri uri) {
  182. if (uri.host == "totp") {
  183. return Type.totp;
  184. } else if (uri.host == "steam") {
  185. return Type.steam;
  186. } else if (uri.host == "hotp") {
  187. return Type.hotp;
  188. }
  189. throw UnsupportedError("Unsupported format with host ${uri.host}");
  190. }
  191. @override
  192. bool operator ==(Object other) {
  193. if (identical(this, other)) return true;
  194. return other is Code &&
  195. other.account == account &&
  196. other.issuer == issuer &&
  197. other.digits == digits &&
  198. other.period == period &&
  199. other.secret == secret &&
  200. other.counter == counter &&
  201. other.type == type &&
  202. other.rawData == rawData;
  203. }
  204. @override
  205. int get hashCode {
  206. return account.hashCode ^
  207. issuer.hashCode ^
  208. digits.hashCode ^
  209. period.hashCode ^
  210. secret.hashCode ^
  211. type.hashCode ^
  212. counter.hashCode ^
  213. rawData.hashCode;
  214. }
  215. }
  216. enum Type {
  217. totp,
  218. hotp,
  219. steam;
  220. bool get isTOTPCompatible => this == totp || this == steam;
  221. }
  222. enum Algorithm {
  223. sha1,
  224. sha256,
  225. sha512,
  226. }