code.dart 5.7 KB

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