Skip to content

Commit d6e82d4

Browse files
committed
V5.1.0
- Added support for HKDF (HMAC-based Key Derivation Function) - Added support for X25519 operations, including ScalarMultBase and ScalarMult
1 parent 9b93cad commit d6e82d4

27 files changed

Lines changed: 1551 additions & 77 deletions

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 5.1.0
2+
3+
- Added support for HKDF (HMAC-based Key Derivation Function)
4+
- Added support for X25519 operations, including ScalarMultBase and ScalarMult
5+
16
## 5.0.0
27

38
- Ported the Bitcoin secp256k1 cryptographic library to Dart.

example/pubspec.lock

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ packages:
4444
path: ".."
4545
relative: true
4646
source: path
47-
version: "4.3.0"
47+
version: "5.1.0"
4848
boolean_selector:
4949
dependency: transitive
5050
description:
@@ -93,6 +93,14 @@ packages:
9393
url: "https://pub.dev"
9494
source: hosted
9595
version: "3.0.5"
96+
cryptography:
97+
dependency: transitive
98+
description:
99+
name: cryptography
100+
sha256: d146b76d33d94548cf035233fbc2f4338c1242fa119013bead807d033fc4ae05
101+
url: "https://pub.dev"
102+
source: hosted
103+
version: "2.7.0"
96104
cupertino_icons:
97105
dependency: "direct main"
98106
description:
@@ -101,6 +109,14 @@ packages:
101109
url: "https://pub.dev"
102110
source: hosted
103111
version: "1.0.8"
112+
ffi:
113+
dependency: transitive
114+
description:
115+
name: ffi
116+
sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
117+
url: "https://pub.dev"
118+
source: hosted
119+
version: "2.1.4"
104120
file:
105121
dependency: transitive
106122
description:
@@ -166,10 +182,10 @@ packages:
166182
dependency: transitive
167183
description:
168184
name: js
169-
sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf
185+
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
170186
url: "https://pub.dev"
171187
source: hosted
172-
version: "0.7.1"
188+
version: "0.6.7"
173189
lints:
174190
dependency: transitive
175191
description:
@@ -456,4 +472,4 @@ packages:
456472
source: hosted
457473
version: "3.1.2"
458474
sdks:
459-
dart: ">=3.7.0-0 <4.0.0"
475+
dart: ">=3.7.0 <4.0.0"

lib/base64/base64.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export 'converter/decoding.dart';
2+
export 'converter/encoding.dart';
3+
export 'exception/exception.dart';

lib/base64/converter/decoding.dart

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import 'package:blockchain_utils/base64/exception/exception.dart';
2+
import 'package:blockchain_utils/blockchain_utils.dart';
3+
4+
class _Base64StreamDecoder {
5+
static const _base64Table =
6+
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
7+
static final _base64DecodeTable = () {
8+
final table = List<int>.filled(256, -1);
9+
for (int i = 0; i < _base64Table.length; i++) {
10+
table[_base64Table.codeUnitAt(i)] = i;
11+
}
12+
return table.immutable;
13+
}();
14+
15+
static List<int> _decode(String encoded) {
16+
final cleaned = encoded.replaceAll('=', '');
17+
final output = <int>[];
18+
int i = 0;
19+
20+
while (i + 4 <= cleaned.length) {
21+
int chunk = (_base64DecodeTable[cleaned.codeUnitAt(i)] << 18) |
22+
(_base64DecodeTable[cleaned.codeUnitAt(i + 1)] << 12) |
23+
(_base64DecodeTable[cleaned.codeUnitAt(i + 2)] << 6) |
24+
(_base64DecodeTable[cleaned.codeUnitAt(i + 3)]);
25+
output.add((chunk >> 16) & 0xFF);
26+
output.add((chunk >> 8) & 0xFF);
27+
output.add(chunk & 0xFF);
28+
i += 4;
29+
}
30+
31+
int rem = cleaned.length - i;
32+
if (rem == 2) {
33+
int chunk = (_base64DecodeTable[cleaned.codeUnitAt(i)] << 18) |
34+
(_base64DecodeTable[cleaned.codeUnitAt(i + 1)] << 12);
35+
output.add((chunk >> 16) & 0xFF);
36+
} else if (rem == 3) {
37+
int chunk = (_base64DecodeTable[cleaned.codeUnitAt(i)] << 18) |
38+
(_base64DecodeTable[cleaned.codeUnitAt(i + 1)] << 12) |
39+
(_base64DecodeTable[cleaned.codeUnitAt(i + 2)] << 6);
40+
output.add((chunk >> 16) & 0xFF);
41+
output.add((chunk >> 8) & 0xFF);
42+
}
43+
44+
return output;
45+
}
46+
47+
final List<int> _output = [];
48+
String _carry = '';
49+
50+
/// Adds Base64 string data to the decoder, buffering as needed.
51+
/// Strips out newline and carriage return characters.
52+
void add(String input) {
53+
_carry += input.replaceAll('\n', '').replaceAll('\r', '');
54+
while (_carry.length >= 4) {
55+
final chunk = _carry.substring(0, 4);
56+
_output.addAll(_decode(chunk));
57+
_carry = _carry.substring(4);
58+
}
59+
}
60+
61+
/// Finalizes decoding by processing any remaining buffered data.
62+
/// Returns the full decoded byte list.
63+
List<int> finalize() {
64+
if (_carry.isNotEmpty) {
65+
_output.addAll(_decode(_carry.padRight(4, '=')));
66+
}
67+
return _output;
68+
}
69+
70+
void clean() {
71+
_output.clear();
72+
_carry = '';
73+
}
74+
}
75+
76+
/// A utility class for decoding Base64-encoded strings with options
77+
/// for URL-safe variant handling and padding validation.
78+
class B64Decoder {
79+
/// Decodes a Base64 [data] string into bytes.
80+
///
81+
/// [validatePadding]: If true (default), requires input length to be a multiple of 4.
82+
/// If false, padding '=' is added automatically to fix length.
83+
///
84+
/// [urlSafe]: If true (default), treats input as URL-safe Base64, converting
85+
/// '-' to '+' and '_' to '/' before decoding. If false, throws if URL-safe
86+
/// characters are present.
87+
///
88+
/// Throws [B64ConverterException] on invalid input or length.
89+
static List<int> decode(String data,
90+
{bool validatePadding = true, bool urlSafe = true}) {
91+
if (validatePadding && data.length % 4 != 0) {
92+
throw B64ConverterException("Invalid length, must be multiple of four");
93+
} else if (!validatePadding) {
94+
while (data.length % 4 != 0) {
95+
data += '=';
96+
}
97+
}
98+
if (urlSafe) {
99+
data = data.replaceAll('-', '+').replaceAll('_', '/');
100+
} else if (data.contains('-') || data.contains('_')) {
101+
throw B64ConverterException(
102+
'Invalid character in standard Base64 string: found URL-safe characters "-" or "_" but urlSafe is false.');
103+
}
104+
final encoder = _Base64StreamDecoder();
105+
try {
106+
encoder.add(data);
107+
return encoder.finalize().clone();
108+
} finally {
109+
encoder.clean();
110+
}
111+
}
112+
113+
/// Tries to decode a Base64 string, returning null if decoding fails.
114+
///
115+
/// Same parameters as [decode], but catches exceptions and returns null on error.
116+
static List<int>? tryDecode(String data,
117+
{bool validatePadding = true, bool urlSafe = true}) {
118+
try {
119+
return decode(data, urlSafe: urlSafe, validatePadding: validatePadding);
120+
} catch (_) {
121+
return null;
122+
}
123+
}
124+
}

lib/base64/converter/encoding.dart

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import 'package:blockchain_utils/blockchain_utils.dart';
2+
3+
/// A Base64 encoder that supports streaming data in chunks.
4+
/// It accumulates bytes and encodes them in groups of 3,
5+
/// handling padding for incomplete final blocks.
6+
class _Base64StreamEncoder {
7+
static const _base64Table =
8+
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
9+
10+
static String _encode(List<int> bytes) {
11+
final output = StringBuffer();
12+
int i = 0;
13+
14+
while (i + 3 <= bytes.length) {
15+
int chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | (bytes[i + 2]);
16+
output.write(_base64Table[(chunk >> 18) & 0x3F]);
17+
output.write(_base64Table[(chunk >> 12) & 0x3F]);
18+
output.write(_base64Table[(chunk >> 6) & 0x3F]);
19+
output.write(_base64Table[chunk & 0x3F]);
20+
i += 3;
21+
}
22+
23+
int remaining = bytes.length - i;
24+
if (remaining == 1) {
25+
int chunk = bytes[i] << 16;
26+
output.write(_base64Table[(chunk >> 18) & 0x3F]);
27+
output.write(_base64Table[(chunk >> 12) & 0x3F]);
28+
output.write('=');
29+
output.write('=');
30+
} else if (remaining == 2) {
31+
int chunk = (bytes[i] << 16) | (bytes[i + 1] << 8);
32+
output.write(_base64Table[(chunk >> 18) & 0x3F]);
33+
output.write(_base64Table[(chunk >> 12) & 0x3F]);
34+
output.write(_base64Table[(chunk >> 6) & 0x3F]);
35+
output.write('=');
36+
}
37+
38+
return output.toString();
39+
}
40+
41+
final StringBuffer _buffer = StringBuffer();
42+
final List<int> _partial = [];
43+
44+
/// Adds bytes to the encoder buffer, encoding full 3-byte chunks immediately.
45+
void add(List<int> bytes) {
46+
_partial.addAll(bytes);
47+
while (_partial.length >= 3) {
48+
final chunk = _partial.sublist(0, 3);
49+
_buffer.write(_encode(chunk));
50+
_partial.removeRange(0, 3);
51+
}
52+
}
53+
54+
/// Finalizes the encoding, encoding any remaining bytes with padding.
55+
/// Returns the full Base64 encoded string.
56+
String finalize() {
57+
if (_partial.isNotEmpty) {
58+
_buffer.write(_encode(_partial));
59+
}
60+
return _buffer.toString();
61+
}
62+
63+
/// Clears internal buffers to reset the encoder state.
64+
void clean() {
65+
_buffer.clear();
66+
_partial.clear();
67+
}
68+
}
69+
70+
/// Utility class for encoding bytes into Base64 strings with options for
71+
/// URL-safe encoding and optional padding removal.
72+
class B64Encoder {
73+
/// Encodes the given [data] bytes to a Base64 string.
74+
///
75+
/// [noPadding]: If true, removes the '=' padding characters from the output.
76+
/// Defaults to false (padding included).
77+
///
78+
/// [urlSafe]: If true, uses URL-safe Base64 encoding by replacing '+' with '-'
79+
/// and '/' with '_'. Defaults to false (standard Base64).
80+
///
81+
/// Returns the Base64-encoded string.
82+
static String encode(List<int> data,
83+
{bool noPadding = false, bool urlSafe = false}) {
84+
final encoder = _Base64StreamEncoder();
85+
try {
86+
encoder.add(data.asBytes);
87+
String b64 = encoder.finalize();
88+
if (urlSafe) {
89+
b64 = b64.replaceAll('+', '-').replaceAll('/', '_');
90+
}
91+
if (noPadding) {
92+
b64 = b64.replaceAll('=', '');
93+
}
94+
return b64;
95+
} finally {
96+
encoder.clean();
97+
}
98+
}
99+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import 'package:blockchain_utils/blockchain_utils.dart';
2+
3+
class B64ConverterException extends BlockchainUtilsException {
4+
const B64ConverterException(super.message, {super.details});
5+
}

lib/crypto/crypto/crypto.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,7 @@ export 'x_modem_crc/x_modem_crc.dart';
6565

6666
export 'crc16/crc16.dart';
6767
export 'exception/exception.dart';
68+
69+
export 'x25519/x25519.dart';
70+
export 'hkdf/hkdf.dart';
71+
export 'jwt/jwt.dart';

lib/crypto/crypto/hkdf/hkdf.dart

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import 'package:blockchain_utils/blockchain_utils.dart';
2+
3+
/// A Dart implementation of the HKDF (HMAC-based Key Derivation Function) as defined in RFC 5869.
4+
/// This class supports both the extract and expand phases.
5+
class HKDF {
6+
/// Pseudorandom key (output of HKDF-Extract if enabled)
7+
final List<int> ork;
8+
9+
/// Internal HMAC instance used for HKDF-Expand
10+
final HMAC _hmac;
11+
12+
/// Desired length of output keying material (OKM)
13+
final int length;
14+
15+
/// Optional context/application-specific information
16+
final List<int> info;
17+
HKDF._(
18+
{required List<int> ork,
19+
required HMAC hmac,
20+
List<int>? info,
21+
required this.length})
22+
: ork = ork.asImmutableBytes,
23+
info = (info ?? []).asImmutableBytes,
24+
_hmac = hmac;
25+
factory HKDF(
26+
{required List<int> ikm,
27+
required HashFunc hash,
28+
int length = 32,
29+
List<int>? salt,
30+
List<int>? info,
31+
bool hkdfExtract = true}) {
32+
final h = hash();
33+
int iteration = (length / h.getDigestLength).ceil();
34+
if (iteration > 255) {
35+
throw CryptoException('Cannot expand to more than 255 blocks');
36+
}
37+
if (hkdfExtract) {
38+
final ork = HMAC.hmac(hash, salt ?? List<int>.filled(32, 0), ikm);
39+
return HKDF._(
40+
ork: ork, info: info, hmac: HMAC(hash, ork), length: length);
41+
}
42+
return HKDF._(ork: ikm, hmac: HMAC(hash, ikm), info: info, length: length);
43+
}
44+
// HMAC-SHA256 helper
45+
List<int> _hash(List<int> data) {
46+
try {
47+
return _hmac.update(data).digest();
48+
} finally {
49+
_hmac.reset();
50+
}
51+
}
52+
53+
/// Derives the final output keying material (OKM) using HKDF-Expand
54+
List<int> derive({List<int> info = const []}) {
55+
int iteration = (length / _hmac.getDigestLength).ceil();
56+
List<int> okm = [];
57+
List<int> previousBlock = [];
58+
for (int i = 1; i <= iteration; i++) {
59+
final data = <int>[];
60+
data.addAll(previousBlock);
61+
data.addAll([...this.info, ...info]);
62+
data.add(i);
63+
previousBlock = _hash(data);
64+
okm.addAll(previousBlock);
65+
}
66+
67+
return okm.sublist(0, length);
68+
}
69+
}

0 commit comments

Comments
 (0)