【问题标题】:Secure way to store password in Windows在 Windows 中存储密码的安全方法
【发布时间】:2012-10-30 18:07:54
【问题描述】:

我正在尝试保护包含敏感信息的本地数据库(类似于this question,仅适用于delphi 2010)

我正在使用DISQLite component,它确实支持 AES 加密,但我仍然需要保护我用来解密和读取数据库的密码。

我最初的想法是生成一个随机密码,使用 DPAPI(Crypt32.dll 中的CryptProtectData 和 CryptUnprotectData 函数)之类的东西存储它,但我找不到任何 Delphi 的示例

我的问题是:如何安全地存储随机生成的密码?或者,假设 DPAPI 路径是安全的,我该如何在 Delphi 中实现这个 DPAPI?

【问题讨论】:

  • 像往常一样,JEDI 库在 JwaWinCrypt.pas 单元中包含 Windows Cryptography API 接口的包装器。
  • 加密密码将如何保护任何东西?攻击者肯定可以从内存中读取解密后的密码吗?
  • @DavidHeffernan 那么从应用程序中解密任何内容没有意义吗? :-(
  • @TheDude 是的,让那些愚蠢的脚本小子生活得更艰难! :-D
  • @Glenn1234:不幸的是,我必须拥有真实密码才能打开数据库!哈希不会削减它:(

标签: delphi encryption delphi-2010 password-protection


【解决方案1】:

最好使用 Windows 的DPAPI。它比使用其他方法更安全:

  • CryptProtectData / CryptProtectMemory
  • CryptUnprotectData / CryptUnprotectMemory

CryptProtectMemory / CryptUnprotectMemory 提供更大的灵活性:

  • CRYPTPROTECTMEMORY_SAME_PROCESS:只有您的进程才能解密您的数据
  • CRYPTPROTECTMEMORY_CROSS_PROCESS:任何进程都可以解密您的数据
  • CRYPTPROTECTMEMORY_SAME_LOGON:只有在同一用户和同一会话中运行的进程才能解密数据

优点:

  1. 不需要钥匙 - Windows do it for you
  2. 精细控制:每个进程/每个会话/每个登录/每个机器
  3. CryptProtectData 存在于 Windows 2000 和更新版本中
  4. DPAPI Windows 比使用你、我和相信 Random() 返回绝对随机数的人编写的“安全”相关代码更安全 :) 事实上,微软在安全领域拥有数十年的经验,受到的攻击最多操作系统:o)

缺点:

  1. 在 CRYPTPROTECTMEMORY_SAME_PROCESS 的情况下,One* 可以在您的进程中注入一个新线程,该线程可以解密您的数据
  2. 如果有人重置用户密码(不更改),您将无法解密您的数据
  3. 在 CRYPTPROTECTMEMORY_SAME_LOGON 的情况下:如果用户*运行被黑进程,它可以解密您的数据
  4. 如果您使用 CRYPTPROTECT_LOCAL_MACHINE - 该机器上的每个用户* 都可以解密数据。 This is why it's not recommended to save passwords in .RDP files
  5. Known issues

注意:“每个用户”是指拥有使用 DPAPI 的工具或技能的用户

无论如何 - 你有一个选择。

请注意,@David-Heffernan 是对的 - 存储在计算机上的任何内容都可以解密 - 从内存中读取,在进程中注入线程等。

另一方面......我们为什么不让饼干的生活更艰难呢? :)

经验法则:使用后清除所有包含敏感数据的缓冲区。这不会让事情变得超级安全,但会降低您的内存包含敏感数据的可能性。 当然这并不能解决另一个主要问题:其他 Delphi 组件如何处理您传递给它们的敏感数据 :)

Security Library by JEDI 具有面向对象的 DPAPI 方法。 JEDI 项目还包含用于 DPAPI (JWA IIRC) 的已翻译 Windows 标头

更新:这是使用 DPAPI 的示例代码(使用 JEDI API):

Uses SysUtils, jwaWinCrypt, jwaWinBase, jwaWinType;

function dpApiProtectData(var fpDataIn: tBytes): tBytes;
var
  dataIn,               // Input buffer (clear-text/data)
  dataOut: DATA_BLOB;   // Output buffer (encrypted)
begin
  // Initializing variables
  dataOut.cbData := 0;
  dataOut.pbData := nil;

  dataIn.cbData := length(fpDataIn); // How much data (in bytes) we want to encrypt
  dataIn.pbData := @fpDataIn[0];     // Pointer to the data itself - the address of the first element of the input byte array

  if not CryptProtectData(@dataIn, nil, nil, nil, nil, 0, @dataOut) then
    RaiseLastOSError; // Bad things happen sometimes

  // Copy the encrypted bytes to RESULT variable
  setLength(result, dataOut.cbData);
  move(dataOut.pbData^, result[0], dataOut.cbData);
  LocalFree(HLOCAL(dataOut.pbData));                  // http://msdn.microsoft.com/en-us/library/windows/desktop/aa380261(v=vs.85).aspx
//  fillChar(fpDataIn[0], length(fpDataIn), #0);  // Eventually erase input buffer i.e. not to leave sensitive data in memory
end;

function dpApiUnprotectData(fpDataIn: tBytes): tBytes;
var
  dataIn,               // Input buffer (clear-text/data)
  dataOut: DATA_BLOB;   // Output buffer (encrypted)
begin
  dataOut.cbData := 0;
  dataOut.pbData := nil;

  dataIn.cbData := length(fpDataIn);
  dataIn.pbData := @fpDataIn[0];

  if not CryptUnprotectData(
    @dataIn,  
    nil, 
    nil, 
    nil, 
    nil, 
    0,         // Possible flags: http://msdn.microsoft.com/en-us/library/windows/desktop/aa380261%28v=vs.85%29.aspx 
               // 0 (zero) means only the user that encrypted the data will be able to decrypt it
    @dataOut
  ) then
    RaiseLastOSError;

  setLength(result, dataOut.cbData);                  // Copy decrypted bytes in the RESULT variable
  move(dataOut.pbData^, result[0], dataOut.cbData);   
  LocalFree(HLOCAL(dataOut.pbData));                  // http://msdn.microsoft.com/en-us/library/windows/desktop/aa380882%28v=vs.85%29.aspx
end;

procedure testDpApi;
var
  bytesClearTextIn,       // Holds input bytes
  bytesClearTextOut,      // Holds output bytes
  bytesEncrypted: tBytes; // Holds the resulting encrypted bytes
  strIn, strOut: string;  // Input / Output strings
begin

  // *** ENCRYPT STRING TO BYTE ARRAY
  strIn := 'Some Secret Data Here';

  // Copy string contents to bytesClearTextIn
  // NB: this works for STRING type only!!! (AnsiString / UnicodeString)
  setLength(bytesClearTextIn, length(strIn) * sizeOf(char));
  move(strIn[1], bytesClearTextIn[0], length(strIn) * sizeOf(char));

  bytesEncrypted := dpApiProtectData(bytesClearTextIn);     // Encrypt data

  // *** DECRYPT BYTE ARRAY TO STRING
  bytesClearTextOut := dpApiUnprotectData(bytesEncrypted);  // Decrypt data

  // Copy decrypted bytes (bytesClearTextOut) to the output string variable
  // NB: this works for STRING type only!!! (AnsiString / UnicodeString)    
  setLength(strOut, length(bytesClearTextOut) div sizeOf(char));
  move(bytesClearTextOut[0], strOut[1], length(bytesClearTextOut));

  assert(strOut = strIn, 'Boom!');  // Boom should never booom :)

end;

注意事项:

  • 示例是使用 CryptProtectData / CryptUnprotectData 的轻量级版本;
  • 加密是面向字节的,因此更容易使用 tBytes(tBytes = 字节数组);
  • 如果输入输出字符串为UTF8String,则去掉“* sizeOf(char)”,因为UTF8String的char只有1个字节
  • CryptProtectMemory/CryptUnProtectMemory的用法类似

【讨论】:

  • 警告词:CryptProtectMemory 是确定性的,这意味着它容易受到频率分析的影响。 (即,如果您在内存中的多个位置对相同的数据进行加密,它们将产生相同的密文,因此攻击者将能够判断它们是同一事物,这可能会泄露信息。)
【解决方案2】:

如果您的问题只是让用户不必每次都输入密码,您应该知道 Windows 已经有密码存储系统。

如果您转到控制面板 -> 凭据管理器。从那里您正在寻找 Windows 凭据 -> 通用凭据。

从那里您可以看到存储远程桌面密码之类的东西的位置相同:

公开此功能的 API 是 CredRead、CredWrite 和 CredDelete。

我将它们封装在三个函数中:

function CredReadGenericCredentials(const Target: UnicodeString; var Username, Password: UnicodeString): Boolean;
function CredWriteGenericCredentials(const Target, Username, Password: UnicodeString): Boolean;
function CredDeleteGenericCredentials(const Target: UnicodeString): Boolean;

目标是用来识别凭据的东西。我通常使用应用程序名称。

String target = ExtractFilename(ParamStr(0)); //e.g. 'Contoso.exe'

那么就很简单了:

CredWriteGenericCredentials(ExtractFilename(ParamStr(0)), username, password);

然后您可以在凭据管理器中看到它们:

当你想读回它们时:

CredReadGenericCredentials(ExtractFilename(ParamStr(0)), {var}username, {var}password);

还有一些额外的 UI 工作需要您完成:

  • 检测到没有存储凭据,并提示用户输入凭据
  • 检测到保存的用户名/密码无效并提示输入新的/正确的凭据,尝试连接并保存新的正确凭据

读取存储的凭据:

function CredReadGenericCredentials(const Target: UnicodeString; var Username, Password: UnicodeString): Boolean;
var
    credential: PCREDENTIALW;
    le: DWORD;
    s: string;
begin
    Result := False;

    credential := nil;
    if not CredReadW(Target, CRED_TYPE_GENERIC, 0, {var}credential) then
    begin
        le := GetLastError;
        s := 'Could not get "'+Target+'" generic credentials: '+SysErrorMessage(le)+' '+IntToStr(le);
        OutputDebugString(PChar(s));
        Exit;
    end;

    try
        username := Credential.UserName;
        password := WideCharToWideString(PWideChar(Credential.CredentialBlob), Credential.CredentialBlobSize div 2); //By convention blobs that contain strings do not have a trailing NULL.
    finally
        CredFree(Credential);
    end;

    Result := True;
end;

编写存储的凭据:

function CredWriteGenericCredentials(const Target, Username, Password: UnicodeString): Boolean;
var
    persistType: DWORD;
    Credentials: CREDENTIALW;
    le: DWORD;
    s: string;
begin
    ZeroMemory(@Credentials, SizeOf(Credentials));
    Credentials.TargetName := PWideChar(Target); //cannot be longer than CRED_MAX_GENERIC_TARGET_NAME_LENGTH (32767) characters. Recommended format "Company_Target"
    Credentials.Type_ := CRED_TYPE_GENERIC;
    Credentials.UserName := PWideChar(Username);
    Credentials.Persist := CRED_PERSIST_LOCAL_MACHINE;
    Credentials.CredentialBlob := PByte(Password);
    Credentials.CredentialBlobSize := 2*(Length(Password)); //By convention no trailing null. Cannot be longer than CRED_MAX_CREDENTIAL_BLOB_SIZE (512) bytes
    Credentials.UserName := PWideChar(Username);
    Result := CredWriteW(Credentials, 0);
    end;
end;

然后删除:

function CredDeleteGenericCredentials(const Target: UnicodeString): Boolean;
begin
    Result := CredDelete(Target, CRED_TYPE_GENERIC);
end;

CredRead 是 CryptProtectData 的包装器

需要注意的是,CredWrite/CredRead 内部使用CryptProtectData。

  • 它还选择将凭据存储在您的某个地方
  • 它还提供了一个 UI 供用户查看、管理,甚至手动输入和更改保存的凭据

您自己使用CryptProtectData 的不同之处在于您只获得了一个blob。您可以将其存储在某个地方,然后再检索它。

在存储密码时,这里有很好的 CryptProtectData 和 CryptUnprotectData 包装器:

function EncryptString(const Plaintext: UnicodeString; const AdditionalEntropy: UnicodeString): TBytes;
function DecryptString(const Blob: TBytes; const AdditionalEntropy: UnicodeString): UnicodeString;

这很容易使用:

procedure TForm1.TestStringEncryption;
var
    encryptedBlob: TBytes;
    plainText: UnicodeString;
const
    Salt = 'Salt doesn''t have to be secret; just different from the next application';
begin
    encryptedBlob := EncryptString('correct battery horse staple', Salt);

    plainText := DecryptString(encryptedBlob, salt);

    if plainText <> 'correct battery horse staple' then
        raise Exception.Create('String encryption self-test failed');
end;

真正的胆量是:

type
    DATA_BLOB = record
            cbData: DWORD;
            pbData: PByte;
    end;
    PDATA_BLOB = ^DATA_BLOB;

const
    CRYPTPROTECT_UI_FORBIDDEN = $1;

function CryptProtectData(const DataIn: DATA_BLOB; szDataDescr: PWideChar; OptionalEntropy: PDATA_BLOB; Reserved: Pointer; PromptStruct: Pointer{PCRYPTPROTECT_PROMPTSTRUCT}; dwFlags: DWORD; var DataOut: DATA_BLOB): BOOL; stdcall; external 'Crypt32.dll' name 'CryptProtectData';
function CryptUnprotectData(const DataIn: DATA_BLOB; szDataDescr: PPWideChar; OptionalEntropy: PDATA_BLOB; Reserved: Pointer; PromptStruct: Pointer{PCRYPTPROTECT_PROMPTSTRUCT}; dwFlags: DWORD; var DataOut: DATA_BLOB): Bool; stdcall; external 'Crypt32.dll' name 'CryptUnprotectData';

function EncryptString(const Plaintext: UnicodeString; const AdditionalEntropy: UnicodeString): TBytes;
var
    blobIn: DATA_BLOB;
    blobOut: DATA_BLOB;
    entropyBlob: DATA_BLOB;
    pEntropy: Pointer;
    bRes: Boolean;
begin
    blobIn.pbData := Pointer(PlainText);
    blobIn.cbData := Length(PlainText)*SizeOf(WideChar);

    if AdditionalEntropy <> '' then
    begin
        entropyBlob.pbData := Pointer(AdditionalEntropy);
        entropyBlob.cbData := Length(AdditionalEntropy)*SizeOf(WideChar);
        pEntropy := @entropyBlob;
    end
    else
        pEntropy := nil;

    bRes := CryptProtectData(
            blobIn,
            nil, //data description (PWideChar)
            pentropy, //optional entropy (PDATA_BLOB)
            nil, //reserved
            nil, //prompt struct
            CRYPTPROTECT_UI_FORBIDDEN, //flags
            {var}blobOut);
    if not bRes then
        RaiseLastOSError;

    //Move output blob into resulting TBytes
    SetLength(Result, blobOut.cbData);
    Move(blobOut.pbData^, Result[0], blobOut.cbData);

    // When you have finished using the DATA_BLOB structure, free its pbData member by calling the LocalFree function
    LocalFree(HLOCAL(blobOut.pbData));
end;

和解密:

function DecryptString(const blob: TBytes; const AdditionalEntropy: UnicodeString): UnicodeString;
var
    dataIn: DATA_BLOB;
    entropyBlob: DATA_BLOB;
    pentropy: PDATA_BLOB;
    dataOut: DATA_BLOB;
    bRes: BOOL;
begin
    dataIn.pbData := Pointer(blob);
    dataIn.cbData := Length(blob);

    if AdditionalEntropy <> '' then
    begin
        entropyBlob.pbData := Pointer(AdditionalEntropy);
        entropyBlob.cbData := Length(AdditionalEntropy)*SizeOf(WideChar);
        pentropy := @entropyBlob;
    end
    else
        pentropy := nil;

    bRes := CryptUnprotectData(
            DataIn,
            nil, //data description (PWideChar)
            pentropy, //optional entropy (PDATA_BLOB)
            nil, //reserved
            nil, //prompt struct
            CRYPTPROTECT_UI_FORBIDDEN,
            {var}dataOut);
    if not bRes then
        RaiseLastOSError;

    SetLength(Result, dataOut.cbData div 2);
    Move(dataOut.pbData^, Result[1], dataOut.cbData);
    LocalFree(HLOCAL(DataOut.pbData));
end;

【讨论】:

    【解决方案3】:

    好的,这是一个使用 TurboPower Lockbox(版本 2)的示例

      uses LbCipher, LbString;
    
      TaAES = class
      private
        Key: TKey256;
        FPassword: string;
      public
        constructor Create;
    
        function Code(AString: String): String;
        function Decode(AString: String): String;
    
        property Password: string read FPassword write FPassword;
      end;
    
    function TaAES.Code(AString: String): String;
    begin
      try
        RESULT := RDLEncryptStringCBCEx(AString, Key, SizeOf(Key), False);
      except
        RESULT := '';
      end;
    end;
    
    constructor TaAES.Create;
    begin
      GenerateLMDKey(Key, SizeOf(Key), Password);
    end;
    
    function TaAES.Decode(AString: String): String;
    begin
      RESULT := RDLEncryptStringCBCEx(AString, Key, SizeOf(Key), True);
    end;
    

    您可以将密码保存为应用程序中的变量。没有保存到文件示例,但您可以使用TFileStream 保存加密的(code)密码,然后使用decode 读取它:-)

    【讨论】:

    • 感谢 JustMe,但是我必须隐藏加密中使用的密钥,所以我有点回到第一方 :(
    • 不,你没有。密钥只是密钥加密的长度——在这个例子中它是 256 位 AES。并记住之前的@DavidHeffernan points
    • 谢谢 JustMe,我会看看 LockBox(我不确定他们如何在没有钥匙的情况下做到这一点……我得研究一下)
    • 您使用密码加密字符串。密码将是您在程序中的常量。
    • @TheDude:LockBox 生成密钥。请参阅上面 JustMe 答案中 TaAES.Create 中的代码 - 该行显示 GenerateLMDKey ;-)
    猜你喜欢
    • 2011-02-03
    • 1970-01-01
    • 2013-01-18
    • 2010-09-16
    • 1970-01-01
    • 1970-01-01
    • 2014-05-17
    • 2015-05-07
    • 1970-01-01
    相关资源
    最近更新 更多