loadPrivateKeyFromOpenSSHPem static method

Uint8List loadPrivateKeyFromOpenSSHPem(
  1. String pemFileContent, {
  2. String? password,
})

Loads a private key from a pemFileContent that has been encoded with the open ssh file format:

https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key

"openssh-key-v1"0x00 # NULL-terminated "Auth Magic" string 32-bit length, "none" # ciphername length and string 32-bit length, "none" # kdfname length and string 32-bit length, nil # kdf (0 length, no kdf) 32-bit 0x01 # number of keys, hard-coded to 1 (no length) 32-bit length, sshpub # public key in ssh format 32-bit length, keytype 32-bit length, pub0 32-bit length, pub1 32-bit length for rnd+prv+comment+pad 64-bit dummy checksum? # a random 32-bit int, repeated 32-bit length, keytype # the private key (including public) 32-bit length, pub0 # Public Key parts 32-bit length, pub1 32-bit length, prv0 # Private Key parts ... # (number varies by type) 32-bit length, comment # comment string padding bytes 0x010203 # pad to blocksize

Implementation

static Uint8List loadPrivateKeyFromOpenSSHPem(String pemFileContent,
    {String? password}) {
  if (!pemFileContent.startsWith(openSshHeader) ||
      !pemFileContent.startsWith(openSshHeader)) {
    throw Exception('Wrong file format');
  }

  pemFileContent = pemFileContent.replaceAll(RegExp(r'[\n\r]'), '');
  pemFileContent = pemFileContent.substring(openSshHeader.length).trimLeft();
  pemFileContent = pemFileContent
      .substring(0, pemFileContent.length - openSshFooter.length)
      .trimRight();

  var binaryContent =
      base64.decode(pemFileContent.replaceAll(RegExp(r'[\n\r]'), ''));
  var header = binaryContent.sublist(0, binaryContent.indexOf(0));
  if (String.fromCharCodes(header) == 'openssh-key-v1') {
    var readerIndex = header.length + 1;
    var cypherNameLength = _readOpenSshKeyUInt32(binaryContent, readerIndex);
    readerIndex += 4;
    var cypherName =
        _readOpenSshKeyString(binaryContent, readerIndex, cypherNameLength);
    readerIndex += cypherNameLength;
    var keyDerivationFunctionNameLength =
        _readOpenSshKeyUInt32(binaryContent, readerIndex);
    readerIndex += 4;
    var keyDerivationFunctionName = _readOpenSshKeyString(
        binaryContent, readerIndex, keyDerivationFunctionNameLength);
    readerIndex += keyDerivationFunctionNameLength;
    var keyDerivationFunctionOptionsLength =
        _readOpenSshKeyUInt32(binaryContent, readerIndex);
    readerIndex += 4;
    //keyDerivationFunctionLength should be 0 for no kdf
    var keyDerivationFunctionOptions = binaryContent.sublist(
        readerIndex, readerIndex + keyDerivationFunctionOptionsLength);
    readerIndex += keyDerivationFunctionOptionsLength;
    var keyCount = _readOpenSshKeyUInt32(binaryContent, readerIndex);
    if (keyCount != 1) {
      throw Exception('Only single key files are supported for now!');
    }
    readerIndex += 4; //key count should always be 1
    var publicKeyLength = _readOpenSshKeyUInt32(binaryContent, readerIndex);
    readerIndex += 4 + publicKeyLength;
    var privateKeyLength = _readOpenSshKeyUInt32(binaryContent, readerIndex);
    readerIndex += 4;
    var privateKeyIndex = readerIndex;
    if (keyDerivationFunctionName == 'none') {
      return _readOpenSshPrivateKeySeed(binaryContent, readerIndex);
    } else if (keyDerivationFunctionName == 'bcrypt') {
      if (password == null || password.isEmpty) {
        throw Exception('No password supported for encrypted file');
      }
      var keyIv = Uint8List(48);
      var pbkdfSaltLength =
          _readOpenSshKeyUInt32(keyDerivationFunctionOptions, 0);
      BcryptPbkdf.pbkdf(
          password,
          keyDerivationFunctionOptions.sublist(4, 4 + pbkdfSaltLength),
          _readOpenSshKeyUInt32(
              keyDerivationFunctionOptions, 4 + pbkdfSaltLength),
          keyIv);
      var key = keyIv.sublist(0, 32);
      var iv = keyIv.sublist(32, 48);
      late BlockCipher cypher;
      if (cypherName == 'aes256-ctr') {
        cypher = CTRBlockCipher(32, CTRStreamCipher(AESEngine()))
          ..init(false, ParametersWithIV(KeyParameter(key), iv));
      } else if (cypherName == 'aes256-cbc') {
        cypher = CBCBlockCipher(AESEngine())
          ..init(false, ParametersWithIV(KeyParameter(key), iv));
      }

      // Decrypt the cipherText block-by-block

      final paddedPlainText = Uint8List(privateKeyLength); // allocate space
      final cipherText = binaryContent.sublist(
          privateKeyIndex, privateKeyIndex + privateKeyLength);

      var offset = 0;
      while (offset < privateKeyLength) {
        offset +=
            cypher.processBlock(cipherText, offset, paddedPlainText, offset);
      }
      assert(offset == privateKeyLength);

      return _readOpenSshPrivateKeySeed(paddedPlainText, 0);
    } else {
      throw Exception('The given cypherName $cypherName is not supported!');
    }
  } else {
    throw Exception('This is not a valid open ssh key file format!');
  }
}