secure_qr_validator 1.2.0
secure_qr_validator: ^1.2.0 copied to clipboard
A robust Flutter package for validating secure QR codes with battle-tested encryption, digital signatures, and customizable business rules. Perfect for access control, ticketing, and secure document v [...]
Secure QR Validator #
A robust Flutter package for validating secure QR codes with battle-tested encryption, digital signatures, and customizable business rules. Perfect for access control, ticketing, and secure document verification.
🔐 Compatible with Secure QR Generator package
Table of Contents #
- Features
- Installation
- Quick Start
- Format Compatibility
- Advanced Usage
- Business Rules
- Status Visualization
- Best Practices
- Error Handling
- License
Features #
🔒 Secure Validation #
- AES Encryption: Industry-standard AES-256 encryption support
- Digital Signatures: HMAC-SHA256 signature verification
- Temporal Validation: Time-based validation with expiration
- Data Access for Expired QR Codes: Access to data even when QR code is expired
- Replay Protection: Built-in mechanisms against replay attacks
- Tampering Detection: Automatic detection of modified QR codes
📦 Dual Format Support #
- Automatic Detection: Transparently reads both the compact (CBOR + Base45) and legacy (JSON + Base64) payload formats emitted by
secure_qr_generator - No Configuration Needed: Format detection happens per-payload, based on its character set — nothing to toggle
- Safe Fallback: Automatically retries with the other format if a structural decode failure suggests misdetection
🎯 Business Rules #
- Flexible Rules Engine: Define custom validation logic
- Pre-built Validations: Common validation rules ready to use
- Rule Composition: Combine rules using builder pattern
- Type-Safe: Generic type support for data validation
- Extensible: Easy to add new validation rules
🎨 Flutter Integration #
- Ready-to-Use Widgets: Drop-in validation status displays
- Customizable UI: Full control over styling and animations
- Responsive: Adapts to different screen sizes
- Platform Support: Works on iOS, Android, and Web
- Dark Mode: Automatic theme adaptation
Installation #
- Add dependency to your
pubspec.yaml:
dependencies:
secure_qr_validator: ^1.2.0
- Install packages:
flutter pub get
- Import the package:
import 'package:secure_qr_validator/secure_qr_validator.dart';
Quick Start #
Basic Validation #
// Initialize validator
final validator = SecureQRValidator(
ValidatorConfig(
secretKey: 'your-secure-key-min-32-chars-long!!!',
validityDuration: Duration(minutes: 5),
enableEncryption: true,
enableSignature: true,
enableExpirationCheck: true, // Can be set to false to ignore expiration
),
);
// Perform validation — works the same whether the scanned QR code was
// produced in the legacy or the compact format; detection is automatic.
try {
final result = await validator.validateQRPayload(qrCodeContent);
if (result.isValid) {
// Access validated data safely
final userId = result.getData<String>('userId');
final accessLevel = result.getData<int>('accessLevel');
// Continue with business logic
handleValidAccess(userId, accessLevel);
} else if (result.isExpired && result.data != null) {
// Handle expired QR codes with data available
final userId = result.getData<String>('userId');
handleExpiredQRCode(userId, result.generatedAt);
} else {
handleInvalidAccess(result.error);
}
} catch (e) {
handleError(e);
}
With Business Rules #
final validator = SecureQRValidator(
ValidatorConfig(
secretKey: 'your-secure-key-min-32-chars-long!!!',
validityDuration: Duration(minutes: 5),
enableExpirationCheck: true,
),
businessRules: [
// Required fields
CommonValidationRules.required(['userId', 'accessLevel']),
// Access level range check
CommonValidationRules.numberInRange('accessLevel', 1, 5),
// Custom business logic
(data) {
final accessLevel = data['accessLevel'] as int;
final role = data['role'] as String?;
if (role == 'admin' && accessLevel < 3) {
return 'Admin users must have access level 3 or higher';
}
return null; // Validation passed
},
],
);
Format Compatibility #
Since version 1.2.0, validateQRPayload accepts QR codes in either of the two formats secure_qr_generator can emit, without any configuration change:
| Legacy format | Compact format | |
|---|---|---|
| Emitted by generator when | payloadFormat: legacy (default) |
payloadFormat: compact |
| Serialization | JSON | CBOR |
| Encoding | Base64 | Base45 |
| Detected via | Presence of lowercase letters / + / / / = |
Base45 alphabet only (digits, uppercase, $%*+-./:) |
You don't need to know which format a given QR code was generated with — validateQRPayload figures it out from the payload itself and decodes accordingly. ValidatorConfig (secret key, encryption/signature flags, expiration handling) applies uniformly to both.
Rollout ordering #
If you're migrating a fleet of generators from legacy to compact (for example, in a peer-to-peer scanning setup where devices scan each other), upgrade every validator to 1.2.0+ first. Only once that's done should any generator start emitting compact — a validator on 1.1.x or earlier does not understand the compact format and will fail to read those QR codes. See the secure_qr_generator README for the generator-side rollout guidance.
Advanced Usage #
Type-Safe Data Access #
final result = await validator.validateQRPayload(qrContent);
// Data access works even for expired QR codes
if (!result.isValid && result.isExpired && result.data != null) {
// Process expired QR code data
final userId = result.getData<String>('userId');
showExpiredQrCodeMessage(userId);
}
// Safe data access with type checking
final UserProfile profile = result.getDataModel<UserProfile>(
'profile',
(json) => UserProfile.fromJson(json),
);
// With default values
final age = result.getData<int>('age', defaultValue: 0);
final name = result.getData<String>('name', defaultValue: 'Unknown');
// Presence checking
if (result.hasAllData(['email', 'phone'])) {
final contacts = Contacts(
email: result.getData<String>('email'),
phone: result.getData<String>('phone'),
);
}
Handling Expired QR Codes #
// Check if a QR code is expired but still has accessible data
if (result.isExpired && result.data != null) {
// Access expiration details
final expirationDate = result.generatedAt?.add(
validator.config.validityDuration,
);
// Access the data even though it's expired
final ticketId = result.getData<String>('ticketId');
final userName = result.getData<String>('userName');
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Expired Ticket'),
content: Text(
'This ticket expired on ${expirationDate?.toString()}.\n'
'Ticket ID: $ticketId\n'
'User: $userName'
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Close'),
),
TextButton(
onPressed: () => regenerateTicket(ticketId),
child: Text('Regenerate Ticket'),
),
],
),
);
}
Custom Validation Rules #
class DateRangeRule implements ValidationRule {
final String startField;
final String endField;
DateRangeRule(this.startField, this.endField);
@override
String? validate(Map<String, dynamic> data) {
final start = DateTime.parse(data[startField] as String);
final end = DateTime.parse(data[endField] as String);
if (end.isBefore(start)) {
return 'End date must be after start date';
}
return null;
}
}
// Usage
validator.addRule(DateRangeRule('startDate', 'endDate'));
UI Integration #
Status Indicator #
ValidityIndicatorView(
result: validationResult,
builder: (context, status, child) => Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: status.color,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(status.icon),
SizedBox(width: 8),
Text(status.message),
],
),
),
)
Error Handling #
try {
final result = await validator.validateQRPayload(qrContent);
if (result.isValid) {
handleValidationResult(result);
} else if (result.isExpired && result.data != null) {
// Special handling for expired QR codes with data
handleExpiredQRWithData(result);
} else {
handleInvalidQR(result.error);
}
} on ValidationException catch (e) {
// Handle validation errors (invalid format, expired, etc.)
showError('Validation Error', e.message);
} on SecurityException catch (e) {
// Handle security errors (invalid signature, decryption failed, etc.)
showError('Security Error', e.message);
} on BusinessRuleException catch (e) {
// Handle business rule violations
showError('Business Rule Violation', e.message);
} catch (e) {
// Handle unexpected errors
reportError(e);
}
ValidationErrorType.decoding / .format / .decryption can surface from either format's decode path — check result.error?.type and result.error?.message if you need to distinguish which stage failed for logging purposes.
Best Practices #
Security #
- Use strong encryption keys (32+ characters)
- Enable both encryption and signature verification
- Use short validity durations for sensitive operations
- Implement proper key management and rotation
- Add rate limiting for validation attempts
Performance #
- Use appropriate validity durations
- Implement proper error handling
- Profile memory usage in production
Integration #
- Use dependency injection for validator instances
- Add logging for security events, including
result.error?.typeon failed validations — this is the fastest way to distinguish a genuinely invalid QR code from a format-detection or rollout-ordering issue - Use type-safe data access
- Write comprehensive tests covering both payload formats if your generators haven't fully migrated to
compactyet
Handling Expired QR Codes #
- Decide whether to enable expiration checks based on your use case
- Implement graceful degradation for expired QR codes
- Consider adding refresh/regeneration flows for expired codes
- Add clear user messaging for expiration scenarios
License #
This project is licensed under the MIT License - see the LICENSE file for details.