code.dart 5.5 KB

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