2023-05-29 15:24:57 -04:00
|
|
|
import 'dart:async';
|
2023-08-02 21:09:47 -04:00
|
|
|
import 'dart:convert';
|
2023-05-29 15:24:57 -04:00
|
|
|
import 'dart:typed_data';
|
|
|
|
|
|
|
|
import 'package:charcode/charcode.dart';
|
2023-07-05 18:48:06 -04:00
|
|
|
import 'package:equatable/equatable.dart';
|
|
|
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
2023-05-29 15:24:57 -04:00
|
|
|
|
|
|
|
import 'veilid.dart';
|
|
|
|
|
|
|
|
//////////////////////////////////////
|
|
|
|
/// CryptoKind
|
|
|
|
|
|
|
|
typedef CryptoKind = int;
|
|
|
|
const CryptoKind cryptoKindVLD0 =
|
2023-08-05 13:50:16 -04:00
|
|
|
$V << 24 | $L << 16 | $D << 8 | $0 << 0; // "VLD0"
|
2023-05-29 15:24:57 -04:00
|
|
|
const CryptoKind cryptoKindNONE =
|
2023-08-05 13:50:16 -04:00
|
|
|
$N << 24 | $O << 16 | $N << 8 | $E << 0; // "NONE"
|
2023-05-29 15:24:57 -04:00
|
|
|
|
2023-07-30 15:57:06 -04:00
|
|
|
String cryptoKindToString(CryptoKind kind) =>
|
|
|
|
cryptoKindToBytes(kind).map(String.fromCharCode).join();
|
2023-05-29 15:24:57 -04:00
|
|
|
|
2023-07-21 14:30:10 -04:00
|
|
|
const CryptoKind bestCryptoKind = cryptoKindVLD0;
|
|
|
|
|
2023-07-09 10:55:43 -04:00
|
|
|
Uint8List cryptoKindToBytes(CryptoKind kind) {
|
2023-07-26 14:20:17 -04:00
|
|
|
final b = Uint8List(4);
|
|
|
|
ByteData.sublistView(b).setUint32(0, kind);
|
2023-07-09 10:55:43 -04:00
|
|
|
return b;
|
|
|
|
}
|
|
|
|
|
2024-05-15 21:20:39 -04:00
|
|
|
CryptoKind cryptoKindFromBytes(Uint8List b) =>
|
|
|
|
ByteData.sublistView(b).getUint32(0);
|
|
|
|
|
2023-05-29 15:24:57 -04:00
|
|
|
CryptoKind cryptoKindFromString(String s) {
|
|
|
|
if (s.codeUnits.length != 4) {
|
2023-07-26 14:20:17 -04:00
|
|
|
throw const FormatException('malformed string');
|
2023-05-29 15:24:57 -04:00
|
|
|
}
|
2023-07-30 15:57:06 -04:00
|
|
|
final kind =
|
|
|
|
ByteData.sublistView(Uint8List.fromList(s.codeUnits)).getUint32(0);
|
2023-05-29 15:24:57 -04:00
|
|
|
return kind;
|
|
|
|
}
|
|
|
|
|
|
|
|
//////////////////////////////////////
|
|
|
|
/// Types
|
|
|
|
|
2023-07-05 18:48:06 -04:00
|
|
|
@immutable
|
|
|
|
class Typed<V extends EncodedString> extends Equatable {
|
|
|
|
const Typed({required this.kind, required this.value});
|
2023-05-29 15:24:57 -04:00
|
|
|
|
2023-07-05 18:48:06 -04:00
|
|
|
factory Typed.fromString(String s) {
|
2023-07-26 14:20:17 -04:00
|
|
|
final parts = s.split(':');
|
2023-05-29 15:24:57 -04:00
|
|
|
if (parts.length < 2 || parts[0].codeUnits.length != 4) {
|
2023-07-26 14:20:17 -04:00
|
|
|
throw const FormatException('malformed string');
|
2023-05-29 15:24:57 -04:00
|
|
|
}
|
2023-07-05 18:48:06 -04:00
|
|
|
final kind = cryptoKindFromString(parts[0]);
|
2023-07-26 14:20:17 -04:00
|
|
|
final value = EncodedString.fromString<V>(parts.sublist(1).join(':'));
|
2023-07-05 18:48:06 -04:00
|
|
|
return Typed(kind: kind, value: value);
|
2023-05-29 15:24:57 -04:00
|
|
|
}
|
2024-05-15 21:20:39 -04:00
|
|
|
factory Typed.fromBytes(Uint8List b) {
|
|
|
|
final kind = cryptoKindFromBytes(b);
|
|
|
|
final value = EncodedString.fromBytes<V>(b.sublist(4));
|
|
|
|
return Typed(kind: kind, value: value);
|
|
|
|
}
|
2023-07-26 14:20:17 -04:00
|
|
|
factory Typed.fromJson(dynamic json) => Typed.fromString(json as String);
|
|
|
|
final CryptoKind kind;
|
|
|
|
final V value;
|
|
|
|
@override
|
|
|
|
List<Object> get props => [kind, value];
|
|
|
|
|
|
|
|
@override
|
|
|
|
String toString() => '${cryptoKindToString(kind)}:$value';
|
2023-05-29 15:24:57 -04:00
|
|
|
|
2023-07-09 10:55:43 -04:00
|
|
|
Uint8List decode() {
|
2023-07-30 15:57:06 -04:00
|
|
|
final b = BytesBuilder()
|
|
|
|
..add(cryptoKindToBytes(kind))
|
|
|
|
..add(value.decode());
|
2023-07-25 01:04:22 -04:00
|
|
|
return b.toBytes();
|
2023-07-09 10:55:43 -04:00
|
|
|
}
|
|
|
|
|
2024-05-15 21:20:39 -04:00
|
|
|
static int encodedLength<X>() {
|
|
|
|
switch (X) {
|
|
|
|
case const (Typed<FixedEncodedString32>):
|
|
|
|
return FixedEncodedString32.encodedLength() + 5;
|
|
|
|
case const (Typed<FixedEncodedString43>):
|
|
|
|
return FixedEncodedString43.encodedLength() + 5;
|
|
|
|
case const (Typed<FixedEncodedString86>):
|
|
|
|
return FixedEncodedString86.encodedLength() + 5;
|
|
|
|
default:
|
|
|
|
throw UnimplementedError();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static int decodedLength<X>() {
|
|
|
|
switch (X) {
|
|
|
|
case const (Typed<FixedEncodedString32>):
|
|
|
|
return FixedEncodedString32.decodedLength() + 4;
|
|
|
|
case const (Typed<FixedEncodedString43>):
|
|
|
|
return FixedEncodedString43.decodedLength() + 4;
|
|
|
|
case const (Typed<FixedEncodedString86>):
|
|
|
|
return FixedEncodedString86.decodedLength() + 4;
|
|
|
|
default:
|
|
|
|
throw UnimplementedError();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-05 18:48:06 -04:00
|
|
|
String toJson() => toString();
|
2023-05-29 15:24:57 -04:00
|
|
|
}
|
|
|
|
|
2023-07-05 18:48:06 -04:00
|
|
|
@immutable
|
|
|
|
class KeyPair extends Equatable {
|
|
|
|
const KeyPair({required this.key, required this.secret});
|
2023-05-29 15:24:57 -04:00
|
|
|
|
2023-07-05 18:48:06 -04:00
|
|
|
factory KeyPair.fromString(String s) {
|
2023-07-26 14:20:17 -04:00
|
|
|
final parts = s.split(':');
|
2023-05-29 15:24:57 -04:00
|
|
|
if (parts.length != 2 ||
|
|
|
|
parts[0].codeUnits.length != 43 ||
|
|
|
|
parts[1].codeUnits.length != 43) {
|
2023-07-26 14:20:17 -04:00
|
|
|
throw const FormatException('malformed string');
|
2023-05-29 15:24:57 -04:00
|
|
|
}
|
2023-07-05 18:48:06 -04:00
|
|
|
final key = PublicKey.fromString(parts[0]);
|
|
|
|
final secret = PublicKey.fromString(parts[1]);
|
|
|
|
return KeyPair(key: key, secret: secret);
|
2023-05-29 15:24:57 -04:00
|
|
|
}
|
2023-07-26 14:20:17 -04:00
|
|
|
factory KeyPair.fromJson(dynamic json) => KeyPair.fromString(json as String);
|
|
|
|
final PublicKey key;
|
|
|
|
final PublicKey secret;
|
|
|
|
@override
|
|
|
|
List<Object> get props => [key, secret];
|
|
|
|
|
|
|
|
@override
|
|
|
|
String toString() => '$key:$secret';
|
2023-05-29 15:24:57 -04:00
|
|
|
|
2023-07-05 18:48:06 -04:00
|
|
|
String toJson() => toString();
|
2023-05-29 15:24:57 -04:00
|
|
|
}
|
|
|
|
|
2023-07-05 18:48:06 -04:00
|
|
|
@immutable
|
|
|
|
class TypedKeyPair extends Equatable {
|
|
|
|
const TypedKeyPair(
|
|
|
|
{required this.kind, required this.key, required this.secret});
|
2023-05-29 15:24:57 -04:00
|
|
|
|
2023-07-05 18:48:06 -04:00
|
|
|
factory TypedKeyPair.fromString(String s) {
|
2023-07-26 14:20:17 -04:00
|
|
|
final parts = s.split(':');
|
2023-05-29 15:24:57 -04:00
|
|
|
if (parts.length != 3 ||
|
|
|
|
parts[0].codeUnits.length != 4 ||
|
|
|
|
parts[1].codeUnits.length != 43 ||
|
|
|
|
parts[2].codeUnits.length != 43) {
|
2023-07-26 14:20:17 -04:00
|
|
|
throw VeilidAPIExceptionInvalidArgument('malformed string', 's', s);
|
2023-05-29 15:24:57 -04:00
|
|
|
}
|
2023-07-05 18:48:06 -04:00
|
|
|
final kind = cryptoKindFromString(parts[0]);
|
|
|
|
final key = PublicKey.fromString(parts[1]);
|
|
|
|
final secret = PublicKey.fromString(parts[2]);
|
|
|
|
return TypedKeyPair(kind: kind, key: key, secret: secret);
|
2023-05-29 15:24:57 -04:00
|
|
|
}
|
2023-07-05 18:48:06 -04:00
|
|
|
factory TypedKeyPair.fromJson(dynamic json) =>
|
|
|
|
TypedKeyPair.fromString(json as String);
|
2023-07-21 14:30:10 -04:00
|
|
|
factory TypedKeyPair.fromKeyPair(CryptoKind kind, KeyPair keyPair) =>
|
|
|
|
TypedKeyPair(kind: kind, key: keyPair.key, secret: keyPair.secret);
|
2023-07-26 14:20:17 -04:00
|
|
|
final CryptoKind kind;
|
|
|
|
final PublicKey key;
|
|
|
|
final PublicKey secret;
|
|
|
|
@override
|
|
|
|
List<Object> get props => [kind, key, secret];
|
|
|
|
|
|
|
|
@override
|
2023-07-30 15:57:06 -04:00
|
|
|
String toString() => '${cryptoKindToString(kind)}:$key:$secret';
|
2023-07-26 14:20:17 -04:00
|
|
|
|
|
|
|
String toJson() => toString();
|
2024-05-09 10:49:02 -04:00
|
|
|
KeyPair toKeyPair() => KeyPair(key: key, secret: secret);
|
2023-05-29 15:24:57 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
typedef CryptoKey = FixedEncodedString43;
|
|
|
|
typedef Signature = FixedEncodedString86;
|
|
|
|
typedef Nonce = FixedEncodedString32;
|
|
|
|
|
|
|
|
typedef PublicKey = CryptoKey;
|
|
|
|
typedef SecretKey = CryptoKey;
|
|
|
|
typedef HashDigest = CryptoKey;
|
|
|
|
typedef SharedSecret = CryptoKey;
|
|
|
|
typedef CryptoKeyDistance = CryptoKey;
|
|
|
|
|
|
|
|
typedef TypedKey = Typed<CryptoKey>;
|
2023-07-02 11:31:53 -04:00
|
|
|
typedef TypedSecret = Typed<SecretKey>;
|
2023-07-07 19:32:59 -04:00
|
|
|
typedef TypedHashDigest = Typed<HashDigest>;
|
2023-07-02 11:31:53 -04:00
|
|
|
|
2023-05-29 15:24:57 -04:00
|
|
|
typedef TypedSignature = Typed<Signature>;
|
|
|
|
|
|
|
|
//////////////////////////////////////
|
|
|
|
/// VeilidCryptoSystem
|
|
|
|
|
|
|
|
abstract class VeilidCryptoSystem {
|
|
|
|
CryptoKind kind();
|
|
|
|
Future<SharedSecret> cachedDH(PublicKey key, SecretKey secret);
|
|
|
|
Future<SharedSecret> computeDH(PublicKey key, SecretKey secret);
|
2024-03-18 10:10:10 -04:00
|
|
|
Future<SharedSecret> generateSharedSecret(
|
|
|
|
PublicKey key, SecretKey secret, Uint8List domain);
|
2023-05-29 15:24:57 -04:00
|
|
|
Future<Uint8List> randomBytes(int len);
|
|
|
|
Future<int> defaultSaltLength();
|
|
|
|
Future<String> hashPassword(Uint8List password, Uint8List salt);
|
|
|
|
Future<bool> verifyPassword(Uint8List password, String passwordHash);
|
|
|
|
Future<SharedSecret> deriveSharedSecret(Uint8List password, Uint8List salt);
|
|
|
|
Future<Nonce> randomNonce();
|
|
|
|
Future<SharedSecret> randomSharedSecret();
|
|
|
|
Future<KeyPair> generateKeyPair();
|
|
|
|
Future<HashDigest> generateHash(Uint8List data);
|
|
|
|
//Future<HashDigest> generateHashReader(Stream<List<int>> reader);
|
|
|
|
Future<bool> validateKeyPair(PublicKey key, SecretKey secret);
|
2023-07-30 15:57:06 -04:00
|
|
|
Future<bool> validateKeyPairWithKeyPair(KeyPair keyPair) =>
|
|
|
|
validateKeyPair(keyPair.key, keyPair.secret);
|
2023-07-19 10:07:51 -04:00
|
|
|
|
2023-05-29 15:24:57 -04:00
|
|
|
Future<bool> validateHash(Uint8List data, HashDigest hash);
|
|
|
|
//Future<bool> validateHashReader(Stream<List<int>> reader, HashDigest hash);
|
|
|
|
Future<CryptoKeyDistance> distance(CryptoKey key1, CryptoKey key2);
|
|
|
|
Future<Signature> sign(PublicKey key, SecretKey secret, Uint8List data);
|
2023-07-30 15:57:06 -04:00
|
|
|
Future<Signature> signWithKeyPair(KeyPair keyPair, Uint8List data) =>
|
|
|
|
sign(keyPair.key, keyPair.secret, data);
|
2023-07-19 10:07:51 -04:00
|
|
|
|
2024-05-31 16:20:58 -04:00
|
|
|
Future<bool> verify(PublicKey key, Uint8List data, Signature signature);
|
2023-05-29 15:24:57 -04:00
|
|
|
Future<int> aeadOverhead();
|
|
|
|
Future<Uint8List> decryptAead(Uint8List body, Nonce nonce,
|
|
|
|
SharedSecret sharedSecret, Uint8List? associatedData);
|
|
|
|
Future<Uint8List> encryptAead(Uint8List body, Nonce nonce,
|
|
|
|
SharedSecret sharedSecret, Uint8List? associatedData);
|
|
|
|
Future<Uint8List> cryptNoAuth(
|
|
|
|
Uint8List body, Nonce nonce, SharedSecret sharedSecret);
|
2023-08-02 21:09:47 -04:00
|
|
|
|
2023-09-24 22:36:15 -04:00
|
|
|
Future<Uint8List> encryptAeadWithNonce(
|
|
|
|
Uint8List body, SharedSecret secret) async {
|
|
|
|
// generate nonce
|
|
|
|
final nonce = await randomNonce();
|
|
|
|
// crypt and append nonce
|
|
|
|
final b = BytesBuilder()
|
|
|
|
..add(await encryptAead(body, nonce, secret, null))
|
|
|
|
..add(nonce.decode());
|
|
|
|
return b.toBytes();
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<Uint8List> decryptAeadWithNonce(
|
|
|
|
Uint8List body, SharedSecret secret) async {
|
|
|
|
if (body.length < Nonce.decodedLength()) {
|
|
|
|
throw const FormatException('not enough data to decrypt');
|
|
|
|
}
|
|
|
|
final nonce =
|
|
|
|
Nonce.fromBytes(body.sublist(body.length - Nonce.decodedLength()));
|
|
|
|
final encryptedData = body.sublist(0, body.length - Nonce.decodedLength());
|
|
|
|
// decrypt
|
|
|
|
return decryptAead(encryptedData, nonce, secret, null);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<Uint8List> encryptAeadWithPassword(
|
|
|
|
Uint8List body, String password) async {
|
|
|
|
final ekbytes = Uint8List.fromList(utf8.encode(password));
|
|
|
|
final nonce = await randomNonce();
|
|
|
|
final saltBytes = nonce.decode();
|
|
|
|
final sharedSecret = await deriveSharedSecret(ekbytes, saltBytes);
|
|
|
|
return Uint8List.fromList(
|
|
|
|
(await encryptAead(body, nonce, sharedSecret, null)) + saltBytes);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<Uint8List> decryptAeadWithPassword(
|
|
|
|
Uint8List body, String password) async {
|
|
|
|
if (body.length < Nonce.decodedLength()) {
|
|
|
|
throw const FormatException('not enough data to decrypt');
|
|
|
|
}
|
|
|
|
final ekbytes = Uint8List.fromList(utf8.encode(password));
|
|
|
|
final bodyBytes = body.sublist(0, body.length - Nonce.decodedLength());
|
|
|
|
final saltBytes = body.sublist(body.length - Nonce.decodedLength());
|
|
|
|
final nonce = Nonce.fromBytes(saltBytes);
|
|
|
|
final sharedSecret = await deriveSharedSecret(ekbytes, saltBytes);
|
|
|
|
return decryptAead(bodyBytes, nonce, sharedSecret, null);
|
|
|
|
}
|
|
|
|
|
2023-08-02 21:09:47 -04:00
|
|
|
Future<Uint8List> encryptNoAuthWithNonce(
|
|
|
|
Uint8List body, SharedSecret secret) async {
|
|
|
|
// generate nonce
|
|
|
|
final nonce = await randomNonce();
|
|
|
|
// crypt and append nonce
|
|
|
|
final b = BytesBuilder()
|
|
|
|
..add(await cryptNoAuth(body, nonce, secret))
|
|
|
|
..add(nonce.decode());
|
|
|
|
return b.toBytes();
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<Uint8List> decryptNoAuthWithNonce(
|
|
|
|
Uint8List body, SharedSecret secret) async {
|
2023-08-04 01:00:23 -04:00
|
|
|
if (body.length < Nonce.decodedLength()) {
|
2023-08-02 21:09:47 -04:00
|
|
|
throw const FormatException('not enough data to decrypt');
|
|
|
|
}
|
|
|
|
final nonce =
|
|
|
|
Nonce.fromBytes(body.sublist(body.length - Nonce.decodedLength()));
|
|
|
|
final encryptedData = body.sublist(0, body.length - Nonce.decodedLength());
|
|
|
|
// decrypt
|
|
|
|
return cryptNoAuth(encryptedData, nonce, secret);
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<Uint8List> encryptNoAuthWithPassword(
|
|
|
|
Uint8List body, String password) async {
|
|
|
|
final ekbytes = Uint8List.fromList(utf8.encode(password));
|
|
|
|
final nonce = await randomNonce();
|
|
|
|
final saltBytes = nonce.decode();
|
|
|
|
final sharedSecret = await deriveSharedSecret(ekbytes, saltBytes);
|
2023-09-24 15:59:47 -04:00
|
|
|
return Uint8List.fromList(
|
|
|
|
(await cryptNoAuth(body, nonce, sharedSecret)) + saltBytes);
|
2023-08-02 21:09:47 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
Future<Uint8List> decryptNoAuthWithPassword(
|
|
|
|
Uint8List body, String password) async {
|
2023-08-04 01:00:23 -04:00
|
|
|
if (body.length < Nonce.decodedLength()) {
|
2023-08-02 21:09:47 -04:00
|
|
|
throw const FormatException('not enough data to decrypt');
|
|
|
|
}
|
|
|
|
final ekbytes = Uint8List.fromList(utf8.encode(password));
|
|
|
|
final bodyBytes = body.sublist(0, body.length - Nonce.decodedLength());
|
|
|
|
final saltBytes = body.sublist(body.length - Nonce.decodedLength());
|
|
|
|
final nonce = Nonce.fromBytes(saltBytes);
|
|
|
|
final sharedSecret = await deriveSharedSecret(ekbytes, saltBytes);
|
|
|
|
return cryptNoAuth(bodyBytes, nonce, sharedSecret);
|
|
|
|
}
|
2023-05-29 15:24:57 -04:00
|
|
|
}
|