crypto_util.dart 14 KB

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