otpauth_migration.dart 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. /// Provides encode and decode functions for the otpauth-migration URI format used to import into and export 2FA secrets from the Google Authenticator app
  2. library otpauth_migration;
  3. import 'dart:convert';
  4. import 'dart:typed_data';
  5. import 'generated/GoogleAuthenticatorImport.pb.dart';
  6. /// A stateless class (not a singleton) that provides encode and decode functions for the otpauth-migration URI format used to import into and export 2FA secrets from the Google Authenticator app
  7. class OtpAuthMigration {
  8. final List<String> _rfc3548 = [
  9. "A",
  10. "B",
  11. "C",
  12. "D",
  13. "E",
  14. "F",
  15. "G",
  16. "H",
  17. "I",
  18. "J",
  19. "K",
  20. "L",
  21. "M",
  22. "N",
  23. "O",
  24. "P",
  25. "Q",
  26. "R",
  27. "S",
  28. "T",
  29. "U",
  30. "V",
  31. "W",
  32. "X",
  33. "Y",
  34. "Z",
  35. "2",
  36. "3",
  37. "4",
  38. "5",
  39. "6",
  40. "7"
  41. ];
  42. /// encode given list of optauth URIs into a single otpauth-migration URI
  43. String encode(List<String> otpAuths,
  44. {bool debug = false,
  45. int version = -1,
  46. int batchSize = -1,
  47. int batchIndex = -1,
  48. int batchId = -1}) {
  49. var gai = GoogleAuthenticatorImport();
  50. for (var otp in otpAuths) {
  51. var uri = Uri.parse(otp);
  52. //uri.queryParameters.forEach((k, v) {
  53. // print('key: $k - value: $v');
  54. // });
  55. var gaip = GoogleAuthenticatorImport_OtpParameters();
  56. gaip.name = Uri.decodeFull(uri.path.substring(1));
  57. //print("name = ${gaip.name}");
  58. try {
  59. gaip.secret = _encodeBase32(uri.queryParameters['secret']);
  60. } catch (e) {
  61. return "";
  62. }
  63. //print("issuer = ${uri.queryParameters['issuer']}");
  64. var issuer = uri.queryParameters['issuer'];
  65. if (issuer != null && issuer != "") {
  66. gaip.issuer = issuer;
  67. }
  68. gaip.type = GoogleAuthenticatorImport_OtpType.OTP_TYPE_TOTP;
  69. gaip.algorithm = GoogleAuthenticatorImport_Algorithm.ALGORITHM_SHA1;
  70. gaip.digits = GoogleAuthenticatorImport_DigitCount.DIGIT_COUNT_SIX;
  71. gai.otpParameters.add(gaip);
  72. if (debug) print(gaip);
  73. }
  74. if (version >= 0) gai.version = version;
  75. if (batchSize >= 0) gai.batchSize = batchSize;
  76. if (batchIndex >= 0) gai.batchIndex = batchIndex;
  77. if (batchId >= 0) gai.batchId = batchId;
  78. final bytes = gai.writeToBuffer();
  79. return "otpauth-migration://offline?data=${base64.encode(bytes)}";
  80. }
  81. int batchSize(String value) {
  82. RegExp exp = RegExp(r"otpauth-migration\:\/\/offline\?data=(.*)$");
  83. final match = exp.firstMatch(value);
  84. final encodedUrl = match?.group(1);
  85. if (encodedUrl != null) {
  86. final encoded = Uri.decodeComponent(encodedUrl);
  87. var decoded = base64.decode(encoded);
  88. try {
  89. final gai = GoogleAuthenticatorImport.fromBuffer(decoded);
  90. return gai.batchSize;
  91. } catch(e) {
  92. return -1;
  93. }
  94. } else {
  95. return -1;
  96. }
  97. }
  98. int batchIndex(String value) {
  99. RegExp exp = RegExp(r"otpauth-migration\:\/\/offline\?data=(.*)$");
  100. final match = exp.firstMatch(value);
  101. final encodedUrl = match?.group(1);
  102. if (encodedUrl != null) {
  103. final encoded = Uri.decodeComponent(encodedUrl);
  104. var decoded = base64.decode(encoded);
  105. try {
  106. final gai = GoogleAuthenticatorImport.fromBuffer(decoded);
  107. return gai.batchIndex;
  108. } catch(e) {
  109. return -1;
  110. }
  111. } else {
  112. return -1;
  113. }
  114. }
  115. /// decode a given otpauth-migration URI into a list of one or more otpauth URIs
  116. List<String> decode(String value, {bool debug = false}) {
  117. // check prefix "otpauth-migration://offline?data="
  118. // extract suffix - Base64 encode
  119. List<String> results = [];
  120. RegExp exp = RegExp(r"otpauth-migration\:\/\/offline\?data=(.*)$");
  121. final match = exp.firstMatch(value);
  122. final encodedUrl = match?.group(1);
  123. if (encodedUrl != null) {
  124. final encoded = Uri.decodeComponent(encodedUrl);
  125. var decoded = base64.decode(encoded);
  126. try {
  127. final gai = GoogleAuthenticatorImport.fromBuffer(decoded);
  128. if (debug) print(gai);
  129. //print(gai.otpParameters.length);
  130. for (var param in gai.otpParameters) {
  131. //print(param);
  132. var base32 = _decodeBase32(param.secret);
  133. //print("otpauth://totp/${param.name}?secret=${base32}");
  134. final name = Uri.encodeFull(param.name);
  135. final issuer = Uri.encodeComponent(param.issuer);
  136. String algorithm = "";
  137. switch(param.algorithm) {
  138. case GoogleAuthenticatorImport_Algorithm.ALGORITHM_SHA1:
  139. algorithm = "&algorithm=SHA1";
  140. break;
  141. case GoogleAuthenticatorImport_Algorithm.ALGORITHM_SHA256:
  142. algorithm = "&algorithm=SHA256";
  143. break;
  144. case GoogleAuthenticatorImport_Algorithm.ALGORITHM_SHA512:
  145. algorithm = "&algorithm=SHA512";
  146. break;
  147. case GoogleAuthenticatorImport_Algorithm.ALGORITHM_MD5:
  148. algorithm = "&algorithm=MD5";
  149. break;
  150. default:
  151. algorithm = "";
  152. break;
  153. }
  154. String digits = "";
  155. switch(param.digits) {
  156. case GoogleAuthenticatorImport_DigitCount.DIGIT_COUNT_SIX:
  157. digits = "&digits=6";
  158. break;
  159. case GoogleAuthenticatorImport_DigitCount.DIGIT_COUNT_EIGHT:
  160. digits = "&digits=8";
  161. break;
  162. default:
  163. digits = "";
  164. break;
  165. }
  166. results.add("otpauth://totp/$name?secret=$base32&issuer=$issuer$algorithm$digits&period=30");
  167. }
  168. //print("good");
  169. return results;
  170. } catch (e) {
  171. return results;
  172. }
  173. } else {
  174. //print("bad");
  175. return [];
  176. }
  177. }
  178. List<int> _encodeBase32(String? s) {
  179. int i = 0;
  180. var j = 0;
  181. if (s != null) {
  182. Uint8List result = Uint8List((s.length * 5) ~/ 8);
  183. while (i < s.length) {
  184. // ZERO
  185. var c = s[i];
  186. var k = _rfc3548.indexOf(c);
  187. //print(k);
  188. result[j] = k << 3;
  189. i++;
  190. c = s[i];
  191. k = _rfc3548.indexOf(c);
  192. //print(k);
  193. result[j] = result[j] | (k >> 2);
  194. //print("---> ${result[j]}");
  195. j++;
  196. // ONE
  197. result[j] = k << 6;
  198. i++;
  199. c = s[i];
  200. k = _rfc3548.indexOf(c);
  201. //print(k);
  202. result[j] = result[j] | (k << 1);
  203. i++;
  204. c = s[i];
  205. k = _rfc3548.indexOf(c);
  206. //print(k);
  207. result[j] = result[j] | (k >> 4);
  208. //print("---> ${result[j]}");
  209. j++;
  210. // TWO
  211. result[j] = k << 4;
  212. i++;
  213. c = s[i];
  214. k = _rfc3548.indexOf(c);
  215. //print(k);
  216. result[j] = result[j] | (k >> 1);
  217. //print("---> ${result[j]}");
  218. j++;
  219. // THREE
  220. result[j] = k << 7;
  221. i++;
  222. c = s[i];
  223. k = _rfc3548.indexOf(c);
  224. //print(k);
  225. result[j] = result[j] | (k << 2);
  226. i++;
  227. c = s[i];
  228. k = _rfc3548.indexOf(c);
  229. //print(k);
  230. result[j] = result[j] | (k >> 3);
  231. //print("---> ${result[j]}");
  232. j++;
  233. // FOUR
  234. result[j] = k << 5;
  235. i++;
  236. c = s[i];
  237. k = _rfc3548.indexOf(c);
  238. //print(k);
  239. result[j] = result[j] | k;
  240. i++;
  241. //print("---> ${result[j]}");
  242. j++;
  243. }
  244. //print(result);
  245. return result;
  246. }
  247. return Uint8List(0);
  248. }
  249. String _decodeBase32(List<int> s) {
  250. //print(s);
  251. Uint8List ulist = s as Uint8List;
  252. String result = "";
  253. var i = 0;
  254. while (i < ulist.length) {
  255. var q0 = ulist[i] & 0xF8;
  256. q0 = q0 >> 3;
  257. //print(_rfc3548[q0]);
  258. result += _rfc3548[q0];
  259. var q1 = ulist[i++] & 0x07;
  260. q1 = q1 << 2;
  261. var temp = ulist[i] & 0xC0;
  262. temp = temp >> 6;
  263. q1 = q1 + temp;
  264. //print(_rfc3548[q1]);
  265. result += _rfc3548[q1];
  266. var q2 = ulist[i] & 0x3E;
  267. q2 = q2 >> 1;
  268. //print(_rfc3548[q2]);
  269. result += _rfc3548[q2];
  270. var q3 = ulist[i++] & 0x01;
  271. q3 = q3 << 4;
  272. temp = ulist[i] & 0xF0;
  273. temp = temp >> 4;
  274. q3 = q3 + temp;
  275. //print(_rfc3548[q3]);
  276. result += _rfc3548[q3];
  277. var q4 = ulist[i++] & 0x0F;
  278. q4 = q4 << 1;
  279. temp = ulist[i] & 0x80;
  280. temp = temp >> 7;
  281. q4 = q4 + temp;
  282. //print(_rfc3548[q4]);
  283. result += _rfc3548[q4];
  284. var q5 = ulist[i] & 0x7c;
  285. q5 = q5 >> 2;
  286. //print(_rfc3548[q5]);
  287. result += _rfc3548[q5];
  288. var q6 = ulist[i++] & 0x03;
  289. q6 = q6 << 3;
  290. temp = ulist[i] & 0xE0;
  291. temp = temp >> 5;
  292. q6 = q6 + temp;
  293. //print(_rfc3548[q6]);
  294. result += _rfc3548[q6];
  295. var q7 = ulist[i++] & 0x1F;
  296. //print(_rfc3548[q7]);
  297. result += _rfc3548[q7];
  298. }
  299. //print(result);
  300. return result;
  301. }
  302. }