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