Безопасный способ хранения пароля в Windows
Я пытаюсь защитить локальную базу , которая содержит конфиденциальную информацию (похожую на этот вопрос, только для delphi 2010)
Я использую компонент DISQLite, который поддерживает шифрование AES, но мне все же нужно защитить этот пароль, который я использую для дешифрования и чтения базы данных.
Моя первоначальная идея состояла в том, чтобы сгенерировать случайный пароль, сохранить его, используя что-то вроде DPAPI (CryptProtectData
и CryptUnprotectData
функций, найденных в Crypt32.dll), но я не смог найти ни одного примера для Delphi
Мой вопрос: как я могу безопасно хранить случайно сгенерированный пароль? Или, предполагая, что дорога DPAPI безопасна, как я могу реализовать этот DPAPI в Delphi?
Ответы
Ответ 1
Лучше использовать Windows DPAPI. Это гораздо безопаснее, чем использование других методов:
- CryptProtectData/CryptProtectMemory
- CryptUnprotectData/CryptUnprotectMemory
CryptProtectMemory/CryptUnprotectMemory предлагают большую гибкость:
- CRYPTPROTECTMEMORY_SAME_PROCESS: только ваш процесс может расшифровать ваши данные.
- CRYPTPROTECTMEMORY_CROSS_PROCESS: любой процесс может dectypt ваши данные
- CRYPTPROTECTMEMORY_SAME_LOGON: только процессы, запущенные с одним и тем же пользователем, и в одном сеансе могут дешифровать данные.
Плюсы:
- Не нужно иметь ключ - Windows делает это за вас
- Гранулированное управление: за процесс/за сеанс/за вход/на машину
- CryptProtectData существует в Windows 2000 и более поздней версии
- DPAPI Windows более безопасна, чем использование кода безопасности, написанного от вас, меня и людей, которые считают, что Random() возвращает абсолютно случайное число:) На самом деле у Microsoft есть многолетний опыт работы в области безопасности, наиболее подверженный атаке OS: o)
Минусы:
- В случае CRYPTPROTECTMEMORY_SAME_PROCESS One * может просто вставить новый поток в ваш процесс, и этот поток может расшифровать ваши данные.
- Если кто-то reset пароль пользователя (не изменяется), вы не сможете расшифровать свои данные.
- В случае CRYPTPROTECTMEMORY_SAME_LOGON: если пользователь * запускает взломанный процесс, он может расшифровать ваши данные.
- Если вы используете CRYPTPROTECT_LOCAL_MACHINE - каждый пользователь * на этом компьютере может расшифровать данные. Вот почему не рекомендуется сохранять пароли в файлах .RDP
- Известные проблемы
Примечание. "каждый пользователь" - это пользователь, у которого есть инструменты или навыки для использования DPAPI
В любом случае - у вас есть выбор.
Обратите внимание, что @David-Heffernan прав - все, что хранится на компьютере, может быть расшифровано - чтение его из памяти, впрыскивание потоков в ваш процесс и т.д.
С другой стороны... почему бы нам не сделать жизнь крекера сложнее?:)
Правило большого пальца: очистите все буферы, содержащие конфиденциальные данные после их использования. Это не делает вещи супер безопасными, но уменьшает возможность хранения вашей памяти конфиденциальными данными.
Конечно, это не решает другой серьезной проблемы: как другие компоненты Delphi обрабатывают конфиденциальные данные, которые вы передаете им:)
Библиотека безопасности JEDI имеет объектно-ориентированный подход к DPAPI. Также проект JEDI содержит переведенные заголовки окон для DPAPI (JWA IIRC)
UPDATE: Здесь пример кода, который использует 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 похоже
Ответ 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);
Существует дополнительная часть работы пользовательского интерфейса, где вы должны:
- обнаружить, что не было сохраненных учетных данных, и запрашивать у пользователя учетные данные
- обнаружит, что сохраненное имя пользователя/пароль не работает и запрашивает новые/правильные учетные данные, попробуйте подключиться и сохранить новые правильные учетные данные
Чтение сохраненных учетных данных:
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 - это оболочка вокруг CryptProtectDatah2 >
Следует отметить, что CredWrite/CredRead внутренне использует CryptProtectData
.
- Он также просто хочет сохранить учетные данные где-то для вас
- Он также предоставляет пользовательский интерфейс для просмотра, управления и даже ручного ввода и изменения сохраненных учетных данных
Различие с помощью 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
его для чтения: -)