DSInternals/Src/DSInternals.Common/Data/DPAPI/RoamedCredential.cs

233 lines
8.9 KiB
C#

namespace DSInternals.Common.Data
{
using System;
using System.IO;
using System.Security.Principal;
using System.Text;
using DSInternals.Common.Cryptography;
public class RoamedCredential : DPAPIObject
{
private const string MasterKeyCommandFormat = "dpapi::masterkey /in:\"{0}\" /sid:{1}";
private const string CapiKeyCommandFormat = "dpapi::capi /in:\"{0}\"";
private const string CNGKeyCommandFormat = "dpapi::cng /in:\"{0}\"";
private const string CertificateCommandFormat = "crypto::system /file:\"{0}\" /export";
private const string CurrentMasterKeyPointerId = "Preferred";
private const string CapiRSAKeyDirectoryFormat = @"Crypto\RSA\{0}\";
private const string CapiDSAKeyDirectoryFormat = @"Crypto\DSS\{0}\";
private const string CngKeyDirectory = @"Crypto\Keys\";
private const string CertificateDirectory = @"SystemCertificates\My\Certificates\";
private const string CertificateRequestDirectory = @"SystemCertificates\Request\Certificates\";
private const int MinLength = 132;
private const int IdentifierMaxSize = 93;
private const int HashSize = 20;
public RoamedCredential(byte[] blob, string accountName, SecurityIdentifier accountSid)
{
// Validate the input
Validator.AssertNotNull(blob, nameof(blob));
Validator.AssertMinLength(blob, MinLength, nameof(blob));
Validator.AssertNotNull(accountName, nameof(accountName));
Validator.AssertNotNull(accountSid, nameof(accountSid));
this.AccountName = accountName;
this.AccountSid = accountSid;
// Parse the blob
using (var stream = new MemoryStream(blob, false))
{
using (var reader = new BinaryReader(stream))
{
// The 1st byte is always '%'
char c1 = reader.ReadChar();
Validator.AssertEquals('%', c1, nameof(blob));
// The 2nd char encodes the type of the roamed credential
this.Type = (RoamedCredentialType)(reader.ReadChar() - '0');
// The 3rd char is always '\\'
char c3 = reader.ReadChar();
Validator.AssertEquals('\\', c3, nameof(blob));
// Now comes the identifier padded with zeros
var sb = new StringBuilder(IdentifierMaxSize);
for(int i = 0; i < IdentifierMaxSize; i++)
{
char currentChar = reader.ReadChar();
if(currentChar != Char.MinValue)
{
sb.Append(currentChar);
}
else
{
// We have reached the end of the string, so we skip the padding
stream.Seek(IdentifierMaxSize - i - 1, SeekOrigin.Current);
break;
}
}
this.Id = sb.ToString();
// Time of the last modification
long modifiedFileTime = reader.ReadInt64();
this.ModifiedTime = DateTime.FromFileTime(modifiedFileTime);
// Flags
this.Flags = (RoamedCredentialFlags)reader.ReadInt16();
// Size of additional data, typically 4B
short providerDataSize = reader.ReadInt16();
// SHA1 hash of the stored data
byte[] hash = reader.ReadBytes(HashSize);
// Data size
int dataSize = reader.ReadInt32();
// The actual roamed data
this.Data = reader.ReadBytes(dataSize);
// Note: The data structure might be corrupted and Data.Length can be less than dataSize.
if (this.Type == RoamedCredentialType.CNGPrivateKey && this.Data.Length > 0)
{
// Remove Software KSP NCRYPT_OPAQUETRANSPORT_BLOB header
this.Data = new CngSoftwareProviderTransportBlob(this.Data).KeyData;
}
if(providerDataSize > 0)
{
byte[] providerData = reader.ReadBytes(providerDataSize);
}
}
}
}
public RoamedCredentialType Type
{
get;
private set;
}
public RoamedCredentialFlags Flags
{
get;
private set;
}
public string Id
{
get;
private set;
}
public DateTime ModifiedTime
{
get;
private set;
}
public string AccountName
{
get;
private set;
}
public SecurityIdentifier AccountSid
{
get;
private set;
}
public override void Save(string directoryPath)
{
// The target directory must exist
Validator.AssertDirectoryExists(directoryPath);
string fullFilePath = Path.Combine(directoryPath, this.FilePath);
// Some blobs need to be saved in a specific folder structure that we have to create first
Directory.CreateDirectory(Path.GetDirectoryName(fullFilePath));
// Finally, we can save the file
File.WriteAllBytes(fullFilePath, this.Data);
}
/// <summary>
/// Gets the path to the credential file.
/// </summary>
public override string FilePath
{
get
{
var sb = new StringBuilder();
// Common base path in user's profile (we are skipping AppData\Roaming\Microsoft\)
sb.AppendFormat(@"{0}\", this.AccountName);
// Select the type-specific folder
switch(this.Type)
{
case RoamedCredentialType.DPAPIMasterKey:
sb.AppendFormat(@"Protect\{0}\", this.AccountSid);
break;
case RoamedCredentialType.CryptoApiCertificate:
case RoamedCredentialType.CNGCertificate:
sb.Append(CertificateDirectory);
break;
case RoamedCredentialType.CryptoApiRequest:
case RoamedCredentialType.CNGRequest:
sb.Append(CertificateRequestDirectory);
break;
case RoamedCredentialType.RSAPrivateKey:
sb.AppendFormat(CapiRSAKeyDirectoryFormat, this.AccountSid);
break;
case RoamedCredentialType.DSAPrivateKey:
sb.AppendFormat(CapiDSAKeyDirectoryFormat, this.AccountSid);
break;
case RoamedCredentialType.CNGPrivateKey:
sb.Append(CngKeyDirectory);
break;
default:
// Unknown type
break;
}
// The identifier of the blob is also its file name. Any invalid characters must be remved first.
string fileName = string.Concat(this.Id.Split(Path.GetInvalidFileNameChars()));
sb.Append(fileName);
return sb.ToString();
}
}
public override string KiwiCommand
{
get
{
switch(this.Type)
{
case RoamedCredentialType.DPAPIMasterKey:
return this.Id != CurrentMasterKeyPointerId ? String.Format(MasterKeyCommandFormat, this.FilePath, this.AccountSid) : null;
case RoamedCredentialType.CNGCertificate:
case RoamedCredentialType.CNGRequest:
case RoamedCredentialType.CryptoApiCertificate:
case RoamedCredentialType.CryptoApiRequest:
return String.Format(CertificateCommandFormat, this.FilePath);
case RoamedCredentialType.RSAPrivateKey:
case RoamedCredentialType.DSAPrivateKey:
return String.Format(CapiKeyCommandFormat, this.FilePath);
case RoamedCredentialType.CNGPrivateKey:
return String.Format(CNGKeyCommandFormat, this.FilePath);
default:
// Unknown/future credential type
return null;
}
}
}
public override string ToString()
{
return String.Format("{0}: {1}", this.Type, this.FilePath);
}
}
}