crypto_util.dart 15 KB

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