123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509 |
- import "dart:convert";
- import "dart:io";
- import 'dart:typed_data';
- import 'package:computer/computer.dart';
- import 'package:flutter_sodium/flutter_sodium.dart';
- import 'package:logging/logging.dart';
- import "package:photos/core/errors.dart";
- import 'package:photos/models/derived_key_result.dart';
- import 'package:photos/models/encryption_result.dart';
- import "package:photos/utils/device_info.dart";
- const int encryptionChunkSize = 4 * 1024 * 1024;
- final int decryptionChunkSize =
- encryptionChunkSize + Sodium.cryptoSecretstreamXchacha20poly1305Abytes;
- const int hashChunkSize = 4 * 1024 * 1024;
- const int loginSubKeyLen = 32;
- const int loginSubKeyId = 1;
- const String loginSubKeyContext = "loginctx";
- Uint8List cryptoSecretboxEasy(Map<String, dynamic> args) {
- return Sodium.cryptoSecretboxEasy(args["source"], args["nonce"], args["key"]);
- }
- Uint8List cryptoSecretboxOpenEasy(Map<String, dynamic> args) {
- return Sodium.cryptoSecretboxOpenEasy(
- args["cipher"],
- args["nonce"],
- args["key"],
- );
- }
- Uint8List cryptoPwHash(Map<String, dynamic> args) {
- return Sodium.cryptoPwhash(
- Sodium.cryptoSecretboxKeybytes,
- args["password"],
- args["salt"],
- args["opsLimit"],
- args["memLimit"],
- Sodium.cryptoPwhashAlgArgon2id13,
- );
- }
- Uint8List cryptoKdfDeriveFromKey(
- Map<String, dynamic> args,
- ) {
- return Sodium.cryptoKdfDeriveFromKey(
- args["subkeyLen"],
- args["subkeyId"],
- args["context"],
- args["key"],
- );
- }
- // Returns the hash for a given file, chunking it in batches of hashChunkSize
- Future<Uint8List> cryptoGenericHash(Map<String, dynamic> args) async {
- final sourceFile = File(args["sourceFilePath"]);
- final sourceFileLength = await sourceFile.length();
- final inputFile = sourceFile.openSync(mode: FileMode.read);
- final state =
- Sodium.cryptoGenerichashInit(null, Sodium.cryptoGenerichashBytesMax);
- var bytesRead = 0;
- bool isDone = false;
- while (!isDone) {
- var chunkSize = hashChunkSize;
- if (bytesRead + chunkSize >= sourceFileLength) {
- chunkSize = sourceFileLength - bytesRead;
- isDone = true;
- }
- final buffer = await inputFile.read(chunkSize);
- bytesRead += chunkSize;
- Sodium.cryptoGenerichashUpdate(state, buffer);
- }
- await inputFile.close();
- return Sodium.cryptoGenerichashFinal(state, Sodium.cryptoGenerichashBytesMax);
- }
- EncryptionResult chachaEncryptData(Map<String, dynamic> args) {
- final initPushResult =
- Sodium.cryptoSecretstreamXchacha20poly1305InitPush(args["key"]);
- final encryptedData = Sodium.cryptoSecretstreamXchacha20poly1305Push(
- initPushResult.state,
- args["source"],
- null,
- Sodium.cryptoSecretstreamXchacha20poly1305TagFinal,
- );
- return EncryptionResult(
- encryptedData: encryptedData,
- header: initPushResult.header,
- );
- }
- // Encrypts a given file, in chunks of encryptionChunkSize
- Future<EncryptionResult> chachaEncryptFile(Map<String, dynamic> args) async {
- final encryptionStartTime = DateTime.now().millisecondsSinceEpoch;
- final logger = Logger("ChaChaEncrypt");
- final sourceFile = File(args["sourceFilePath"]);
- final destinationFile = File(args["destinationFilePath"]);
- final sourceFileLength = await sourceFile.length();
- logger.info("Encrypting file of size " + sourceFileLength.toString());
- final inputFile = sourceFile.openSync(mode: FileMode.read);
- final key = args["key"] ?? Sodium.cryptoSecretstreamXchacha20poly1305Keygen();
- final initPushResult =
- Sodium.cryptoSecretstreamXchacha20poly1305InitPush(key);
- var bytesRead = 0;
- var tag = Sodium.cryptoSecretstreamXchacha20poly1305TagMessage;
- while (tag != Sodium.cryptoSecretstreamXchacha20poly1305TagFinal) {
- var chunkSize = encryptionChunkSize;
- if (bytesRead + chunkSize >= sourceFileLength) {
- chunkSize = sourceFileLength - bytesRead;
- tag = Sodium.cryptoSecretstreamXchacha20poly1305TagFinal;
- }
- final buffer = await inputFile.read(chunkSize);
- bytesRead += chunkSize;
- final encryptedData = Sodium.cryptoSecretstreamXchacha20poly1305Push(
- initPushResult.state,
- buffer,
- null,
- tag,
- );
- await destinationFile.writeAsBytes(encryptedData, mode: FileMode.append);
- }
- await inputFile.close();
- logger.info(
- "Encryption time: " +
- (DateTime.now().millisecondsSinceEpoch - encryptionStartTime)
- .toString(),
- );
- return EncryptionResult(key: key, header: initPushResult.header);
- }
- Future<void> chachaDecryptFile(Map<String, dynamic> args) async {
- final logger = Logger("ChaChaDecrypt");
- final decryptionStartTime = DateTime.now().millisecondsSinceEpoch;
- final sourceFile = File(args["sourceFilePath"]);
- final destinationFile = File(args["destinationFilePath"]);
- final sourceFileLength = await sourceFile.length();
- logger.info("Decrypting file of size " + sourceFileLength.toString());
- final inputFile = sourceFile.openSync(mode: FileMode.read);
- final pullState = Sodium.cryptoSecretstreamXchacha20poly1305InitPull(
- args["header"],
- args["key"],
- );
- var bytesRead = 0;
- var tag = Sodium.cryptoSecretstreamXchacha20poly1305TagMessage;
- while (tag != Sodium.cryptoSecretstreamXchacha20poly1305TagFinal) {
- var chunkSize = decryptionChunkSize;
- if (bytesRead + chunkSize >= sourceFileLength) {
- chunkSize = sourceFileLength - bytesRead;
- }
- final buffer = await inputFile.read(chunkSize);
- bytesRead += chunkSize;
- final pullResult =
- Sodium.cryptoSecretstreamXchacha20poly1305Pull(pullState, buffer, null);
- await destinationFile.writeAsBytes(pullResult.m, mode: FileMode.append);
- tag = pullResult.tag;
- }
- inputFile.closeSync();
- logger.info(
- "ChaCha20 Decryption time: " +
- (DateTime.now().millisecondsSinceEpoch - decryptionStartTime)
- .toString(),
- );
- }
- Uint8List chachaDecryptData(Map<String, dynamic> args) {
- final pullState = Sodium.cryptoSecretstreamXchacha20poly1305InitPull(
- args["header"],
- args["key"],
- );
- final pullResult = Sodium.cryptoSecretstreamXchacha20poly1305Pull(
- pullState,
- args["source"],
- null,
- );
- return pullResult.m;
- }
- class CryptoUtil {
- // Note: workers are turned on during app startup.
- static final Computer _computer = Computer.shared();
- static init() {
- Sodium.init();
- }
- static Uint8List base642bin(
- String b64, {
- String? ignore,
- int variant = Sodium.base64VariantOriginal,
- }) {
- return Sodium.base642bin(b64, ignore: ignore, variant: variant);
- }
- static String bin2base64(
- Uint8List bin, {
- bool urlSafe = false,
- }) {
- return Sodium.bin2base64(
- bin,
- variant:
- urlSafe ? Sodium.base64VariantUrlsafe : Sodium.base64VariantOriginal,
- );
- }
- static String bin2hex(Uint8List bin) {
- return Sodium.bin2hex(bin);
- }
- static Uint8List hex2bin(String hex) {
- return Sodium.hex2bin(hex);
- }
- // Encrypts the given source, with the given key and a randomly generated
- // nonce, using XSalsa20 (w Poly1305 MAC).
- // This function runs on the same thread as the caller, so should be used only
- // for small amounts of data where thread switching can result in a degraded
- // user experience
- static EncryptionResult encryptSync(Uint8List source, Uint8List key) {
- final nonce = Sodium.randombytesBuf(Sodium.cryptoSecretboxNoncebytes);
- final args = <String, dynamic>{};
- args["source"] = source;
- args["nonce"] = nonce;
- args["key"] = key;
- final encryptedData = cryptoSecretboxEasy(args);
- return EncryptionResult(
- key: key,
- nonce: nonce,
- encryptedData: encryptedData,
- );
- }
- // Decrypts the given cipher, with the given key and nonce using XSalsa20
- // (w Poly1305 MAC).
- static Future<Uint8List> decrypt(
- Uint8List cipher,
- Uint8List key,
- Uint8List nonce,
- ) async {
- final args = <String, dynamic>{};
- args["cipher"] = cipher;
- args["nonce"] = nonce;
- args["key"] = key;
- return _computer.compute(
- cryptoSecretboxOpenEasy,
- param: args,
- taskName: "decrypt",
- );
- }
- // Decrypts the given cipher, with the given key and nonce using XSalsa20
- // (w Poly1305 MAC).
- // This function runs on the same thread as the caller, so should be used only
- // for small amounts of data where thread switching can result in a degraded
- // user experience
- static Uint8List decryptSync(
- Uint8List cipher,
- Uint8List key,
- Uint8List nonce,
- ) {
- final args = <String, dynamic>{};
- args["cipher"] = cipher;
- args["nonce"] = nonce;
- args["key"] = key;
- return cryptoSecretboxOpenEasy(args);
- }
- // Encrypts the given source, with the given key and a randomly generated
- // nonce, using XChaCha20 (w Poly1305 MAC).
- // This function runs on the isolate pool held by `_computer`.
- // TODO: Remove "ChaCha", an implementation detail from the function name
- static Future<EncryptionResult> encryptChaCha(
- Uint8List source,
- Uint8List key,
- ) async {
- final args = <String, dynamic>{};
- args["source"] = source;
- args["key"] = key;
- return _computer.compute(
- chachaEncryptData,
- param: args,
- taskName: "encryptChaCha",
- );
- }
- // Decrypts the given source, with the given key and header using XChaCha20
- // (w Poly1305 MAC).
- // TODO: Remove "ChaCha", an implementation detail from the function name
- static Future<Uint8List> decryptChaCha(
- Uint8List source,
- Uint8List key,
- Uint8List header,
- ) async {
- final args = <String, dynamic>{};
- args["source"] = source;
- args["key"] = key;
- args["header"] = header;
- return _computer.compute(
- chachaDecryptData,
- param: args,
- taskName: "decryptChaCha",
- );
- }
- // Encrypts the file at sourceFilePath, with the key (if provided) and a
- // randomly generated nonce using XChaCha20 (w Poly1305 MAC), and writes it
- // to the destinationFilePath.
- // If a key is not provided, one is generated and returned.
- static Future<EncryptionResult> encryptFile(
- String sourceFilePath,
- String destinationFilePath, {
- Uint8List? key,
- }) {
- final args = <String, dynamic>{};
- args["sourceFilePath"] = sourceFilePath;
- args["destinationFilePath"] = destinationFilePath;
- args["key"] = key;
- return _computer.compute(
- chachaEncryptFile,
- param: args,
- taskName: "encryptFile",
- );
- }
- // Decrypts the file at sourceFilePath, with the given key and header using
- // XChaCha20 (w Poly1305 MAC), and writes it to the destinationFilePath.
- static Future<void> decryptFile(
- String sourceFilePath,
- String destinationFilePath,
- Uint8List header,
- Uint8List key,
- ) {
- final args = <String, dynamic>{};
- args["sourceFilePath"] = sourceFilePath;
- args["destinationFilePath"] = destinationFilePath;
- args["header"] = header;
- args["key"] = key;
- return _computer.compute(
- chachaDecryptFile,
- param: args,
- taskName: "decryptFile",
- );
- }
- // Generates and returns a 256-bit key.
- static Uint8List generateKey() {
- return Sodium.cryptoSecretboxKeygen();
- }
- // Generates and returns a random byte buffer of length
- // crypto_pwhash_SALTBYTES (16)
- static Uint8List getSaltToDeriveKey() {
- return Sodium.randombytesBuf(Sodium.cryptoPwhashSaltbytes);
- }
- // Generates and returns a secret key and the corresponding public key.
- static Future<KeyPair> generateKeyPair() async {
- return Sodium.cryptoBoxKeypair();
- }
- // Decrypts the input using the given publicKey-secretKey pair
- static Uint8List openSealSync(
- Uint8List input,
- Uint8List publicKey,
- Uint8List secretKey,
- ) {
- return Sodium.cryptoBoxSealOpen(input, publicKey, secretKey);
- }
- // Encrypts the input using the given publicKey
- static Uint8List sealSync(Uint8List input, Uint8List publicKey) {
- return Sodium.cryptoBoxSeal(input, publicKey);
- }
- // Derives a key for a given password and salt using Argon2id, v1.3.
- // The function first attempts to derive a key with both memLimit and opsLimit
- // set to their Sensitive variants.
- // If this fails, say on a device with insufficient RAM, we retry by halving
- // the memLimit and doubling the opsLimit, while ensuring that we stay within
- // the min and max limits for both parameters.
- // At all points, we ensure that the product of these two variables (the area
- // under the graph that determines the amount of work required) is a constant.
- static Future<DerivedKeyResult> deriveSensitiveKey(
- Uint8List password,
- Uint8List salt,
- ) async {
- final logger = Logger("pwhash");
- int memLimit = Sodium.cryptoPwhashMemlimitSensitive;
- int opsLimit = Sodium.cryptoPwhashOpslimitSensitive;
- if (await isLowSpecDevice()) {
- logger.info("low spec device detected");
- // When sensitive memLimit (1 GB) is used, on low spec device the OS might
- // kill the app with OOM. To avoid that, start with 256 MB and
- // corresponding ops limit (16).
- // This ensures that the product of these two variables
- // (the area under the graph that determines the amount of work required)
- // stays the same
- // SODIUM_CRYPTO_PWHASH_MEMLIMIT_SENSITIVE: 1073741824
- // SODIUM_CRYPTO_PWHASH_MEMLIMIT_MODERATE: 268435456
- // SODIUM_CRYPTO_PWHASH_OPSLIMIT_SENSITIVE: 4
- memLimit = Sodium.cryptoPwhashMemlimitModerate;
- final factor = Sodium.cryptoPwhashMemlimitSensitive ~/
- Sodium.cryptoPwhashMemlimitModerate; // = 4
- opsLimit = opsLimit * factor; // = 16
- }
- Uint8List key;
- while (memLimit >= Sodium.cryptoPwhashMemlimitMin &&
- opsLimit <= Sodium.cryptoPwhashOpslimitMax) {
- try {
- key = await deriveKey(password, salt, memLimit, opsLimit);
- return DerivedKeyResult(key, memLimit, opsLimit);
- } catch (e, s) {
- logger.warning(
- "failed to deriveKey mem: $memLimit, ops: $opsLimit",
- e,
- s,
- );
- }
- memLimit = (memLimit / 2).round();
- opsLimit = opsLimit * 2;
- }
- throw UnsupportedError("Cannot perform this operation on this device");
- }
- // Derives a key for the given password and salt, using Argon2id, v1.3
- // with memory and ops limit hardcoded to their Interactive variants
- // NOTE: This is only used while setting passwords for shared links, as an
- // extra layer of authentication (atop the access token and collection key).
- // More details @ https://ente.io/blog/building-shareable-links/
- static Future<DerivedKeyResult> deriveInteractiveKey(
- Uint8List password,
- Uint8List salt,
- ) async {
- final int memLimit = Sodium.cryptoPwhashMemlimitInteractive;
- final int opsLimit = Sodium.cryptoPwhashOpslimitInteractive;
- final key = await deriveKey(password, salt, memLimit, opsLimit);
- return DerivedKeyResult(key, memLimit, opsLimit);
- }
- // Derives a key for a given password, salt, memLimit and opsLimit using
- // Argon2id, v1.3.
- static Future<Uint8List> deriveKey(
- Uint8List password,
- Uint8List salt,
- int memLimit,
- int opsLimit,
- ) {
- try {
- return _computer.compute(
- cryptoPwHash,
- param: {
- "password": password,
- "salt": salt,
- "memLimit": memLimit,
- "opsLimit": opsLimit,
- },
- taskName: "deriveKey",
- );
- } catch (e, s) {
- final String errMessage = 'failed to deriveKey memLimit: $memLimit and '
- 'opsLimit: $opsLimit';
- Logger("CryptoUtilDeriveKey").warning(errMessage, e, s);
- throw KeyDerivationError();
- }
- }
- // derives a Login key as subKey from the given key by applying KDF
- // (Key Derivation Function) with the `loginSubKeyId` and
- // `loginSubKeyLen` and `loginSubKeyContext` as context
- static Future<Uint8List> deriveLoginKey(
- Uint8List key,
- ) async {
- try {
- final Uint8List derivedKey = await _computer.compute(
- cryptoKdfDeriveFromKey,
- param: {
- "key": key,
- "subkeyId": loginSubKeyId,
- "subkeyLen": loginSubKeyLen,
- "context": utf8.encode(loginSubKeyContext),
- },
- taskName: "deriveLoginKey",
- );
- // return the first 16 bytes of the derived key
- return derivedKey.sublist(0, 16);
- } catch (e, s) {
- Logger("deriveLoginKey").severe("loginKeyDerivation failed", e, s);
- throw LoginKeyDerivationError();
- }
- }
- // Computes and returns the hash of the source file
- static Future<Uint8List> getHash(File source) {
- return _computer.compute(
- cryptoGenericHash,
- param: {
- "sourceFilePath": source.path,
- },
- taskName: "fileHash",
- );
- }
- }
|