243 lines
7.3 KiB
Dart
243 lines
7.3 KiB
Dart
import 'dart:convert';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:pointycastle/asn1.dart';
|
|
import 'package:pointycastle/asn1/object_identifiers.dart';
|
|
import 'package:pointycastle/ecc/api.dart';
|
|
|
|
///
|
|
/// Utils class holding different methods to ease the handling of ANS1Objects and their byte representation.
|
|
///
|
|
class ASN1Utils {
|
|
///
|
|
/// Calculates the start position of the value bytes for the given [encodedBytes].
|
|
///
|
|
/// It will return 2 if the **length byte** is less than or equal 127 or the length calculate on the **length byte** value.
|
|
/// This will throw a [RangeError] if the given [encodedBytes] has length < 2.
|
|
///
|
|
static int calculateValueStartPosition(Uint8List encodedBytes) {
|
|
// TODO tag length can be >1
|
|
var length = encodedBytes[1];
|
|
if (length <= 0x7F) {
|
|
return 2;
|
|
} else {
|
|
return 2 + (length & 0x7F);
|
|
}
|
|
}
|
|
|
|
///
|
|
/// Calculates the length of the **value bytes** for the given [encodedBytes].
|
|
///
|
|
/// Will return **-1** if the length byte equals **0x80**. Throws an [ArgumentError] if the length could not be calculated for the given [encodedBytes].
|
|
///
|
|
static int decodeLength(Uint8List encodedBytes) {
|
|
var valueStartPosition = 2;
|
|
var length = encodedBytes[1];
|
|
if (length <= 0x7F) {
|
|
return length;
|
|
}
|
|
if (length == 0x80) {
|
|
return -1;
|
|
}
|
|
if (length > 127) {
|
|
var length = encodedBytes[1] & 0x7F;
|
|
|
|
var numLengthBytes = length;
|
|
|
|
length = 0;
|
|
for (var i = 0; i < numLengthBytes; i++) {
|
|
length <<= 8;
|
|
length |= encodedBytes[valueStartPosition++] & 0xFF;
|
|
}
|
|
return length;
|
|
}
|
|
throw ArgumentError('Could not calculate the length from the given bytes.');
|
|
}
|
|
|
|
///
|
|
/// Encode the given [length] to byte representation.
|
|
///
|
|
static Uint8List encodeLength(int length, {bool longform = false}) {
|
|
Uint8List e;
|
|
if (length <= 127 && longform == false) {
|
|
e = Uint8List(1);
|
|
e[0] = length;
|
|
} else {
|
|
var x = Uint32List(1);
|
|
x[0] = length;
|
|
var y = Uint8List.view(x.buffer);
|
|
// Skip null bytes
|
|
var num = 3;
|
|
while (y[num] == 0) {
|
|
--num;
|
|
}
|
|
e = Uint8List(num + 2);
|
|
e[0] = 0x80 + num + 1;
|
|
for (var i = 1; i < e.length; ++i) {
|
|
e[i] = y[num--];
|
|
}
|
|
}
|
|
return e;
|
|
}
|
|
|
|
///
|
|
/// Checks if the given int [i] is constructed according to <https://www.bouncycastle.org/asn1_layman_93.txt> section 3.2.
|
|
///
|
|
/// The Identifier octets (represented by the given [i]) is marked as constructed if bit 6 has the value **1**.
|
|
///
|
|
/// Example with the IA5 String tag:
|
|
///
|
|
/// 0x36 = 0 0 1 1 0 1 1 0
|
|
///
|
|
/// 0x16 = 0 0 0 1 0 1 1 0
|
|
/// ```
|
|
/// ASN1Utils.isConstructed(0x36); // true
|
|
/// ASN1Utils.isConstructed(0x16); // false
|
|
/// ```
|
|
///
|
|
///
|
|
static bool isConstructed(int i) {
|
|
// Shift bits
|
|
var newNum = i >> (6 - 1);
|
|
// Check if bit is set to 1
|
|
return (newNum & 1) == 1;
|
|
}
|
|
|
|
static bool isASN1Tag(int i) {
|
|
return ASN1Tags.TAGS.contains(i);
|
|
}
|
|
|
|
///
|
|
/// Checks if the given [bytes] ends with 0x00, 0x00
|
|
///
|
|
static bool hasIndefiniteLengthEnding(Uint8List bytes) {
|
|
var last = bytes.elementAt(bytes.length - 1);
|
|
var lastMinus1 = bytes.elementAt(bytes.length - 2);
|
|
if (last == 0 && lastMinus1 == 0) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
///
|
|
/// Calculates the indefinite length of the ASN1 object.
|
|
/// Throws an [ArgumentError] if the end of content octets is not found.
|
|
///
|
|
static int calculateIndefiniteLength(Uint8List bytes, int startPosition) {
|
|
var currentPosition = startPosition;
|
|
var indefiniteLengthObjects = 0;
|
|
while (currentPosition < bytes.length - 1) {
|
|
if (bytes[currentPosition] == 0x00 &&
|
|
bytes[currentPosition + 1] == 0x00) {
|
|
indefiniteLengthObjects--;
|
|
if (indefiniteLengthObjects == 0) {
|
|
return currentPosition - startPosition;
|
|
}
|
|
currentPosition += 2;
|
|
} else {
|
|
final nextLength =
|
|
ASN1Utils.decodeLength(bytes.sublist(currentPosition));
|
|
final valueStartPosition = ASN1Utils.calculateValueStartPosition(
|
|
bytes.sublist(currentPosition));
|
|
if (nextLength == 0) {
|
|
throw ArgumentError('Invalid length of zero.');
|
|
}
|
|
if (valueStartPosition <= 0) {
|
|
throw ArgumentError(
|
|
'Invalid value start position: $valueStartPosition');
|
|
}
|
|
|
|
if (nextLength == -1) {
|
|
indefiniteLengthObjects++;
|
|
currentPosition += valueStartPosition;
|
|
} else {
|
|
currentPosition += valueStartPosition + nextLength;
|
|
}
|
|
}
|
|
}
|
|
|
|
throw ArgumentError('End of content octets not found');
|
|
}
|
|
|
|
static Uint8List getBytesFromPEMString(String pem,
|
|
{bool checkHeader = true}) {
|
|
var lines = LineSplitter.split(pem)
|
|
.map((line) => line.trim())
|
|
.where((line) => line.isNotEmpty)
|
|
.toList();
|
|
String base64;
|
|
if (checkHeader) {
|
|
if (lines.length < 2 ||
|
|
!lines.first.startsWith('-----BEGIN') ||
|
|
!lines.last.startsWith('-----END')) {
|
|
throw ArgumentError('The given string does not have the correct '
|
|
'begin/end markers expected in a PEM file.');
|
|
}
|
|
base64 = lines.sublist(1, lines.length - 1).join('');
|
|
} else {
|
|
base64 = lines.join('');
|
|
}
|
|
|
|
return Uint8List.fromList(base64Decode(base64));
|
|
}
|
|
|
|
static ECPrivateKey ecPrivateKeyFromDerBytes(Uint8List bytes,
|
|
{bool pkcs8 = false}) {
|
|
var asn1Parser = ASN1Parser(bytes);
|
|
var topLevelSeq = asn1Parser.nextObject() as ASN1Sequence;
|
|
late String curveName;
|
|
Uint8List x;
|
|
if (pkcs8) {
|
|
// Parse the PKCS8 format
|
|
var innerSeq = topLevelSeq.elements!.elementAt(1) as ASN1Sequence;
|
|
var b2 = innerSeq.elements!.elementAt(1) as ASN1ObjectIdentifier;
|
|
var b2Data = b2.objectIdentifierAsString;
|
|
var b2Curvedata = ObjectIdentifiers.getIdentifierByIdentifier(b2Data);
|
|
if (b2Curvedata != null) {
|
|
curveName = b2Curvedata['readableName'] as String;
|
|
}
|
|
|
|
var octetString = topLevelSeq.elements!.elementAt(2) as ASN1OctetString;
|
|
asn1Parser = ASN1Parser(octetString.valueBytes);
|
|
var octetStringSeq = asn1Parser.nextObject() as ASN1Sequence;
|
|
var octetStringKeyData =
|
|
octetStringSeq.elements!.elementAt(1) as ASN1OctetString;
|
|
|
|
x = octetStringKeyData.valueBytes!;
|
|
} else {
|
|
// Parse the SEC1 format
|
|
var privateKeyAsOctetString =
|
|
topLevelSeq.elements!.elementAt(1) as ASN1OctetString;
|
|
var choice = topLevelSeq.elements!.elementAt(2);
|
|
var s = ASN1Sequence();
|
|
var parser = ASN1Parser(choice.valueBytes);
|
|
while (parser.hasNext()) {
|
|
s.add(parser.nextObject());
|
|
}
|
|
var curveNameOi = s.elements!.elementAt(0) as ASN1ObjectIdentifier;
|
|
var data = ObjectIdentifiers.getIdentifierByIdentifier(
|
|
curveNameOi.objectIdentifierAsString);
|
|
if (data != null) {
|
|
curveName = data['readableName'] as String;
|
|
}
|
|
|
|
x = privateKeyAsOctetString.valueBytes!;
|
|
}
|
|
|
|
return ECPrivateKey(osp2i(x), ECDomainParameters(curveName));
|
|
}
|
|
|
|
static BigInt osp2i(Iterable<int> bytes, {Endian endian = Endian.big}) {
|
|
var result = BigInt.from(0);
|
|
if (endian == Endian.little) {
|
|
bytes = bytes.toList().reversed;
|
|
}
|
|
|
|
for (var byte in bytes) {
|
|
result = result << 8;
|
|
result |= BigInt.from(byte);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
}
|