PuttyPrivateKey.decode constructor

PuttyPrivateKey.decode(
  1. String str, {
  2. int offset = 0,
})

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');
  }
}