Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# https://dart.dev/guides/libraries/private-files

# Don't commit the following files and directories created by pub
.dart_tool/
build/

# Don't commit the API documentation directory created by dart doc
doc/api/

# Don't commit files and directories created by other development environments
## IntelliJ
*.iml
*.ipr
*.iws
.idea/

## Mac
.DS_Store
49 changes: 27 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@

## Getting started

To use this SDK, you first need login into to [blockfrost.io](https://blockfrost.io) create your project to retrive your API token.
To use this SDK, you first need login into to [blockfrost.io](https://blockfrost.io) create your
project to retrieve your API token.

<img src="https://i.imgur.com/smY12ro.png">

Expand All @@ -31,29 +32,33 @@ Using the SDK is pretty straight-forward as you can see from the following examp
```dart
import 'package:blockfrost_api/blockfrost_api.dart';

void main() async {

String projectId = "<insert project id>";

String out;

try
{
BlockService service = BlockService(Service.networkCardanoMainnet, projectId);

BlockContent block = await service.getLatestBlock();

out = block.hash;
}

catch(e)
{
out = e.toString();
}

print(out);
void main() async {
String projectId = "<insert project id>";

String out;

try {
BlockService service = BlockService(Service.networkCardanoMainnet, projectId);

BlockContent block = await service.getLatestBlock();

out = block.hash;
}

catch (e) {
out = e.toString();
}

print(out);
}
```

# How to run the unit tests?

## Note: Replace the placeholders by values from https://blockfrost.io/dashboard

```
PROJECT_ID_MAINNET="[YOUR_PROJECT_ID_MAINNET_HERE]" PROJECT_ID_IPFS="YOUR_PROJECT_IPFS_ID" dart run test test/
```


3 changes: 1 addition & 2 deletions lib/blockfrost_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,4 @@ export 'src/scripts_service.dart';
export 'src/transactions_service.dart';
export 'src/utilities_service.dart';
export 'src/ipfs_service.dart';


export 'src/utils/utils_exports.dart';
124 changes: 124 additions & 0 deletions lib/src/utils/blockfrost_signature_validator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import 'dart:convert';

import 'package:blockfrost_api/src/utils/signature_validation_exception.dart';
import 'package:blockfrost_api/src/utils/signature_validator.dart';
import 'package:crypto/crypto.dart';

/// Adapter class which implements the validator interface to validate the blockfrost webhook signature.
class BlockfrostSignatureValidator implements SignatureValidator {
@override
bool validate({

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

required String requestPayload,
required String signatureHeader,
required String secretAuthToken,
int maxToleranceSeconds = defaultMaxToleranceSeconds,
}) {
return _validateSignature(
requestPayload: requestPayload,
signatureHeader: signatureHeader,
secretAuthToken: secretAuthToken,
maxToleranceSeconds: maxToleranceSeconds,
);
}
}

bool _validateSignature({
required String requestPayload,
required String signatureHeader,
required String secretAuthToken,
required int maxToleranceSeconds,
int? currentUnixTime,
}) {
// Parse the timestamp and signature from the header
String? timestampString;
List<String> providedSignatures = [];

final parts = signatureHeader.split(',');
for (final part in parts) {
final pair = part.trim().split('=');
if (pair.length == 2) {
final key = pair[0];
final value = pair[1];
if (key == 't') {
timestampString = value;
} else if (key == 'v1') {
providedSignatures.add(value);
}
}
}

if (timestampString == null || providedSignatures.isEmpty) {
throw SignatureValidationException(
"Invalid signature header format.",
header: signatureHeader,
payload: requestPayload,
);
}

final int timestamp;
try {
timestamp = int.parse(timestampString);
} catch (e) {
throw SignatureValidationException(
"Invalid timestamp format.",
header: signatureHeader,
payload: requestPayload,
);
}

// Prepare the signature_payload (timestamp.payload)
final signaturePayload = '$timestampString.$requestPayload';

// Compute the expected signature (HMAC-SHA256)
final key = utf8.encode(secretAuthToken);
final messageBytes = utf8.encode(signaturePayload);

final hmac = Hmac(sha256, key);
final digest = hmac.convert(messageBytes);
final expectedSignature = digest.toString();

// Check for matching signature
bool signatureMatch =
providedSignatures.any((sig) => sig == expectedSignature);

if (!signatureMatch) {
throw SignatureValidationException(
"No signature matches the expected signature for the payload.",
header: signatureHeader,
payload: requestPayload,
);
}

// Check timestamp tolerance (prevent replay attacks)
// Note: currentUnixTime can be injected for testing purposes
final currentTimestamp =
currentUnixTime ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000);
final timeDifference = (currentTimestamp - timestamp).abs();
if (timeDifference > maxToleranceSeconds) {
throw SignatureValidationException(
"Signature's timestamp is outside of the time tolerance.",
header: signatureHeader,
payload: requestPayload,
);
}
return true;
}

/// TESTING ACCESSOR: used only by unit test to access the private method
class TestBlockfrostValidatorAccessor {
bool callValidateSignature({
required String requestPayload,
required String signatureHeader,
required String secretAuthToken,
required int currentUnixTime,
int maxToleranceSeconds = defaultMaxToleranceSeconds,
}) {
return _validateSignature(
requestPayload: requestPayload,
signatureHeader: signatureHeader,
secretAuthToken: secretAuthToken,
currentUnixTime: currentUnixTime,
maxToleranceSeconds: maxToleranceSeconds,
);
}
}
18 changes: 18 additions & 0 deletions lib/src/utils/signature_validation_exception.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
class SignatureValidationException implements Exception {
final String message;
final String header;
final String payload;

SignatureValidationException(
this.message, {
required this.header,
required this.payload,
});

@override
String toString() {
return 'SignatureValidationException: $message\n'
'Header: $header\n'
'Payload: $payload';
}
}
11 changes: 11 additions & 0 deletions lib/src/utils/signature_validator.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// The interface for the signature validation
const int defaultMaxToleranceSeconds = 600;

abstract class SignatureValidator {
bool validate({
required String requestPayload,
required String signatureHeader,
required String secretAuthToken,
int maxToleranceSeconds,
});
}
4 changes: 4 additions & 0 deletions lib/src/utils/utils_exports.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Export validator files
export 'blockfrost_signature_validator.dart';
export 'signature_validation_exception.dart';
export 'signature_validator.dart';
Loading