sodium 3.4.3 sodium: ^3.4.3 copied to clipboard
Dart bindings for libsodium, for the Dart-VM and for the Web
sodium #
Dart bindings for libsodium, supporting both the VM and JS without flutter dependencies.
Table of contents #
Table of contents generated with markdown-toc
Features #
- Provides a simple to use dart API for accessing libsodium - High-Level API that is the same for both VM and JS - Aims to provide access to all primary libsodium APIs. See API Status for more details.
- Provides native APIs for tighter integration, if necessary
API Status #
The following table shows the current status of the implementation. APIs that have already been ported get the ✔️, those that are planned but not there yet have 🚧. If you see an ❌, it means that the API is not available on that platform and thus cannot be implemented. The Sumo column specifies whether the API is available as sumo extension. Those listed with a ✔️ are available only in the sumo API, while those marked with ➕ have extended sumo APIs. APIs that are not listet yet have either been forgotten or are not planned. If you find one you would like to have made available, please create an issue for it, and I will add it to the list.
API based on libsodium version: 1.0.20
Note: Memory Management in JS is limited to overwriting the memory with 0. All other Memory-APIs are only available in the VM.
Considered for the future
The following APIs I considered adding, but since they all appear below the "Advanced" Tab in the documentation, I decided against it for version 1.0.0. However, with version 2.0.0, support for advanced APIs has been enabled via sumo. This means all of those have now become feasible to implement and might be added in the future. If you need one of these or some other advanced API, please create an issue.
libsodium API | VM | JS | Documentation |
---|---|---|---|
crypto_onetimeauth | ❔ | ❔ | https://libsodium.gitbook.io/doc/advanced/poly1305 |
crypto_hash_sha | ❔ | ❔ | https://libsodium.gitbook.io/doc/advanced/sha-2_hash_function |
crypto_auth_hmacsha | ❔ | ❔ | https://libsodium.gitbook.io/doc/advanced/hmac-sha2 |
Installation #
Simply add sodium
to your pubspec.yaml
and run pub get
(or flutter pub get
).
Usage #
The usage can be split into two parts. The first one is about loading the native libsodium into dart, the second one about using the API.
Sodium vs SodiumSumo #
As this library aims to support both native and JavaScript targets, it needs to unify both APIs under one single dart API. This comes with one major consideration: The separation between a "normal" and a "sumo" variant of the library. These terms are absent in the C-library, but have been introduced in the JS-Variant (See https://github.com/jedisct1/libsodium.js/?tab=readme-ov-file#standard-vs-sumo-version).
In order support both library variants in this library, the APIs have been split here as well. However, this only affects the JS part of the code, as for the native implementation there is no differentiation between the two. What this means for you as a consumer of the library is the following:
- If you only ever intend to use the native variante (i. e. your application will not be transpiled to JS), you can simply always use the sumo variant.
- If you want to support bith, native and web, you have to check wich of the APIs you need are available in which version. The Sumo-Variant is more complete, but has a bigger binary size, which might matter depending on the usecase.
Loading libsodium #
How you load the library depends on whether you are running in the dart VM or as transpiled JS code.
Note: For flutter users, you should use the sodium_libs package, as it
provides embedded (or compile time added) binaries for every flutter platform. This way you can simply use the library
without thinking about this part. You can check the documentation of sodium_libs
to add it to your project and then
continue at Using the API.
Note: When using the sumo APIs, simply replace SodiumInit
with SodiumSumoInit
from the
package:sodium/sodium_sumo.dart
import.
VM - loading the dynamic library
In the dart VM, dart:ffi
is used as backend to load and interact with the libsodium binary. So, all you need to do is
load such a library and then pass it to the sodium APIs. This generally looks like this:
// required imports
import 'dart:ffi';
import 'package:sodium/sodium.dart';
// define a loader method that loads the dynamic library into dart
DynamicLibrary loadLibsodium() {
return DynamicLibrary.open('/path/to/libsodium.XXX'); // or DynamicLibrary.process()
}
// initialize the sodium APIs
final sodium = await SodiumInit.init2(loadLibsodium);
The tricky part here is the path, aka '/path/to/libsodium.XXX'
. It depends on the platform and how you intend to use
the library. My recommendation is to follow https://libsodium.gitbook.io/doc/installation to get the library binary for
your platform and then pass the correct path. If you are linking statically, you can use DynamicLibrary.process()
(except on windows) instead of the path.
However, here are some tips on how to get the library for some platforms and how to load it there. For flutter users, you can simply add sodium_libs to your project, which takes care of this for you.
- Linux: Install
libsodium
via your system package manager. Then, you can load thelibsodium.so
from where the package manager put it. - Windows: Download the correct binary from https://download.libsodium.org/libsodium/releases/ and simply use the path where you placed the library.
- macOS: Use homebrew and run
brew install libsodium
- then locate the binary in the Cellar. It is typically something like/usr/local/Cellar/libsodium/<version>/lib/libsodium.dylib
. - Android: Clone the official sources and run the correct build script located at
https://github.com/jedisct1/libsodium/tree/master/dist-build. The build will produce an
.so
file you can add to yourmain/src/jniLibs
folder. - iOS: Simply add the swift-sodium package to your project. It will
statically link your app with the library. You can use
DynamicLibrary.process()
to access the symbols.
Transpiled JavaScript - loading the JavaScript code.
The correct setup depends on your JavaScript environment (i.e. browser, nodejs, ...) - however, the general way is the same:
// required imports
import 'package:sodium/sodium.dart';
// define a loader method that loads the javascript object into dart
dynamic loadSodiumJS() {
return ...; // somehow load the sodium.js into dart
}
// initialize the sodium APIs
final sodium = await SodiumInit.init2(loadSodiumJS);
The complex part is how to load the library into dart. Generally, you can refer to https://github.com/jedisct1/libsodium.js/#installation on how to load the library into your JS environment. However, since we are running JavaScript code, the setup is a little more complex. For flutter users, you can simply add sodium_libs to your project, which takes care of this for you.
The only platform I have tried so far is the browser. However, similar approaches should work for all JS environments that you can run transpiled dart code in.
Loading sodium.js into the browser via dart.
The idea here is, that the dart code asynchronously loads the sodium.js
into the browser and then acquires the result
of loading it (As recommended in https://github.com/jedisct1/libsodium.js/#usage-in-a-web-browser-via-a-callback). The
following code uses the package:js
to interop with JavaScript and perform these steps.
You can download the sodium.js
file from here: https://github.com/jedisct1/libsodium.js/tree/master/dist/browsers
// make the dart library JS-interoperable
@JS()
library interop;
// required imports
import 'package:js/js.dart';
import 'package:sodium/sodium.dart';
// declare a JavaScript type that will provide the callback for the loaded
// sodium JavaScript object.
@JS()
@anonymous
class SodiumBrowserInit {
external void Function(dynamic sodium) get onload;
external factory SodiumBrowserInit({void Function(dynamic sodium) onload});
}
Future<dynamic> loadSodiumInBrowser() async {
// create a completer that will wait for the library to be loaded
final completer = Completer<dynamic>();
// Set the global `sodium` property to our JS type, with the callback being
// redirected to the completer
setProperty(window, 'sodium', SodiumBrowserInit(
onload: allowInterop(completer.complete),
));
// Load the sodium.js into the page by appending a `<script>` element
final script = ScriptElement();
script
..type = 'text/javascript'
..async = true
..src = 'sodium.js'; // use the path where you put the file on your server
document.head!.append(script);
// return the completer
return completer.future;
}
// in your main:
final sodium = await SodiumInit.init2(loadSodiumInBrowser);
Using the API #
Once you have acquired the Sodium
instance, usage is fairly straight forward. The API mirrors the original native C
api, splitting different categories of methods into different classes for maintainability, which are all built up in
hierarchical order starting at Sodium
. For example, if you wanted to use the crypto_secretbox_easy
method from the C
api, the equivalent dart code would be:
final sodium = // load libsodium for your platform
// The message to be encrypted, converted to an unsigned char array.
final String message = 'my very secret message';
final Int8List messageChars = message.toCharArray();
final Uint8List messageBytes = messageChars.unsignedView();
// A randomly generated nonce
final nonce = sodium.randombytes.buf(
sodium.crypto.secretBox.nonceBytes,
);
// Generate a secret key
final SecureKey key = sodium.crypto.secretBox.keygen();
// Encrypt the data
final encryptedData = sodium.crypto.secretBox.easy(
message: messageBytes,
nonce: nonce,
key: key,
)
print(encryptedData);
// after you are done:
key.dispose();
The only main differences here are, that instead of raw pointers, the dart typed lists are used. Also, instead of simply
passing a byte array as the key, the SecureKey
is used. It is a special class created for this library that wraps
native memory, thus providing a secure way of keeping your keys in memory. You can either create such keys via the
*_keygen
methods, or directly via sodium.secure*
.
Note: Since these keys wrap native memory, it is mandatory that you dispose of them after you are done with a key, as otherwise they will leak memory.
Running computations in a separate isolate #
Some operations with libsodium (like password hashing) can take multiple seconds to execute. In such a case, running the
computation on a separate isolate is mandatory to not block the UI. However, the standard
compute or
Isolate.run methods will not work, as it is not
possible to pass a Sodium
instance between isolates. For this reason, the library has a helper method:
Sodium.runIsolated
The usage is pretty straight forward: It is a compute callback, but any SecretKey
or KeyPair
values must be passed
as addition parameters, as they need special intervention to be transferred to the isolate. Returning however works and
allows you to simply pass back a key (or pair) if needed. A simple example that runs a key derivation would look like
this:
final subkeyId = BigInt.from(42);
final masterKey = sodium.crypto.kdf.keygen();
final derivedKey = await sodium.runIsolated(
secureKeys: [masterKey],
// keyPairs: use if a KeyPair needs to be passed to the isolate
(sodium, secureKeys, keyPairs) {
final [masterKey] = secureKeys;
final derivedKey = sodium.crypto.kdf.deriveFromKey(
masterKey: masterKey, // keys must be passed via the extra parameters
context: 'computed',
subkeyId: subkeyId, // normal values can be used as usual
subkeyLen: 64,
);
return derivedKey; // keys can be returned
}
);
Using custom isolates
While the example above works fine if you simply want to run an asynchronous computation, it might not be sufficient for more complex scenarios. For example, if you want to use an isolate pool, you would want to execute sodium methods on an existing isolate and not create a new one every time. For this, the library provides low-level isolate APIs that allow you to do exactly this.
Warning: These APIs are low-level and can lead to memory leaks and hard crashes if used incorrectly. So read the following with care!
The first is Sodium.isolateFactory
. That property returns a factory method to create new Sodium
instances for the
same native library and can easily be transferred between different isolates. The second are the create/materialize
methods. They allow you to make a secure key "transferrable". This is needed, as dart does not allow the transfer of
pointers between isolates. The Sodium.createTransferrableSecureKey
and createTransferrableKeyPair
will create a
special copy of the original key/key pair that can be transferred. You can then call
Sodium.materializeTransferrableSecureKey
or Sodium.materializeTransferrableKeyPair
on the target isolate to get a
normal key/key pair back. This is preferred over simply sending the keys as Uint8List
, as the transferrable keys will
still apply all the advanced security measures that SecureKey
uses as well.
IMPORTANT: As the transferrable variants do work around darts pointer management, they will not be automatically garbage collected if left dangling. You MUST materialize every transferrable key/key pair exactly once, or you will create memory leaks for sensitive data! If you need to send keys to multiple isolates, create one per isolate.
For other data, like public keys or plain/cipher text, you can simply send the Uint8List
, however, if performance is
relevant, you should instead use the
TransferableTypedData, as it will
reduce the number of times data has to be copied.
Here is a simple variant of the above example that uses the low level APIs instead.
Future<SecureKey> deriveKey() {
final subkeyId = BigInt.from(42);
final masterKey = sodium.crypto.kdf.keygen();
final sodiumFactory = sodium.isolateFactory;
final transferrableMasterKey = sodium.createTransferrableSecureKey(masterKey);
final result = compute(_deriveKey, (sodiumFactory, transferrableMasterKey, subkeyId));
return sodium.materializeTransferrableSecureKey(result);
}
// at the end of the file
Future<TransferrableSecureKey> _deriveKey((SodiumFactory, TransferrableSecureKey, BigInt) message) async {
final (sodiumFactory, transferrableMasterKey, subkeyId) = message;
final sodium = await sodiumFactory();
final masterKey = sodium.materializeTransferrableSecureKey(transferrableMasterKey);
final derivedKey = sodium.crypto.kdf.deriveFromKey(
masterKey: masterKey, // keys must be passed via the extra parameters
context: 'computed',
subkeyId: subkeyId, // normal values can be used as usual
subkeyLen: 64,
);
final transferrableDerivedKey = sodium.createTransferrableSecureKey(derivedKey);
return transferrableDerivedKey;
}
Documentation #
The documentation is available at https://pub.dev/documentation/sodium/latest/. A full example can be found at https://pub.dev/packages/sodium/example.
The example runs both in the VM and on the web. To use it, see below.
As preparation for all platforms, run the following steps:
cd packages/sodium
dart pub get
dart run build_runner build
Example for the dart VM #
Locate/Download the libsodium binary and run the example with it:
cd packages/sodium/example
dart pub get
dart run bin/main_native.dart '/path/to/libsodium.XXX'
Example in the browser #
First download sodium.js
into the examples web directory. Then simply run the example:
dart pub global activate webdev
cd packages/sodium/example/web
curl -Lo sodium.js https://raw.githubusercontent.com/jedisct1/libsodium.js/master/dist/browsers/sodium.js
cd ..
dart pub get
dart pub global run webdev serve --release
# Visit http://127.0.0.1:8080 in the browser