- Poll: How reliable are ChatGPT and Bing Chat? - Tue, May 23 2023
- Pip install Boto3 - Thu, Mar 24 2022
- Install Boto3 (AWS SDK for Python) in Visual Studio Code (VS Code) on Windows - Wed, Feb 23 2022
This problem has haunted scripting guys for ages. Whenever you want to run a script without prompting the user for credentials, you have to store the password in your script. Of course, this causes security headaches. PowerShell offers a few ways to handle this problem, although none of them are really satisfying.
The -Credential parameter
Quite a few cmdlets exist that allow you to provide credentials in order to perform tasks that require authentication. To get a list of these cmdlets, you can run the following command:
Get-Command -ParameterName -Credential
The -Credential parameter expects an object of the type PSCredential as input. However, if you provide a username as a string, PowerShell will produce a dialog window where you can enter a password. The following command maps a network share using the Administrator account. After you run the command, PowerShell will prompt you to enter a password.
New-PSDrive -Name K -PSProvider FileSystem -Root \\server\share -Credential "Administrator"
The -Credentials parameter opens a dialog window.
Creating a PSCredential object
If you want to create a PSCredential object in your script to avoid prompting the user multiple times, you have two options. You can use the Get-Credential cmdlet or the New-Object cmdlet. The Get-Credential cmdlet prompts the user for username and password. You can use the method with the New-Object cmdlet if you don’t want to prompt the user but load the password from a file instead.
The next example demonstrates the usage of the Get-Credential cmdlet. We store the PSCredential object in the $Credentials variable, which allows us to reuse the credentials in the script.
$Credentials = Get-Credential New-PSDrive -Name K -PSProvider FileSystem -Root \\server\share -Credential $Credentials
The PSCredential object has a few properties and methods that allow you to evaluate the credentials before you use them for authentication. To get a list of the available properties and methods, you can pipe the object to the Get-Member cmdlet:
$Credentials | Get-Member
To view the username, you can use this command:
$Credentials.UserName
To read the domain the user enters, you need this command:
$Credential.GetNetworkCredential().Domain
You can access the password length like this:
$Credentials.GetNetworkCredential().Password.Length
And, if you’re wondering whether you can also get the password in clear text, yes, this is possible as well. Here’s how:
$Credentials.GetNetworkCredential().Password
The second option mentioned above to create a PSCredential object using the New-Object cmdlet works like this:
$Credentials = New-Object System.Management.Automation.PSCredential $UserName, $SecureString
The $UserName variable should contain a string with the username. The $SecureString variable contains the password as a secure string. The term “secure string” is perhaps a bit misleading because a secure string is not a standard string but an object of the type SecureString.
Creating a SecureString object
You have two options for creating a SecureString object. You can prompt the user to enter the password with the Read-Host cmdlet, together with the -AsSecureString parameter, or you can use the ConvertTo-SecureString cmdlet.
The difference between Read-Host and the Get-Credential method discussed above is that, with Read-Host, you only provide the password.
$SecureString = Read-Host -AsSecureString
After you’ve stored the password in a secure string, you can create the PSCredential object with the New-Object cmdlet as described above. You can use this method to also create an encrypted password and save it to a file (see below).
If you don’t want to prompt a user for the password, you can use the ConvertTo-SecureString cmdlet, like this:
$SecureString = ConvertTo-SecureString "mypw" -AsPlainText -Force
The thing with the -Force parameter is a bit awkward. It is always required if you use the -AsPlainText parameter. I guess it is the developer’s way of saying that you are about to do something naughty. A password in clear text in a script is not really best practice.
Saving an encrypted password
A better option is to save the encrypted password in a text file:
$SecureString = Read-Host -AsSecureString $EncryptedPW = ConvertFrom-SecureString -SecureString $SecureString Set-Content -Path "C:\tmp\mypw.txt" -Value $EncryptedPW
The ConvertFrom-SecureString cmdlet takes a secure string as input and converts it to a real string that contains the encrypted password. The Set-Content cmdlet then saves the password in a text file.
The following code reads the encrypted password from the text file and then creates a PSCredential object:
$EncryptedPW = Get-Content -Path "C:\tmp\mypw.txt" $SecureString = ConvertTo-SecureString -String $EncryptedPW $Credentials = New-Object System.Management.Automation.PSCredential "User", $SecureString
A problem with this method is that it only works if you are logged on to the same computer with the account that created the encrypted passwords. If you try to decrypt the password on another machine or with another Windows account, you will receive the following error message:
ConvertTo-SecureString : Key not valid for use in specified state
This is why it is possible to access the password in clear text as mentioned above. This works similar to the Encrypting File System. After you log on to the Windows machine, you have access to the key that allows you to decrypt the data. This is all managed by the Data Protection Application Programming Interface (DPAPI).
Another problem is that, if you sysprep the machine or reset the password of the Windows account that created the encrypted password, you also no longer have access to the key to decrypt your saved password.
Using the -Key parameter
A remedy is to not rely on the DPAPI but to use your own key to encrypt the password instead. The ConvertTo-SecureString and the ConvertFrom-SecureString cmdlets support the -Key and -SecureKey parameters for this purpose.
The -Key parameter accepts an array with elements of the data type Byte. The key lengths can be 16, 24, or 32 bytes. The following example demonstrates how you can use the -Key parameter:
Function RandomKey { $RKey = @() For ($i=1; $i -le 16; $i++) { [Byte]$RByte = Get-Random -Minimum 0 -Maximum 256 $RKey += $RByte } $RKey } $Key = RandomKey $ScureString = ConvertTo-SecureString -String "mypw" -AsPlainText -Force $EncryptedPW = ConvertFrom-SecureString -SecureString $ScureString -Key $Key Set-Content -Path "C:\tmp\mypw.txt" -Value $EncryptedPW
The function RandomKey uses the Get-Random cmdlet to generate an array with 16 random byte elements. We then use this key for the ConvertFrom-SecureString cmdlet to create the encrypted password which we then save to a file with the Set-Content cmdlet.
To decrypt the password, you can use this code:
$EncryptedPW = Get-Content -Path C:\tmp\mypw.txt $SecureString = ConvertTo-SecureString -String $EncryptedPW -Key $Key $Credentials = New-Object System.Management.Automation.PSCredential "User", $SecureString $Credentials.GetNetworkCredential().Password
Of course, you have to use the same key to decrypt the password. If you test the scripts in PowerShell ISE, the $Key variable will still hold the value from the first script when you start the second script. In practice, you could store the key in the script. But then you just obfuscated the password a little because it is not visible in clear text. However, anyone who knows a little PowerShell can reveal the password easily. Thus, in a way, the solution with your own key is less secure than using DPAPI to encrypt the password because DPAPI ensures that the password can only be decrypted on a particular machine and with a particular Windows account.
Using the -SecureKey parameter
If you think that working with the -SecureKey option improves security, I have to disappoint you. The ‑SecureKey parameter takes a secure string as input. The example below first creates a random password with 16 ASCII characters. This random password is then used to create the secure string, which we use to encrypt our sample password (mypw).
Function RandomPW { $RPW = "" For ($i=1; $i -le 16; $i++) { [Char]$RChar = Get-Random -Minimum 33 -Maximum 127 $RPW += $RChar } $RPW } $RandomPW = RandomPW $SecureString = ConvertTo-SecureString -String "mypw" -AsPlainText -Force $RandomSecureString = ConvertTo-SecureString -String $RandomPW -AsPlainText -Force $EncryptedPW = ConvertFrom-SecureString -SecureString $SecureString -SecureKey $RandomSecureString Set-Content -Path "C:\tmp\mypw.txt" -Value $EncryptedPW
To decrypt the sample password, you can use the following code:
$EncryptedPW = Get-Content -Path C:\tmp\mypw.txt $SecureString = ConvertTo-SecureString -String $EncryptedPW -SecureKey $RandomSecureString $Credentials = New-Object System.Management.Automation.PSCredential "User", $SecureString $Credentials.GetNetworkCredential().Password
The problem here is that you can’t store the secure string that we used to encrypt the password in your script or in a file. This means you that can’t use the last script to decrypt the password independently from the previous script that encrypts the password. Hence, this solution also doesn’t solve our problem—that is, to securely hide a password in the script.
Conclusion
This is not really surprising because we are using symmetric cryptography here. Whenever your script requires access to the encrypted password, you will always have to provide the key. Asymmetric encryption would solve the problem, but the cmdlets discussed in this post do not support this.
So what are these methods good for? You can use these techniques in scripts that you run yourself. If you store the key at a secure location, say an encrypted VeraCrypt container that you open with a password before you run your scripts, you can store all the passwords you need for your work securely in your scripts. This can save you typing if you have to authenticate often with different credentials. You can also share scripts that have encrypted passwords with other admins if they have access to the key.
If you want to run scripts with different credentials than those of the logged-on user, you can use the Windows Task Scheduler. But that would be a topic for another post.
Great article, like always. I, along with other readers, appreciate you and the others sharing your experience knowledge!
Mike, thanks!
I use a bunch of scheduled tasks set to run as the same account which was used to create the encrypted password in a textfile, and after a few months I got the ConvertTo-SecureString : Key not valid for use in specified state error. It was obviously being run on the same machine (which had never been taken off and put back on the domain or anything like that), running as the same user account which had never been recreated, or password changed etc.
As tempting as using the -key parameter would be to specify the key manually (which could be a workaround for this), this is not allowed in the current environment as someone could use that key to find out the password stored in the encrypted text file. I want to first of all understand why we even get the ConvertTo-SecureString : Key not valid for use in specified state error given it’s being used on the same machine?
I searched a little and it could be a Windows bug. Did you install all updates on the computer?
Up until about 2 weeks ago, these servers only had patches until March 2015. Only over the course of the last few weeks have we now got WSUS working to start rolling out patches. Is there a specific patch do you think that resolves this issue? I could see to make sure we’ve rolled this specific one out.
If you google the error message, you will find many pages that attribute the error to an update. Before you go deeper into this, I recommend installing all new updates. If the problem still exists, I would just encrypt the file again. I think there are many other possible causes. One is that someone reset the password of the account that was used to encrypt the file