Ever find yourself juggling endless passwords, worried about whether they’re secure enough? You’re not alone. In today’s digital landscape, protecting sensitive accounts and data goes beyond just a clever passphrase. Enter multi-factor authentication (MFA)—an effective layer of defense that makes sure a random intruder can’t waltz in with stolen credentials.
Time-Based One-Time Password (TOTP) systems are a powerful way to implement MFA, producing short-lived codes that are invalid mere seconds later. If you’ve ever used an authenticator app on your phone, you’ve already seen TOTP at work. However, building your own TOTP generator can offer greater flexibility for custom workflows, automation, and even just tinkering to learn something new.
In this tutorial, we’ll walk through a PowerShell script that securely stores your TOTP secret keys in the Windows Registry, then generates fresh codes whenever you need them. By the end, you’ll have a handy, easily integrable tool that keeps your secrets encrypted and your one-time passwords safe and ready on demand. Let’s jump in!
Why Multi-Factor Authentication (MFA) and TOTP Are Important
Two-factor authentication (MFA) provides an additional layer of security beyond just a username and password. If an attacker obtains your login credentials, having a second requirement—like a one-time code—protects your account from unauthorized access. A common way to implement that second factor is through Time-Based One-Time Passwords (TOTP). TOTP codes expire quickly (usually every 30 seconds), making them nearly impossible to reuse if intercepted.
This approach drastically reduces the risk of attacks such as credential stuffing or password reuse because even if your password is compromised, an attacker also needs the temporary code. TOTP has become a staple in modern security thanks to its simplicity and reliability.
The script discussed here aims to streamline and secure the process of generating TOTP codes. First, it stores a secret key—needed for TOTP generation—in an encrypted format within the Windows Registry. Then, it provides a mechanism to retrieve that key and compute a valid TOTP code on demand. This allows you to seamlessly integrate TOTP into your automation workflows, scheduled tasks, or day-to-day administration without exposing sensitive information in plain text.
By combining registry-based encryption with industry-standard TOTP logic, this solution delivers a convenient and secure way to incorporate multi-factor authentication capabilities into your PowerShell toolkit.
This content is most valuable for anyone working with PowerShell who wants to improve account security. IT professionals can use it to bolster protection on internal tools, while security enthusiasts can learn more about integrating cryptographic practices into everyday scripts. In addition, administrators or developers who automate tasks using PowerShell will appreciate having a straightforward way to generate one-time passwords for applications or services that support TOTP-based MFA. Whether you’re a seasoned PowerShell scripter or just starting to explore enhanced security measures, this tutorial can help you integrate TOTP authentication into your existing workflows.
Understanding Time-Based One-Time Passwords (TOTP)
Brief Overview of TOTP
Time-Based One-Time Passwords (TOTP) are a popular form of two-factor authentication (MFA) that generate temporary codes based on a combination of a secret key and the current time. When you enable TOTP, you’re essentially adding a new layer of security to your login process. Even if a malicious user happens to get hold of your username and password, they still need the short-lived TOTP code, which usually expires every 30 seconds or so. This time-sensitive nature makes TOTP an excellent defense against common credential attacks.
Key Components
-
Secret Key
This is a shared, usually Base32-encoded string that both the server and your TOTP generator (like an authenticator app or a script) know. It’s crucial to keep this secret key safe, because it’s the “seed” used to generate valid codes. -
Time Window
TOTP operates on discrete time intervals—often 30 seconds—called “windows.” Once a time window elapses, the current code becomes invalid and a new code takes its place. This ticking clock aspect helps ensure that codes can’t be reused. -
Hashing Algorithm (HMAC-SHA1)
TOTP relies on the HMAC-based One-Time Password (HOTP) algorithm, which, in turn, uses a hashing function. By default, TOTP typically uses SHA1, although variations with stronger algorithms like SHA256 or SHA512 exist.
Relevant Standards
TOTP is defined by RFC 6238, which builds upon the HOTP specification in RFC 4226. HOTP provides a framework for generating one-time passwords using a secret key and a moving factor. TOTP simply updates that moving factor to be time-based instead of incrementing counters, delivering short-lived codes that are widely used in authenticator apps today.
Why Store Secrets Securely in PowerShell
Challenge: Hard-Coding or Saving Secrets in Plain Text Is Risky
If you’ve ever stored passwords, keys, or other credentials right in your scripts, you know how quickly that can become a security nightmare. Anyone with access to your code (or a local drive backup, or a GitHub repository) could potentially see and misuse those secrets. Even a seemingly harmless administrative script could turn into a backdoor for an attacker, which is why we need a safer alternative to stuffing our credentials in code.
Solution: Encrypting and Storing Secrets in the Windows Registry (Per-User)
An elegant fix is to offload secrets to the Windows Registry, leveraging encryption behind the scenes. By saving sensitive data under your user account, you isolate your secrets from other users on the same machine. You can still easily retrieve them within PowerShell, but casual prying eyes won’t see the raw key. It’s a handy blend of convenience and security.
Mechanism: Using DPAPI (ConvertTo-SecureString, ConvertFrom-SecureString) to Handle Encryption/Decryption Seamlessly
Microsoft’s Data Protection API (DPAPI) is the backbone of secure storage in Windows. It works by linking encrypted data to either the local machine or a specific user account. In PowerShell, DPAPI is exposed through:
ConvertTo-SecureString
: Transforms plaintext into an in-memory secure string.ConvertFrom-SecureString
: Transforms that secure string into an encrypted, storable format.
When we store the DPAPI-encrypted text in the registry, only the original user account that performed the encryption can decrypt it. This ensures that even if someone copies the registry values, they can’t decrypt the data unless they’re running under the same user context.
Script Breakdown
Overview of Functions
This PowerShell script is organized into a few core functions that handle saving, retrieving, and using secrets for TOTP generation. Below is a quick tour of each major function and what it does.
Save-Secret
- Purpose: Encrypt and store a secret in the registry.
- Explanation:
This function leverages the Windows Data Protection API (DPAPI) viaConvertTo-SecureString
andConvertFrom-SecureString
. The secret (e.g., your TOTP seed) gets transformed into a secure, encrypted blob, then encoded in Base64 for easy storage. The registry pathHKCU:\Software\TOTPSecrets
is used, and each secret is associated with a friendly name. Because it’s stored under the user’s profile, only that same user can decrypt it later.
function Save-Secret {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[string]$Name,
[Parameter(Mandatory = $true)]
[string]$Secret
)
# Define the registry path
$regPath = "HKCU:\Software\TOTPSecrets"
# Ensure the registry key exists
if (-not (Test-Path $regPath)) {
New-Item -Path $regPath -Force | Out-Null
}
try {
# Convert the secret to a secure string
$secureSecret = ConvertTo-SecureString $Secret -AsPlainText -Force
$dpapiString = ConvertFrom-SecureString $secureSecret
# Save the encrypted secret in the registry
Set-ItemProperty -Path $regPath -Name $Name -Value $dpapiString
Write-Host "Secret saved securely under the name '$Name'." -ForegroundColor Green
}
catch {
Write-Host "Failed to save the secret. Error: $_" -ForegroundColor Red
}
}
Get-Secret
- Purpose: Retrieve and decrypt the previously stored secret.
- Explanation:
When you callGet-Secret
, it looks up the Base64-encoded, encrypted string from the registry. It then decodes it back into the encrypted data, which DPAPI converts to a plain-text secure string. In other words, only the same user account that saved the secret can successfully unlock it. The final result is your original key or password, ready to be used securely within your scripts.
function Get-Secret {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[string]$Name
)
# Define the registry path
$regPath = "HKCU:\Software\TOTPSecrets"
try {
if (Test-Path $regPath) {
# Retrieve the encrypted string from the registry
$encryptedSecret = (Get-ItemProperty -Path $regPath -Name $Name).$Name
$secureString = ConvertTo-SecureString $encryptedSecret
# Convert the SecureString back into plaintext
$plainText = [System.Net.NetworkCredential]::new("", $secureString).Password
return $plainText
}
else {
Write-Host "No secret found with the name '$Name'." -ForegroundColor Yellow
}
}
catch {
Write-Host "Failed to retrieve the secret. Error: $_" -ForegroundColor Red
}
}
Get-Otp
- Purpose: Generate a TOTP code based on the stored secret.
- Explanation:
Get-Otp
begins by callingGet-Secret
to retrieve the user’s stored secret key. Then, it calculates a hash using the HMAC-SHA1 algorithm, incorporating the current time window. It applies “dynamic truncation,” an approach that extracts a short piece of the resulting hash to produce the one-time code. The code is then padded (if necessary) to match the desired length—commonly six digits. By default, each code typically lasts 30 seconds before the next is generated.
function Get-Otp {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[string]$Name,
[Parameter(Mandatory = $false)]
[int]$LENGTH = 6,
[Parameter(Mandatory = $false)]
[int]$WINDOW = 30
)
try {
# Retrieve the secret securely
$SECRET = Get-Secret -Name $Name
if (-not $SECRET) {
throw "No secret found with the name '$Name'."
}
# Convert secret to HMAC key
$hmac = New-Object -TypeName System.Security.Cryptography.HMACSHA1
$hmac.key = Convert-HexToByteArray(Convert-Base32ToHex($SECRET.ToUpper()))
# Generate Time Byte Array
$timeBytes = Get-TimeByteArray $WINDOW
# Compute Hash
$randHash = $hmac.ComputeHash($timeBytes)
# Dynamic Truncation to extract the OTP
$offset = $randHash[$randHash.Length - 1] -band 0x0f
$fullOTP = ($randHash[$offset] -band 0x7f) * [math]::Pow(2, 24)
$fullOTP += ($randHash[$offset + 1] -band 0xff) * [math]::Pow(2, 16)
$fullOTP += ($randHash[$offset + 2] -band 0xff) * [math]::Pow(2, 8)
$fullOTP += ($randHash[$offset + 3] -band 0xff)
# Generate the final OTP
$modNumber = [math]::Pow(10, $LENGTH)
$otp = $fullOTP % $modNumber
$otp = $otp.ToString("0" * $LENGTH)
return $otp
}
catch {
Write-Host "Failed to generate OTP. Error: $_" -ForegroundColor Red
return $null
}
}
Supporting Functions
The complete code for these functions is available in the Github link at the end of this page
-
Get-TimeByteArray
Responsible for converting the current Unix time into a Big-Endian byte array. It divides the current timestamp by a specified window (e.g., 30 seconds) to figure out which time slot we’re in. -
Convert-Base32ToHex, Convert-HexToByteArray
These helper functions handle essential string conversions. Some TOTP secrets are provided in Base32 format, soConvert-Base32ToHex
transforms that into a Hex string, andConvert-HexToByteArray
converts the Hex into raw bytes, ready for hashing. -
Add-LeftPad, Convert-IntToHex
Small utility functions to format and convert data.Add-LeftPad
ensures we can align our strings (like adding zeros in front of a numeric code), andConvert-IntToHex
simply converts integers to Hex strings for internal operations.
This modular approach—separating secure storage, secret retrieval, and TOTP generation—keeps the script organized and easier to maintain.
Step-by-Step Tutorial
1. Preparation
Before diving into the commands, make sure you have:
- PowerShell: Version 2 or later (though newer versions are recommended).
- User Permissions: You’ll need a user account with permission to write to
HKCU:\Software
. Since DPAPI ties encryption to your user profile, you should run all commands under the same user to ensure decryption works later. - Script Access: Have the PowerShell script containing these functions (e.g.,
Save-Secret
,Get-Secret
,Get-Otp
) imported or in your working directory.
2. Saving a Secret
Use Save-Secret
to encrypt and store your TOTP secret in the registry:
Save-Secret -Name "MyTOTPSecret" -Secret "MY_SECRET_VALUE"
- The
-Name
parameter is the identifier for the secret. - The
-Secret
parameter is your actual Base32-encoded TOTP key (or any secret string). - Internally,
Save-Secret
encrypts your secret using DPAPI, encodes it in Base64, and writes it toHKCU:\Software\TOTPSecrets
.
3. Verifying the Registry
After saving a secret, you can confirm it’s there by checking the registry path:
Get-ItemProperty -Path "HKCU:\Software\TOTPSecrets" -Name "MyTOTPSecret"
- You should see a property named
MyTOTPSecret
with a Base64-encoded string as its value. - Because it’s encrypted specifically for your user account, it won’t be readable in plain text.
4. Retrieving the Secret
To retrieve and decrypt the value, run Get-Secret
:
$retrievedSecret = Get-Secret -Name "MyTOTPSecret"
Write-Host "Retrieved Secret: $retrievedSecret"
Get-Secret
looks up the encrypted data, decodes it from Base64, and uses DPAPI to convert it back to the original plain-text value.- If all goes well, you’ll see your secret key (e.g., “MY_SECRET_VALUE”) output to the console.
5. Generating a TOTP
With your secret securely stored, you can now generate time-based one-time passwords using Get-Otp
:
$otp = Get-Otp -Name "MyTOTPSecret" -LENGTH 6 -WINDOW 30
Write-Host "Your OTP is: $otp"
-Name
: Matches the secret you saved earlier.-LENGTH
: The number of digits in the resulting OTP (commonly 6).-WINDOW
: The time step (in seconds) for the TOTP code to remain valid, typically 30 seconds.
Get-Otp
automatically:
- Calls
Get-Secret
to fetch and decrypt the key. - Calculates the current time window.
- Generates an HMAC-SHA1 hash of the time and key.
- Extracts the code using dynamic truncation.
- Formats the code to your specified digit length.
6. Testing
To confirm your code is correct:
- Phone-Based Authenticator: Some apps (e.g., Authy, Google Authenticator) allow you to add a custom TOTP token. Input the same Base32 key there and see if the codes match what
Get-Otp
generates. - TOTP Verification Tool: There are online testers where you can paste your Base32 secret and compare the generated code with what PowerShell returns.
If everything matches, congratulations—you have a working PowerShell-based TOTP solution!
Practical Use Cases
Local Development: Securely Store Tokens for Local Scripts
When creating or testing scripts on your local machine, you might need API keys, database credentials, or other secrets to run various tasks. Instead of embedding those credentials in your code, you can store them using Save-Secret
and retrieve them on demand. This keeps your local scripts clean and your sensitive data protected—handy if you ever need to share your script, push it to source control, or collaborate with others.
Automation: Incorporate the Script into CI/CD Workflows Requiring Short-Lived Tokens
Modern CI/CD pipelines often require short-lived tokens or secrets to deploy code, run tests, or perform automated tasks. By integrating Save-Secret
and Get-Otp
into your pipeline scripts, you can ensure your tokens are safely managed and only retrieved when needed. For instance, if you have a pipeline step that authenticates with a service using TOTP, you can schedule a PowerShell command to generate the code right before the service call. This streamlines your automation process while maintaining a high level of security.
Educational: Teach Colleagues About Secure Secret Management and TOTP
This project provides an excellent teaching tool for anyone looking to learn about PowerShell security practices, two-factor authentication, and how to integrate best practices into real-world scripts. Walk team members through the steps of saving, retrieving, and generating TOTP codes to give them hands-on experience with data protection in Windows. You’ll be equipping them with skills that are directly applicable to safer coding patterns in both production and personal projects.
Troubleshooting & Tips
User Context: Ensuring Both Saving and Retrieving Are Done Under the Same User
One of the most common stumbling blocks when working with DPAPI is that it’s user-specific. If you save the secret under your personal account but then try to retrieve it as a different user—or via a system account—the decryption step will fail. Make sure to run both Save-Secret
and Get-Secret
as the same user. This applies to automated tasks as well; scheduled scripts should be set to run under the original account used to create the secret.
Registry Path: Checking the Registry Key Name for Typos or Mismatches
Simple typos in the -Name
parameter or registry path can prevent you from finding the stored secret. Double-check you’re using the correct spelling and capitalization for the name of your secret. If you’re troubleshooting, you can list out the properties at HKCU:\Software\TOTPSecrets
:
Get-ItemProperty -Path HKCU:\Software\TOTPSecrets
Look for the key name you expect. If it’s not there or you notice a typo, you’ll know what needs fixing.
DPAPI Issues: Verifying PowerShell Version and Making Sure Encryption/Decryption Works as Expected
For DPAPI to work properly:
-
PowerShell Version: Although version 2 or above is technically enough, newer versions are more reliable and have better security practices. It’s worth upgrading if you’re on an older release.
-
Testing Encryption/Decryption: You can do a quick test with a known string to confirm that your environment is correctly converting to a secure string and back again. For instance:
$test = "HelloWorld"
$secure = ConvertTo-SecureString $test -AsPlainText -Force
$encryptedString = ConvertFrom-SecureString $secure
$secureAgain = ConvertTo-SecureString $encryptedString
$final = [System.Net.NetworkCredential]::new("", $secureAgain).Password
Write-Host "Original: $test | Final: $final"
If the Original and Final match, your system is good to go. If not, there may be user permissions or environment policies interfering with DPAPI.
Conclusion
Throughout this guide, we explored the step-by-step creation of a PowerShell-based TOTP solution with secure secret storage. By leveraging DPAPI to encrypt secrets in the Windows Registry, the script ensures that your sensitive credentials remain protected. We also demonstrated how to generate time-based one-time passwords using HMAC-SHA1, handle necessary conversions, and troubleshoot common pitfalls. Together, these pieces make it straightforward to integrate strong, time-sensitive authentication into everyday scripts or larger workflows.
Next Steps
If you’re interested in extending the functionality, there are plenty of ways to build on this foundation. Consider:
- Alternative Hash Algorithms: Supporting HMAC-SHA256 or HMAC-SHA512 for enhanced security.
- Graphical User Interface (GUI): Creating a simple window or tray application that generates codes on demand, no command line required.
- Cross-Platform Adaptations: Investigating non-Windows approaches or containerized solutions that store secrets securely on different operating systems.
Encouragement
Feel free to experiment, fork the code, or tailor it to your specific needs. Every environment is unique, and that’s the beauty of a modular script—you can customize it as you see fit. If you make improvements or discover new techniques, consider sharing them back with others. After all, strong security practices benefit everyone, and collaborative learning fuels innovation.
References
Code Listing
A complete version of this script is available in a GitHub repository for easy reference and collaboration.
- Full Script: GitHub Link
Links
- RFC 6238: TOTP: Time-Based One-Time Password Algorithm
- RFC 4226: HOTP: HMAC-Based One-Time Password Algorithm
- PowerShell Security Documentation: Explore official Microsoft docs on securing scripts and using DPAPI for more insights.