Search code examples
powershellencryptionopensslbase64public-key

Encrypting and Encoding a password string with a public key


What I am doing

I am writing a PowerShell script that automatically logs into a device I have (The device is a deeper network connect), and then returns some values. I have previously worked on a version for BASH, which functions as expected.

What is the problem?

I am struggling to correctly encrypt the password with a public key, and then base64 encode it as required.

So what are the components?

The website has a public key, stored in a JS file named encryption-public.js:

const publicKey = `-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAs5nhtKPdlMXWh5FURGyD
GIoLxNPsAJoJtJ1TfiPaKYKNgZt2q4/HxbM3ArJqf7bDEB69SeYpQrvVNeS0c461
zhvl488HuHb8Ommms3/qyfuTvnyCQcjNXXEoBgGRYB+Rd2Q36cOi5qlHdomTCnZm
GBg80ZamQcANayc9I/cIMUtEZbIYTo9TZAs/7ZJAnuAgqUXOjUdbFxVtjCgVsuUF
BIoDfUIuYDvpGA84w+icjE0tD4jkzLo/pFaKvY8+kW4g/ikb0UZQ53FjZpihN1vc
C5s45t/lVWICB9C8y4LzeITbGWLvjfabuSqzP9TaHsdj4+f1MJ2lHPAxiEtkRe46
UtI4EU6kjU8TiZWPMfE8mPCrrswuEiO3ZRKVpG8Z8bzvqCwHSTnmWM+HHmWJxms+
z+0KOiK4oh/+5D2Zf5ETxZZuWlBKBhxPbIZ4ryu2jEafcGg0ChrKyp3rpiFpitK+
iNdI4xfh5XhtBzPKuDjL2RpRg7stlATgrit98c0g0pUqnOnrnizOPs5yZMCYwPz8
hPxenhTxzTAifAv3aCJlepBfuGnVBZO0CUQ76UVJsM81igH0V+5mZxMjwHdl3vZl
TYXhV7pG+y56vd0kOH8oqrYVtKG8XO94gVwz1lWb202ez4cKTD8zbzaj91kuMive
El82NUJJGOZ9oyBL4bwe1BcCAwEAAQ==
-----END PUBLIC KEY-----
`;

export default publicKey;

I have saved this key to a local file called deeper.pem:

-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAs5nhtKPdlMXWh5FURGyD
GIoLxNPsAJoJtJ1TfiPaKYKNgZt2q4/HxbM3ArJqf7bDEB69SeYpQrvVNeS0c461
zhvl488HuHb8Ommms3/qyfuTvnyCQcjNXXEoBgGRYB+Rd2Q36cOi5qlHdomTCnZm
GBg80ZamQcANayc9I/cIMUtEZbIYTo9TZAs/7ZJAnuAgqUXOjUdbFxVtjCgVsuUF
BIoDfUIuYDvpGA84w+icjE0tD4jkzLo/pFaKvY8+kW4g/ikb0UZQ53FjZpihN1vc
C5s45t/lVWICB9C8y4LzeITbGWLvjfabuSqzP9TaHsdj4+f1MJ2lHPAxiEtkRe46
UtI4EU6kjU8TiZWPMfE8mPCrrswuEiO3ZRKVpG8Z8bzvqCwHSTnmWM+HHmWJxms+
z+0KOiK4oh/+5D2Zf5ETxZZuWlBKBhxPbIZ4ryu2jEafcGg0ChrKyp3rpiFpitK+
iNdI4xfh5XhtBzPKuDjL2RpRg7stlATgrit98c0g0pUqnOnrnizOPs5yZMCYwPz8
hPxenhTxzTAifAv3aCJlepBfuGnVBZO0CUQ76UVJsM81igH0V+5mZxMjwHdl3vZl
TYXhV7pG+y56vd0kOH8oqrYVtKG8XO94gVwz1lWb202ez4cKTD8zbzaj91kuMive
El82NUJJGOZ9oyBL4bwe1BcCAwEAAQ==
-----END PUBLIC KEY-----

The file that handles the Encrypting and Encoding is called authUtils.js

The contents of that file:

import axios from 'axios';
import to from 'await-to-js';

import publicKey from '../keys/encryption-public.js';
const crypto = require('crypto');

export const setAuthToken = function (token) {
  if (token) {
    // apply authorization token to every request if logged in
    axios.defaults.headers.common['Authorization'] = token;
  } else {
    // delete auth header
    delete axios.defaults.headers.common['Authorization'];
  }
};

export const encryptWithPublicKey = function (string) {
  if (string) {
    const encrypted = crypto.publicEncrypt(publicKey, Buffer.from(string));
    return encrypted.toString('base64');
  }
};

export const validateToken = async () => {
  await to(axios.post('/api/admin/validateToken'));
};

export const JWT_TOKEN_KEY = 'deeperDeviceLoginToken';

Now, I am not even remotely skilled in NodeJS (which I believe is what this is), so had some help from a knowledgeable person who wrote this working command for me (in BASH, using OpenSSL):

The command that worked is:

$(echo -n "password" | openssl rsautl -encrypt -pubin -inkey deeper.pem -oaep 2> /dev/null | base64 | tr -d '\n');

The command takes the password, strips out any newline at the end of the string, and runs it though OpenSSL, with an RSA step, using the supplied key. Error messages are suppressed, and the output is encoded in base64 with any new lines (and maybe white spaces?) removed from the base64 output. It's ugly, but it works.

Long story short, the rest of that script is also written in BASH, however, it's not relevant to this question.

I decided I want to re-write it all in PowerShell (which I can write as easily as English). That has been successful, with the exception of this step. I can run my PowerShell script if I grab the resultant string from Dev Tools in the browser, and hard-code it into my script. But I don't want to do that, because I want to distribute my code to other people.

What does my code look like in PowerShell for this one step?

#Encrypt the password - I think this is the bit that needs fixing.
$PasswordENC = cmd /c '<NUL set /p =`"password`"| openssl rsautl -encrypt -pubin -inkey "deeper.pem" -oaep 2> $null'

#Encode the output - This seems to be the correct Base64 code, after testing. 
$PasswordB64 = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($PasswordENC))

After all of that, the password can be passed to the deeper device (works with a hardcoded password string):

$Token = ((Invoke-WebRequest -UseBasicParsing -Uri "http://192.168.1.10/api/admin/login" `
-Method POST `
-ContentType "application/json" `
-Body "{`"username`":`"admin`",`"password`":`"$PasswordB64`"}"  |
Select-Object -ExpandProperty Content) | ConvertFrom-Json | Select-Object token).token

The output of the above provides me the required Bearer Token, which I can then use to do the things I need to do. Or at least it is meant to.

Instead, I get Invoke-WebRequest : {"decryptionError":"Failed to decrypt password"}

enter image description here

Note

cmd /c '<NUL set /p =`"password`"|

This is a workaround attempt which tries to fix a newline that is added during the PowerShell pipeline, taken from this solution. I still have the above error message.

What do I want?

enter image description here

Ideally, I'd like to fix that error. With that being said, I am open to a different approach to achieving the same goal. If I can get rid of the need for OpenSSL, that would be great! I'd love to do everything all in code with no external applications, however, I understand there may also be a way with NodeJS in PowerShell. Another thought was to use "Selenium" to do some browser manipulation, however, I think that is not the best way to go... I want to use PowerShell, not Python.

I have tried making a COM object for IE to get the string the oldskool way, however, MS killed IE, and even on machines where it still works, IE doesn't load the JS properly anyway.

Please can anyone help me? :)

EDIT:

The answer has been found! Thanks Shane! :)

$IPAddress = "192.168.11.199"

$publicKeyFile = "D:\certs\keys\deeper\deeper.pem"
$Password = 'password' #this won't remain hard coded!
$bytes = [System.Text.Encoding]::UTF8.GetBytes($Password)
$rsa = New-Object System.Security.Cryptography.RSACryptoServiceProvider
$rsa.ImportFromPem([string](Get-Content $publicKeyFile))
$encryptedData = $rsa.Encrypt($bytes, [System.Security.Cryptography.RSAEncryptionPadding]::OaepSHA1)
$encryptedPassword = [System.Convert]::ToBase64String($encryptedData)

$Token = ((Invoke-WebRequest -UseBasicParsing -Uri "http://$($IPAddress)/api/admin/login" `
-Method POST `
-ContentType "application/json; charset=utf-8" `
-Body "{`"username`":`"admin`",`"password`":`"$encryptedPassword`"}"  |
Select-Object -ExpandProperty Content) | ConvertFrom-Json | Select-Object token).token

$Token

Solution

  • RSACryptoServiceProvider supports PEM data. So modifing the above code you can use the .pem file instead of a xml file, using your deeper.pem file:

    $publicKeyFile = ".\deeper.pem"
    $securePassword = Read-Host -AsSecureString
    $bytes = [System.Text.Encoding]::Unicode.GetBytes($securePassword)
    $rsa = New-Object System.Security.Cryptography.RSACryptoServiceProvider
    $rsa.ImportFromPem([System.string](Get-Content $publicKeyFile))
    $encryptedData = $rsa.Encrypt($bytes,$false)
    $encryptedPassword = [System.Convert]::ToBase64String($encryptedData)
    Write-Host $encryptedPassword
    

    -- update --

    Yes you need a later version of powershell as we need the later vesion of the .net library. There are two problems, RSACryptoServiceProvider is too old and is using a old oaep padding setup and I also missed the oaep padding requirement as the above script should pass $true as the second argument to support oaep - but that will not work as it's using the wrong hash algothrim.

    Need to switch to the newer RSACng class. Below is script that you can run that will round-trip from encrypting in powershell C# and then decrypt it using openssl.

    $publicKeyFile = "publickey.pem"
    $password = Read-Host
    $bytes = [System.Text.Encoding]::Unicode.GetBytes($password)
    $rsa = New-Object System.Security.Cryptography.RSACng
    $rsa.ImportFromPem([string](Get-Content $publicKeyFile))
    $encryptedData = $rsa.Encrypt($bytes, [System.Security.Cryptography.RSAEncryptionPadding]::OaepSHA1)
    $encryptedPassword = [System.Convert]::ToBase64String($encryptedData)
    Set-Content encryptedPassword.b64 $encryptedPassword
    openssl base64 -d -in encryptedPassword.b64 -out encryptedPassword.bin
    openssl rsautl -decrypt -inkey .\privatekey.pem -oaep -in encryptedPassword.bin
    

    -- solution found! --

    Editing this answer to include the working code, so I can mark as answer. The above code was so close to correct, the only difference is that the bytes need to be in in UTF8 instead of Unicode. Also, to make it work cross platform, "RSACryptoServiceProvider" should be used instead of "RSACng".

    $IPAddress = "192.168.11.199"
    
    $publicKeyFile = "D:\certs\keys\deeper\deeper.pem"
    $Password = 'password' #this won't remain hard coded!
    $bytes = [System.Text.Encoding]::UTF8.GetBytes($Password)
    $rsa = New-Object System.Security.Cryptography.RSACryptoServiceProvider
    $rsa.ImportFromPem([string](Get-Content $publicKeyFile))
    $encryptedData = $rsa.Encrypt($bytes, [System.Security.Cryptography.RSAEncryptionPadding]::OaepSHA1)
    $encryptedPassword = [System.Convert]::ToBase64String($encryptedData)
    
    $Token = ((Invoke-WebRequest -UseBasicParsing -Uri "http://$($IPAddress)/api/admin/login" `
    -Method POST `
    -ContentType "application/json; charset=utf-8" `
    -Body "{`"username`":`"admin`",`"password`":`"$encryptedPassword`"}"  |
    Select-Object -ExpandProperty Content) | ConvertFrom-Json | Select-Object token).token
    
    $Token