crypto_util.dart 16 KB

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