2

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
OutOfThisPlanet
  • 336
  • 3
  • 17
  • With the trailing-newline problem out of the way, my only guess at what _may_ make a difference is to use `-ContentType "application/json; charset=utf-8"` – mklement0 May 01 '23 at 13:36
  • Unfortunately, that didn't seem to make a difference. I am wondering if there is a difference between OpenSSL on Windows, and on Linux? – OutOfThisPlanet May 01 '23 at 13:38
  • I've just realised you're the same person who raised the fix on github, and on here :) – OutOfThisPlanet May 01 '23 at 13:48
  • 1
    :) Since SSL is platform-agnostic, I wouldn't expect there to be platform differences. – mklement0 May 01 '23 at 13:58

2 Answers2

3

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


OutOfThisPlanet
  • 336
  • 3
  • 17
Shane Powell
  • 13,698
  • 2
  • 49
  • 61
  • Nice. I like this, however it doesn't work on PowerShell 5. Will test on PowerShell core later on. Thanks! – OutOfThisPlanet Jun 01 '23 at 07:36
  • On testing with PowerShell 7, the login page unfortunately still fails to decrypt the password encoded in this manner. Thanks anyway! – OutOfThisPlanet Jun 01 '23 at 07:58
  • I have a feeling it could be to do with "adding a buffer" to the string, which is done in the webpage code: `const encrypted = crypto.publicEncrypt(publicKey, Buffer.from(string));` – OutOfThisPlanet Jun 01 '23 at 08:19
  • I feel like this is getting closer! This time I get "password incorrect" instead of "failed decrypt password". Unfortunately, it only gets this far on Windows. On Linux (alpine) it says "Windows Cryptography Next Generation (CNG) is not supported on this platform" I will play around with it a bit. I appreciate your edit :) – OutOfThisPlanet Jun 02 '23 at 21:26
  • Figured it out! it needs to be `[System.Text.Encoding]::UTF8.GetBytes($Password)` You are a legend! :) Now I need to figure out how to make this run on alpine... but that is a different question for another day! – OutOfThisPlanet Jun 02 '23 at 21:47
  • All sorted. Couldn't have done it without you, Shane! :) Thanks very much! – OutOfThisPlanet Jun 02 '23 at 22:26
1

PowerShell does not have a built-in command for encrypting a password with a public key, but you can use the .NET framework to accomplish this. Here's an example script that demonstrates how to do this:

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

Description: This script prompts the user to enter a password and stores it as a secure string in the variable $securePassword. It then converts the secure string to a byte array and creates a new RSACryptoServiceProvider object. The script reads the public key from a file and loads it into the RSACryptoServiceProvider object. It then uses the Encrypt method of the RSACryptoServiceProvider object to encrypt the password, and converts the encrypted data to a base64-encoded string. Finally, it writes the encrypted password to the console.

To use this script, you will need to replace the path to the public key file ($publicKeyFile) with the actual path to your public key file. You can also modify the script to prompt the user for the path to the public key file.

Note that the encrypted password can only be decrypted using the corresponding private key. You should keep the private key secure and share the public key only with authorized users or systems.

This should give you a hint about your issue. hendy

  • I actually had a similar suggestion from ChatGPT (I've tried so many things!). Problem was with this solution, I don't have an XML file with the public key in, and after further reading I wasn't really sure how to split the key into the 2 parts needed. – OutOfThisPlanet May 01 '23 at 14:01
  • $rsa.FromXmlString(Get-Content $publicKeyFile) needs to be $rsa.FromXmlString((Get-Content $publicKeyFile)). Can't edit though, as it is only 2 characters difference. – OutOfThisPlanet May 01 '23 at 14:08
  • I converted my pem file to xml using this https://raskeyconverter.azurewebsites.net/PemToXml Still not decrypting, unfortunately. Thanks for the hint though. – OutOfThisPlanet May 01 '23 at 14:09
  • 1
    Give Powershell a text file containing the password. Powershell can then convert the text file into an XML document: it can do this. This allows you to use the above script. Too bad you cannot use Python... lol. Good luck. – Henderson Hood May 01 '23 at 14:20
  • I'm not sure I follow you. My public key is a .pem file – OutOfThisPlanet May 07 '23 at 10:29