parsePem function

Identity parsePem(
  1. String text,
  2. {StringFunction getPassword,
  3. Identity identity}
)

Privacy-Enhanced Mail (PEM) is a de facto file format for storing and sending cryptographic keys, certificates, and other data.

Implementation

Identity parsePem(String text,
    {StringFunction getPassword, Identity identity}) {
  identity ??= Identity();
  const String beginText = '-----BEGIN ',
      endText = '-----END ',
      termText = '-----';
  int beginBegin, beginEnd, endBegin, endEnd;
  if ((beginBegin = text.indexOf(beginText)) == -1) {
    throw FormatException('missing $beginText');
  }
  if ((beginEnd = text.indexOf(termText, beginBegin + beginText.length)) ==
      -1) {
    throw FormatException('missing $termText');
  }
  if ((endBegin = text.indexOf(endText, beginEnd + termText.length)) == -1) {
    throw FormatException('missing $endText');
  }
  if ((endEnd = text.indexOf(termText, endBegin + endText.length)) == -1) {
    throw FormatException('missing $termText');
  }

  String type = text.substring(beginBegin + beginText.length, beginEnd);
  if (type != text.substring(endBegin + endText.length, endEnd)) {
    throw FormatException('type disagreement: $type');
  }

  int start = beginEnd + termText.length, end = endBegin;
  if (start < text.length && text[start] == '\r') start++;
  if (start < text.length && text[start] == '\n') start++;

  String headersEndText = '\n\n', procType;
  int headersStart = -1, headersEnd = text.indexOf(headersEndText, start);
  if (headersEnd == -1 || headersEnd >= end) {
    headersEndText = '\r\n\r\n';
    headersEnd = text.indexOf(headersEndText, start);
  }
  if (headersEnd != -1 && headersEnd < end) {
    headersStart = start;
    start = headersEnd + headersEndText.length;
    for (String header
        in LineSplitter().convert(text.substring(headersStart, headersEnd))) {
      if (header.startsWith('Proc-Type: ')) {
        procType = header.substring(11);
      } else if (header.startsWith('DEK-Info: ')) {
        throw FormatException('not supported');
      }
    }
  }

  String base64text = '';
  for (String line in LineSplitter().convert(text.substring(start, end))) {
    base64text += line.trim();
  }
  Uint8List payload = base64.decode(base64text);

  switch (type) {
    case 'OPENSSH PRIVATE KEY':
      OpenSSHKey openssh = OpenSSHKey()
        ..deserialize(SerializableInput(payload));
      Uint8List privateKey;
      switch (openssh.kdfname) {
        case 'bcrypt':
          OpenSSHBCryptKDFOptions kdfoptions = OpenSSHBCryptKDFOptions()
            ..deserialize(SerializableInput(openssh.kdfoptions));
          int cipherAlgo;
          if (openssh.ciphername == 'aes256-cbc') {
            cipherAlgo = Cipher.AES256_CBC;
          } else {
            throw FormatException('cipher ${openssh.ciphername}');
          }
          privateKey = opensshKeyCrypt(
              false,
              (getPassword != null ? getPassword() : '').codeUnits,
              kdfoptions.salt,
              kdfoptions.rounds,
              openssh.privatekey,
              cipherAlgo);
          break;

        case 'none':
          privateKey = openssh.privatekey;
          break;

        default:
          throw FormatException('kdf ${openssh.kdfname}');
      }
      SerializableInput input = SerializableInput(privateKey);
      OpenSSHPrivateKeyHeader().deserialize(input);
      String type = deserializeString(SerializableInput(input.viewRemaining()));
      switch (type) {
        case 'ssh-ed25519':
          OpenSSHEd25519PrivateKey ed25519 = OpenSSHEd25519PrivateKey()
            ..deserialize(input);
          if (identity.ed25519 != null) throw FormatException();
          identity.ed25519 =
              tweetnacl.Signature.keyPair_fromSecretKey(ed25519.privkey);
          if (!equalUint8List(identity.ed25519.publicKey, ed25519.pubkey)) {
            throw FormatException();
          }
          return identity;

        case 'ssh-rsa':
          OpenSSHRSAPrivateKey rsaPrivateKey = OpenSSHRSAPrivateKey()
            ..deserialize(input);
          if (identity.rsaPublic != null || identity.rsaPrivate != null) {
            throw FormatException();
          }
          return identity
            ..rsaPublic =
                asymmetric.RSAPublicKey(rsaPrivateKey.n, rsaPrivateKey.e)
            ..rsaPrivate = asymmetric.RSAPrivateKey(rsaPrivateKey.n,
                rsaPrivateKey.d, rsaPrivateKey.p, rsaPrivateKey.q);

        default:
          if (type.startsWith('ecdsa-')) {
            OpenSSHECDSAPrivateKey ecdsaPrivateKey = OpenSSHECDSAPrivateKey()
              ..deserialize(input);
            ECDomainParameters curve =
                Key.ellipticCurve(ecdsaPrivateKey.keyTypeId);
            if (identity.ecdsaPublic != null || identity.ecdsaPrivate != null) {
              throw FormatException();
            }
            identity
              ..ecdsaKeyType = ecdsaPrivateKey.keyTypeId
              ..ecdsaPublic =
                  ECPublicKey(curve.curve.decodePoint(ecdsaPrivateKey.q), curve)
              ..ecdsaPrivate = ECPrivateKey(ecdsaPrivateKey.d, curve);

            if (curve.G * identity.ecdsaPrivate.d != identity.ecdsaPublic.Q) {
              throw FormatException();
            }
            return identity;
          } else {
            throw FormatException('type $type');
          }
      }
      break;

    case 'RSA PRIVATE KEY':
      RSAPrivateKey rsaPrivateKey = RSAPrivateKey()
        ..deserialize(SerializableInput(payload));
      if (identity.rsaPublic != null || identity.rsaPrivate != null) {
        throw FormatException();
      }
      return identity
        ..rsaPublic = asymmetric.RSAPublicKey(rsaPrivateKey.n, rsaPrivateKey.e)
        ..rsaPrivate = asymmetric.RSAPrivateKey(
            rsaPrivateKey.n, rsaPrivateKey.d, rsaPrivateKey.p, rsaPrivateKey.q);

    default:
      throw FormatException('type not supported: $type');
  }
}