crypto_util.dart 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  1. import 'dart:io' as io;
  2. import 'dart:typed_data';
  3. import 'package:computer/computer.dart';
  4. import 'package:flutter_sodium/flutter_sodium.dart';
  5. import 'package:logging/logging.dart';
  6. import 'package:photos/models/derived_key_result.dart';
  7. import 'package:photos/models/encryption_result.dart';
  8. const int encryptionChunkSize = 4 * 1024 * 1024;
  9. final int decryptionChunkSize =
  10. encryptionChunkSize + Sodium.cryptoSecretstreamXchacha20poly1305Abytes;
  11. Uint8List cryptoSecretboxEasy(Map<String, dynamic> args) {
  12. return Sodium.cryptoSecretboxEasy(args["source"], args["nonce"], args["key"]);
  13. }
  14. Uint8List cryptoSecretboxOpenEasy(Map<String, dynamic> args) {
  15. return Sodium.cryptoSecretboxOpenEasy(
  16. args["cipher"],
  17. args["nonce"],
  18. args["key"],
  19. );
  20. }
  21. Uint8List cryptoPwHash(Map<String, dynamic> args) {
  22. return Sodium.cryptoPwhash(
  23. Sodium.cryptoSecretboxKeybytes,
  24. args["password"],
  25. args["salt"],
  26. args["opsLimit"],
  27. args["memLimit"],
  28. Sodium.cryptoPwhashAlgDefault,
  29. );
  30. }
  31. Future<Uint8List> cryptoGenericHash(Map<String, dynamic> args) async {
  32. final sourceFile = io.File(args["sourceFilePath"]);
  33. final sourceFileLength = await sourceFile.length();
  34. final inputFile = sourceFile.openSync(mode: io.FileMode.read);
  35. final state =
  36. Sodium.cryptoGenerichashInit(null, Sodium.cryptoGenerichashBytesMax);
  37. var bytesRead = 0;
  38. bool isDone = false;
  39. while (!isDone) {
  40. var chunkSize = encryptionChunkSize;
  41. if (bytesRead + chunkSize >= sourceFileLength) {
  42. chunkSize = sourceFileLength - bytesRead;
  43. isDone = true;
  44. }
  45. final buffer = await inputFile.read(chunkSize);
  46. bytesRead += chunkSize;
  47. Sodium.cryptoGenerichashUpdate(state, buffer);
  48. }
  49. await inputFile.close();
  50. return Sodium.cryptoGenerichashFinal(state, Sodium.cryptoGenerichashBytesMax);
  51. }
  52. EncryptionResult chachaEncryptData(Map<String, dynamic> args) {
  53. final initPushResult =
  54. Sodium.cryptoSecretstreamXchacha20poly1305InitPush(args["key"]);
  55. final encryptedData = Sodium.cryptoSecretstreamXchacha20poly1305Push(
  56. initPushResult.state,
  57. args["source"],
  58. null,
  59. Sodium.cryptoSecretstreamXchacha20poly1305TagFinal,
  60. );
  61. return EncryptionResult(
  62. encryptedData: encryptedData,
  63. header: initPushResult.header,
  64. );
  65. }
  66. Future<EncryptionResult> chachaEncryptFile(Map<String, dynamic> args) async {
  67. final encryptionStartTime = DateTime.now().millisecondsSinceEpoch;
  68. final logger = Logger("ChaChaEncrypt");
  69. final sourceFile = io.File(args["sourceFilePath"]);
  70. final destinationFile = io.File(args["destinationFilePath"]);
  71. final sourceFileLength = await sourceFile.length();
  72. logger.info("Encrypting file of size " + sourceFileLength.toString());
  73. final inputFile = sourceFile.openSync(mode: io.FileMode.read);
  74. final key = args["key"] ?? Sodium.cryptoSecretstreamXchacha20poly1305Keygen();
  75. final initPushResult =
  76. Sodium.cryptoSecretstreamXchacha20poly1305InitPush(key);
  77. var bytesRead = 0;
  78. var tag = Sodium.cryptoSecretstreamXchacha20poly1305TagMessage;
  79. while (tag != Sodium.cryptoSecretstreamXchacha20poly1305TagFinal) {
  80. var chunkSize = encryptionChunkSize;
  81. if (bytesRead + chunkSize >= sourceFileLength) {
  82. chunkSize = sourceFileLength - bytesRead;
  83. tag = Sodium.cryptoSecretstreamXchacha20poly1305TagFinal;
  84. }
  85. final buffer = await inputFile.read(chunkSize);
  86. bytesRead += chunkSize;
  87. final encryptedData = Sodium.cryptoSecretstreamXchacha20poly1305Push(
  88. initPushResult.state,
  89. buffer,
  90. null,
  91. tag,
  92. );
  93. await destinationFile.writeAsBytes(encryptedData, mode: io.FileMode.append);
  94. }
  95. await inputFile.close();
  96. logger.info(
  97. "Encryption time: " +
  98. (DateTime.now().millisecondsSinceEpoch - encryptionStartTime)
  99. .toString(),
  100. );
  101. return EncryptionResult(key: key, header: initPushResult.header);
  102. }
  103. Future<void> chachaDecryptFile(Map<String, dynamic> args) async {
  104. final logger = Logger("ChaChaDecrypt");
  105. final decryptionStartTime = DateTime.now().millisecondsSinceEpoch;
  106. final sourceFile = io.File(args["sourceFilePath"]);
  107. final destinationFile = io.File(args["destinationFilePath"]);
  108. final sourceFileLength = await sourceFile.length();
  109. logger.info("Decrypting file of size " + sourceFileLength.toString());
  110. final inputFile = sourceFile.openSync(mode: io.FileMode.read);
  111. final pullState = Sodium.cryptoSecretstreamXchacha20poly1305InitPull(
  112. args["header"],
  113. args["key"],
  114. );
  115. var bytesRead = 0;
  116. var tag = Sodium.cryptoSecretstreamXchacha20poly1305TagMessage;
  117. while (tag != Sodium.cryptoSecretstreamXchacha20poly1305TagFinal) {
  118. var chunkSize = decryptionChunkSize;
  119. if (bytesRead + chunkSize >= sourceFileLength) {
  120. chunkSize = sourceFileLength - bytesRead;
  121. }
  122. final buffer = await inputFile.read(chunkSize);
  123. bytesRead += chunkSize;
  124. final pullResult =
  125. Sodium.cryptoSecretstreamXchacha20poly1305Pull(pullState, buffer, null);
  126. await destinationFile.writeAsBytes(pullResult.m, mode: io.FileMode.append);
  127. tag = pullResult.tag;
  128. }
  129. inputFile.closeSync();
  130. logger.info(
  131. "ChaCha20 Decryption time: " +
  132. (DateTime.now().millisecondsSinceEpoch - decryptionStartTime)
  133. .toString(),
  134. );
  135. }
  136. Uint8List chachaDecryptData(Map<String, dynamic> args) {
  137. final pullState = Sodium.cryptoSecretstreamXchacha20poly1305InitPull(
  138. args["header"],
  139. args["key"],
  140. );
  141. final pullResult = Sodium.cryptoSecretstreamXchacha20poly1305Pull(
  142. pullState,
  143. args["source"],
  144. null,
  145. );
  146. return pullResult.m;
  147. }
  148. class CryptoUtil {
  149. static final Computer _computer = Computer();
  150. static init() {
  151. _computer.turnOn(workersCount: 4);
  152. Sodium.init();
  153. }
  154. static Uint8List base642bin(
  155. String b64, {
  156. String? ignore,
  157. int variant = Sodium.base64VariantOriginal,
  158. }) {
  159. return Sodium.base642bin(b64, ignore: ignore, variant: variant);
  160. }
  161. static String bin2base64(
  162. Uint8List bin, {
  163. bool urlSafe = false,
  164. }) {
  165. return Sodium.bin2base64(
  166. bin,
  167. variant:
  168. urlSafe ? Sodium.base64VariantUrlsafe : Sodium.base64VariantOriginal,
  169. );
  170. }
  171. static String bin2hex(Uint8List bin) {
  172. return Sodium.bin2hex(bin);
  173. }
  174. static Uint8List hex2bin(String hex) {
  175. return Sodium.hex2bin(hex);
  176. }
  177. static EncryptionResult encryptSync(Uint8List source, Uint8List key) {
  178. final nonce = Sodium.randombytesBuf(Sodium.cryptoSecretboxNoncebytes);
  179. final args = <String, dynamic>{};
  180. args["source"] = source;
  181. args["nonce"] = nonce;
  182. args["key"] = key;
  183. final encryptedData = cryptoSecretboxEasy(args);
  184. return EncryptionResult(
  185. key: key,
  186. nonce: nonce,
  187. encryptedData: encryptedData,
  188. );
  189. }
  190. static Future<Uint8List> decrypt(
  191. Uint8List cipher,
  192. Uint8List key,
  193. Uint8List nonce,
  194. ) async {
  195. final args = <String, dynamic>{};
  196. args["cipher"] = cipher;
  197. args["nonce"] = nonce;
  198. args["key"] = key;
  199. return _computer.compute(cryptoSecretboxOpenEasy, param: args);
  200. }
  201. static Uint8List decryptSync(
  202. Uint8List cipher,
  203. Uint8List? key,
  204. Uint8List nonce,
  205. ) {
  206. assert(key != null, "key can not be null");
  207. final args = <String, dynamic>{};
  208. args["cipher"] = cipher;
  209. args["nonce"] = nonce;
  210. args["key"] = key;
  211. return cryptoSecretboxOpenEasy(args);
  212. }
  213. static Future<EncryptionResult> encryptChaCha(
  214. Uint8List source,
  215. Uint8List key,
  216. ) async {
  217. final args = <String, dynamic>{};
  218. args["source"] = source;
  219. args["key"] = key;
  220. return _computer.compute(chachaEncryptData, param: args);
  221. }
  222. static Future<Uint8List> decryptChaCha(
  223. Uint8List source,
  224. Uint8List key,
  225. Uint8List header,
  226. ) async {
  227. final args = <String, dynamic>{};
  228. args["source"] = source;
  229. args["key"] = key;
  230. args["header"] = header;
  231. return _computer.compute(chachaDecryptData, param: args);
  232. }
  233. static Future<EncryptionResult> encryptFile(
  234. String sourceFilePath,
  235. String destinationFilePath, {
  236. Uint8List? key,
  237. }) {
  238. final args = <String, dynamic>{};
  239. args["sourceFilePath"] = sourceFilePath;
  240. args["destinationFilePath"] = destinationFilePath;
  241. args["key"] = key;
  242. return _computer.compute(chachaEncryptFile, param: args);
  243. }
  244. static Future<void> decryptFile(
  245. String sourceFilePath,
  246. String destinationFilePath,
  247. Uint8List header,
  248. Uint8List key,
  249. ) {
  250. final args = <String, dynamic>{};
  251. args["sourceFilePath"] = sourceFilePath;
  252. args["destinationFilePath"] = destinationFilePath;
  253. args["header"] = header;
  254. args["key"] = key;
  255. return _computer.compute(chachaDecryptFile, param: args);
  256. }
  257. static Uint8List generateKey() {
  258. return Sodium.cryptoSecretboxKeygen();
  259. }
  260. static Uint8List getSaltToDeriveKey() {
  261. return Sodium.randombytesBuf(Sodium.cryptoPwhashSaltbytes);
  262. }
  263. static Future<KeyPair> generateKeyPair() async {
  264. return Sodium.cryptoBoxKeypair();
  265. }
  266. static Uint8List openSealSync(
  267. Uint8List input,
  268. Uint8List publicKey,
  269. Uint8List secretKey,
  270. ) {
  271. return Sodium.cryptoBoxSealOpen(input, publicKey, secretKey);
  272. }
  273. static Uint8List sealSync(Uint8List input, Uint8List publicKey) {
  274. return Sodium.cryptoBoxSeal(input, publicKey);
  275. }
  276. // Derives a key for a given password and salt using Argon2id, v1.3.
  277. // The function first attempts to derive a key with both memLimit and opsLimit
  278. // set to their Sensitive variants.
  279. // If this fails, say on a device with insufficient RAM, we retry by halving
  280. // the memLimit and doubling the opsLimit, while ensuring that we stay within
  281. // the min and max limits for both parameters.
  282. // At all points, we ensure that the product of these two variables (the area
  283. // under the graph that determines the amount of work required) is a constant.
  284. static Future<DerivedKeyResult> deriveSensitiveKey(
  285. Uint8List password,
  286. Uint8List salt,
  287. ) async {
  288. assert(
  289. Sodium.cryptoPwhashAlgArgon2id13 == Sodium.cryptoPwhashAlgDefault,
  290. "mismatch in expected default pw hashing algo",
  291. );
  292. final logger = Logger("pwhash");
  293. int memLimit = Sodium.cryptoPwhashMemlimitSensitive;
  294. int opsLimit = Sodium.cryptoPwhashOpslimitSensitive;
  295. Uint8List key;
  296. while (memLimit >= Sodium.cryptoPwhashMemlimitMin &&
  297. opsLimit <= Sodium.cryptoPwhashOpslimitMax) {
  298. try {
  299. key = await deriveKey(password, salt, memLimit, opsLimit);
  300. return DerivedKeyResult(key, memLimit, opsLimit);
  301. } catch (e, s) {
  302. logger.severe(
  303. "failed to derive memLimit: $memLimit and opsLimit: $opsLimit",
  304. e,
  305. s,
  306. );
  307. }
  308. memLimit = (memLimit / 2).round();
  309. opsLimit = opsLimit * 2;
  310. }
  311. throw UnsupportedError("Cannot perform this operation on this device");
  312. }
  313. // Derives a key for the given password and salt, with memory and ops limit
  314. // hardcoded to their Interactive variants
  315. // NOTE: This is only used while setting passwords for shared links, as an
  316. // extra layer of authentication (atop the access token and collection key).
  317. // More details @ https://ente.io/blog/building-shareable-links/
  318. static Future<DerivedKeyResult> deriveInteractiveKey(
  319. Uint8List password,
  320. Uint8List salt,
  321. ) async {
  322. assert(
  323. Sodium.cryptoPwhashAlgArgon2id13 == Sodium.cryptoPwhashAlgDefault,
  324. "mismatch in expected default pw hashing algo",
  325. );
  326. final int memLimit = Sodium.cryptoPwhashMemlimitInteractive;
  327. final int opsLimit = Sodium.cryptoPwhashOpslimitInteractive;
  328. final key = await deriveKey(password, salt, memLimit, opsLimit);
  329. return DerivedKeyResult(key, memLimit, opsLimit);
  330. }
  331. static Future<Uint8List> deriveKey(
  332. Uint8List password,
  333. Uint8List salt,
  334. int memLimit,
  335. int opsLimit,
  336. ) {
  337. return _computer.compute(
  338. cryptoPwHash,
  339. param: {
  340. "password": password,
  341. "salt": salt,
  342. "memLimit": memLimit,
  343. "opsLimit": opsLimit,
  344. },
  345. );
  346. }
  347. static Future<Uint8List> getHash(io.File source) {
  348. return _computer.compute(
  349. cryptoGenericHash,
  350. param: {
  351. "sourceFilePath": source.path,
  352. },
  353. );
  354. }
  355. }