Search code examples
powershellencryptionrsa

Import RSA Public key for string encryption in Powershell 5


I am working on a non-Powershell tool (NodeJS) that will use Powershell for displaying a password prompt. For the purposes of this question, that means the following:

  • I invoke the Powershell script with a public key.
  • The public key will be used to encrypt the password given by the user.
  • The Powershell script outputs the encrypted password.
  • I can't use a version of Powershell that does not ship with Windows, so I'm stuck at Powershell 5.

For the life of me, I can't figure out a way to get the public key into a class that does encryption.

Some of the things I tried:

$rsa = New-Object System.Security.Cryptography.RSACryptoServiceProvider
$rsa.ImportFromPem($publicKeyPem)
# Method invocation failed because [System.Security.Cryptography.RSACng] does not contain a method named 'ImportFromPem'.

$rsa = New-Object System.Security.Cryptography.RSACryptoServiceProvider
$rsa.Import()
# Method invocation failed because [System.Security.Cryptography.RSACryptoServiceProvider] does not contain a method named 'Import'.

$rsa = [System.Security.Cryptography.AsymmetricAlgorithm]::Create()
$rsa.ImportRSAPublicKey()
# Method invocation failed because [System.Security.Cryptography.RSACryptoServiceProvider] does not contain a method named 'ImportRSAPublicKey'.

I definitely tried more options, all running into the same issue where the function does not exist. Because of this, I can't get it to import the key. When generating a new key pair in Powershell, I can successfully encrypt and decrypt. But that does not help me here.

This question is similar to Encrypting and Encoding a password string with a public key. But that solution is no help for me since its solution requires Powershell > 5.

I'm I trying to do something weird here? Surely using an existing public key for encryption shouldn't be this difficult.


Solution

  • As far as I know, Powershell 5 does not support importing RSA public keys in X.509/SPKI or PKCS#1 format (note that PKCS#8 is a private key format), neither PEM nor DER encoded.
    XML is supported (via FromXmlString()), but not by the crypto module of NodeJS.

    However, it is possible to export the RSA public key in NodeJS in JWK format (supported since v15.9.0). The JWK can be easily converted to the XML format, especially for public RSA keys, since they contain only two parameters, the modulus (n) and the public exponent (e).
    Once the key is in XML format, it can be imported with FromXmlString().

    The following Powershell script imports a public key in JWK format (via conversion to the XML format) and performs an RSA encryption with PKCS#1 v1.5 padding:

    # Function to convert from Base64url to Base64
    function ToBase64($b64Url) {
        $b64 = $b64Url.Replace('_', '/').Replace('-', '+');
        switch ( $b64Url.Length % 4 )
        {
            2 { $b64 += "=="; break }
            3 { $b64 += "="; break    }
        }
        return $b64
    }
    
    # Input: Public RSA key as JWK
    $publicKeyJwk = '{"kty":"RSA","n":"sucdbBGl3zbrWewgHMYqDiToF9EUDFlTUp7zi8F-Gu1cbkZgY_ZzTZsbsQtWu5QlvCIp75U_tDDqogA5RCgGOLrrgL_1sDQqktG6erkjpNmvRqRk2QQc3fVLBPlPXJVmLDV7zwcZTgBwCl-3gO6V-PndTENi17j5aW697RV44ZFIaFBvv74kb7gas-71NE8mKbahlIwAdssk2xHm0E81CvNwBk3ISuNY_vBrDHLXpjlJqUUe2AHMMO_zO4rWEs-fQ5lSlAEa7Un3wRodR-ETJsra-AP81-vwyPPaCA9jkkKsRATDfvunBWyjwFAVSck_TzOaOeKgyGj7MQ4KnuzGRw","e":"AQAB"}';
    
    # Extract modulus (n) and public exponent (e)
    $keyParams = $publicKeyJwk | ConvertFrom-Json
    $nB64url = $keyParams.n 
    $eB64url = $keyParams.e 
    
    # Convert modulus and public exponent from Base64url to Base64
    $nB64 = ToBase64($nB64url);
    $eB64 = ToBase64($eB64url);
    
    # Create public RSA key in XML format
    $publicKeyXml = "<RSAKeyValue><Modulus>" + $nB64 + "</Modulus><Exponent>" + $eB64 + "</Exponent></RSAKeyValue>";
    Write-Output $publicKeyXml;
    
    # Import key 
    $rsa = New-Object System.Security.Cryptography.RSACryptoServiceProvider
    $rsa.FromXmlString($publicKeyXml)
    
    # Encrypt with RSA and PKCS#1 v1.5 padding
    $plaintext = "The quick brown fox jumps over the lazy dog"
    [byte[]] $plaintextBytes = [Text.Encoding]::UTF8.GetBytes($plaintext) 
    $ciphertext = $rsa.Encrypt($plaintextBytes, [System.Security.Cryptography.RSAEncryptionPadding]::Pkcs1)
    $ciphertextB64 = [System.Convert]::ToBase64String($ciphertext)
    
    #Output Base64 encoded ciphertext
    Write-Output $ciphertextB64
    

    A test is possible by generating an RSA key pair with the following NodeJS code:

    var crypto = require('crypto')
    
    var { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', {modulusLength: 2048})
    var pubkeyjwk = publicKey.export({format:'jwk'})
    console.log(JSON.stringify(pubkeyjwk))
    
    var privatejwk = privateKey.export({type:'pkcs1', format:'pem'})
    console.log(privatejwk)
    

    The NodeJS code exports the public key as JWK and the private key as PEM encoded PKCS#1 key.

    The exported public key can be used in the above Powershell script.
    The ciphertext generated by the Powershell script can be decrypted with the private key using a suitable code/tool (e.g. CyberChef), proving the correct encryption by the Powershell script.