The first method has several advantages: your machine doesn’t need to be connected to a network to set the generated password and every administrator who has access to the implementation of the reverse algorithm can recover the password. The latter advantage is also the biggest issue: everyone able to guess or find the algorithm (e.g. by browsing the build share) can deduce the password of all machines. This process also violates Kerckhoffs's principle stipulating that only the key and not the algorithm should be kept secret.

The second method doesn’t rely on a secret algorithm, but introduces two major constraints: the machine and the central service where the randomly generated passwords are stored must be online and this service will likely become the target of a motivated attacker.

Finally, setting any password via GPPs is highly discouraged, as any domain user is able to decrypt these secrets.

Is there any other solution? I think there’s one, based on the following core ideas:

  • The whole setup should allow the algorithm to be public
  • Gaining access to the password storage location should not be considered as sufficient to recover the passwords
  • The element allowing a password recovery doesn’t absolutely need to be connected to a network

Based on this, I imagined a scheme based on public-key cryptography where a password gets randomly generated and then encrypted using the public key of a given certificate. The resulting blob can be stored wherever desired, as only the possession of the private key – held e.g. offline – will allow recovering the password.

This may sound hard to implement or manage, but I’m convinced it can be done fairly efficiently. Additionally, a password rollover could be organised via GPO every few months or so. As a proof of concept, a quick implementation of mine in .NET / C# that everyone (running Windows) can download, compile and test can be found below.

Have a look at this implementation and I’m looking forward to your feedback and critics regarding this other but in my opinion simple way to generate and store unique passwords.


Implementation PoC:

File SafePasswordGenerator.cs

using System;
using System.IO;
using System.Text;
using System.Diagnostics;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
class SafePasswordGenerator {
/*
Prototype for a safe password generation process for e.g. local Windows administrator accounts.
Created by Alexandre Herzog under a CC-BY-SA licence (Creative Commons Attribution + ShareAlike)
This work relies on two part:
1. This file which generates a safe password and encrypts it using the public key of a given certificate (see below)
2. File DecodePassword.cs which allows the decryption of the encrypted string
To save the password in an encrypted form, you just need the public key of the certificate saved in file pub_cert.cer
To decrypt the saved encrypted password, you need the private key of the certificate saved in file private_cert.pfx
A self-signed certificate can be generated using standard Windows tools:
1. Create template template.inf using the following (adapted) template
[NewRequest]
Subject = "CN=W2K8-BO-DC.contoso2.com"
RequestType = Cert
ExportableEncrypted = TRUE
KeyLength = 2048
KeySpec = AT_KEYEXCHANGE
2. Generate the certificate with command
certreq -new template.inf
3. Don't bother with the popup, the self-signed certificate is already in your store
(cf http://serverfault.com/questions/413976/can-you-generate-a-self-signed-certificate-on-windows-server-using-cli-tools-lik)
4. Export the certificate from your windows store (certmgr.msc - personal - certificate)
Compile this program with command
c:\Windows\Microsoft.NET\Framework\v2.0.50727\csc.exe SafePasswordGenerator.cs
Refs:
- http://technet.microsoft.com/en-us/library/dn296456.aspx (CertReq reference)
- http://msdn.microsoft.com/en-us/library/system.security.cryptography.rsacryptoserviceprovider.encrypt.aspx
- http://msdn.microsoft.com/en-us/library/system.security.cryptography.x509certificates.publickey(v=vs.110).aspx
*/
private static Encoding asciiEncoding = Encoding.ASCII;
// Definition of 64 possible characters
private static char[] possiblePasswordChars = new char[] {'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','0','1','2','3','4','5','6','7','8','9','-','_'};
private static RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
private static bool useOAEP = true; // RSA padding (should) avoid an attacker being able to brute force passwords and see if he gets the matching cipher text
// Certificate with only a public key, used to encrypt the password
private static X509Certificate2 cert = new X509Certificate2("pub_cert.cer");
// Text which will be encrypted: {0} is a placeholder for the machine name, {1} a placeholder for the password
const string savedText = "Password for machine '{0}': {1}";
public static void Main(string[] args) {
// Setting password to 16 chars to avoid any LM-hash related issue...
String password = GetRandomString(16);
string strToEncrypt = String.Format(savedText, Environment.MachineName, password);
string base64String = Convert.ToBase64String( Encrypt(strToEncrypt) );
Console.WriteLine(String.Format("String '{0}' gets encrypted to base64 representation '{1}'", strToEncrypt, base64String));
// Uncomment to test decryption
// Don't forget to hardcode the PIN for test purposes in method DecodeMe(byte[] b)
//Console.WriteLine("Decrypted value: {0}", DecodeMe(base64String));
/* Uncomment to store this information somewhere */
/*
// Stores the encrypted value on a network share
File.AppendAllText(
Path.Combine(@"\\network\share$\localAdminLog\", String.Format("machine_{0}.txt", Environment.MachineName)),
String.Format("{0}: {1}", DateTime.Now, base64String)
);
// Saves the password in a environment variable for this execution context
Environment.SetEnvironmentVariable("generatedPassword", password);
// Sets the given password for the local administrator user
Process.Start("cmd.exe", String.Format("net user administrator {0}", password));
*/
}
private static byte[] Encrypt(string text) {
// TODO/NEXTV check if we can rely on ECC!
RSACryptoServiceProvider rsa = (RSACryptoServiceProvider)cert.PublicKey.Key;
byte[] byteToEncrypt = asciiEncoding.GetBytes(text);
return rsa.Encrypt(byteToEncrypt, useOAEP);
}
private static string GetRandomString(int length) {
byte[] random = new byte[length];
rng.GetBytes(random); // The array is now filled with cryptographically strong random bytes
char[] randomStr = new char[length];
for (int i=0; i < length; i++)
randomStr[i] = possiblePasswordChars[random[i] % possiblePasswordChars.Length];
return new String(randomStr);
}
// Not needed for the encryption process itself.
private static string DecodeMe(string txt)
{
byte[] decrypted = DecodeMe(Convert.FromBase64String(txt));
return String.Format("Decrypted string is below:\n{0}", asciiEncoding.GetString(decrypted));
}
// Not needed for the encryption process itself.
private static byte[] DecodeMe(byte[] b)
{
X509Certificate2 privateCert = new X509Certificate2("private_cert.pfx", "");
RSACryptoServiceProvider rsa = (RSACryptoServiceProvider)privateCert.PrivateKey;
return rsa.Decrypt(b, useOAEP);
}
}

File DecodePassword.cs

using System;
using System.Text;
using Microsoft.VisualBasic;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
class DecodePassword {
/*
Prototype for a safe password generation process for e.g. local Windows administrator accounts.
Created by Alexandre Herzog under a CC-BY-SA licence (Creative Commons Attribution + ShareAlike)
This work relies on two part:
1. File SafePasswordGenerator.cs which generates a safe password and encrypts it using the public key of a given certificate (see below)
2. This file which allows the decryption of the encrypted string
Compile with
c:\Windows\Microsoft.NET\Framework\v2.0.50727\csc.exe DecodePassword.cs /reference:Microsoft.VisualBasic.dll
*/
private static bool useOAEP = true;
public static void Main(string[] args)
{
Encoding asciiEncoding = Encoding.ASCII;
// using VB's Interaction, as we all miss these nice looking GUI components!
String certPassword = Interaction.InputBox("Enter the password of your private certificate:", "DecryptPassword", "", -1, -1);
X509Certificate2 cert = new X509Certificate2("private_cert.pfx", certPassword);
// No Console.ReadLine as the copy-paste tends to be truncated...
String strToDecrypt = Interaction.InputBox("Enter the string to decrypt:", "DecryptPassword", "", -1, -1);
RSACryptoServiceProvider rsa = (RSACryptoServiceProvider)cert.PrivateKey;
byte[] decrypted = rsa.Decrypt(Convert.FromBase64String(strToDecrypt), useOAEP);
Console.WriteLine("Decrypted string is below:\n{0}", asciiEncoding.GetString(decrypted));
Console.ReadLine();
}
}
/*
Random string 'Password for machine '[...]': 34-PNxSstfSzZBAq' gets encoded to '
BT[...]lylhw=='
========================
Decrypted string is below:
Password for machine '[...]': 34-PNxSstfSzZBAq
c:\>DecodePassword.exe
Decrypted string is below:
Password for machine '[...]': 34-PNxSstfSzZBAq
*/