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