PuttyPrivateKey.decode constructor
Decode from text
Throws a FormatException if the string does not contain correctly encoded value. Any whitespace at the start of the string is skipped.
Implementation
PuttyPrivateKey.decode(String str, {int offset = 0}) {
// The PuTTY Private Key format is documented in "sshpubk.c" from the
// Putty source code.
if (offset < 0) {
throw ArgumentError.value(offset, 'offset', 'is negative');
}
source =
PvtTextSource(str, offset, str.length, PvtKeyEncoding.puttyPrivateKey);
// Split into lines
final actualStr = (source!.begin == 0) ? str : str.substring(offset);
final lines = const LineSplitter().convert(actualStr);
// Parse the lines. Although the PPK format defines a strict order for
// the lines, this parser will accept the lines in any order.
String? _privateMAC;
String? _keyType;
String? _encryption;
Uint8List? _publicKeyBytes;
Uint8List? _privateKeyBytes;
while (lines.isNotEmpty) {
// Try to match a name: value line.
//
// Important: exactly one space is expected after the colon. This is very
// significant for the comment, where spaces in the value (possibly at the
// beginning, the end, or entirely made up of spaces) are significant.
// The comment is included in the calculation of the Private-MAC, so every
// character is significant, otherwise the HMAC will not match.
final a = RegExp(r'^([\w_-]+): (.*)$').firstMatch(lines[0]);
if (a == null) {
if (RegExp(r'^\s*$').hasMatch(lines[0])) {
lines.removeAt(0);
continue; // skip blank lines
} else {
throw KeyBad('PPK: invalid format: ${lines[0]}');
}
}
final name = a.group(1)!;
final value = a.group(2)!;
String? numBase64Lines;
switch (name) {
case puttyKeyTypeTag:
_keyType = value;
break;
case 'Encryption':
if (value != 'none' && value != 'aes256-cbc') {
throw KeyUnsupported('PKK encryption: $value');
}
_encryption = value;
break;
case 'Comment':
comment = value;
break;
case 'Public-Lines':
numBase64Lines = value;
break;
case 'Private-Lines':
numBase64Lines = value;
break;
case 'Private-MAC':
_privateMAC = value;
break;
default:
throw KeyBad('PPK tag unknown: $name');
}
lines.removeAt(0);
if (numBase64Lines != null) {
// Process indicated number of lines as base-64 encoded data
int numLines;
try {
numLines = int.parse(numBase64Lines);
if (numLines < 0) {
throw KeyBad('PPK: $name: negative');
}
} on FormatException {
throw KeyBad('PPK: $name: not integer');
}
final buf = StringBuffer();
while (0 < numLines) {
if (lines.isEmpty) {
throw KeyBad('PPK: incomplete');
}
buf.write(lines[0]);
lines.removeAt(0);
numLines--;
}
final data = base64.decode(buf.toString());
if (name == 'Public-Lines') {
_publicKeyBytes = data;
} else if (name == 'Private-Lines') {
_privateKeyBytes = data;
} else {
assert(false);
}
}
}
if (_keyType == null) {
throw KeyBad('PPK: missing standard tag');
}
if (_keyType != 'ssh-rsa') {
throw KeyUnsupported('PPK key type: $_keyType');
}
keyType = _keyType;
if (_encryption == null) {
throw KeyBad('PKK: missing encryption tag');
}
encryption = _encryption;
if (comment != null) {
if (comment!.isEmpty) {
// Comment is always present in PPK, even when its value is an empty
// string. This implementation represents an emtpy comment with null,
// so if it is translated to other formats (where the comment is optional)
// empty comments will be omitted.
comment = null;
}
} else {
throw KeyBad('PPK: missing comment tag');
}
if (_publicKeyBytes != null) {
publicKeyBytes = _publicKeyBytes;
} else {
throw KeyBad('PPK: missing Public-Lines');
}
if (_privateKeyBytes != null) {
privateKeyBytes = _privateKeyBytes;
} else {
throw KeyBad('PPK: missing Private-Lines');
}
// Check the MAC
if (_privateMAC == null) {
throw KeyBad('PPK: missing Private-MAC');
}
final passphrase = ''; // TODO
final calculatedMac = _calculatePrivateMAC(keyType, passphrase);
if (_privateMAC != calculatedMac) {
// print('Private-MAC: read="$_privateMAC" calculated="$calculatedMac"');
throw KeyBad('PPK: key tampered with: Private-MAC does not match');
}
}