|
@@ -0,0 +1,409 @@
|
|
|
|
+unit ECDSA;
|
|
|
|
+
|
|
|
|
+{
|
|
|
|
+ Inno Setup
|
|
|
|
+ Copyright (C) 1997-2025 Jordan Russell
|
|
|
|
+ Portions by Martijn Laan
|
|
|
|
+ For conditions of distribution and use, see LICENSE.TXT.
|
|
|
|
+
|
|
|
|
+ ECDSA-P256 signing, verification, and key generation, based on CNG (BCrypt)
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+interface
|
|
|
|
+
|
|
|
|
+uses
|
|
|
|
+ Windows, SysUtils;
|
|
|
|
+
|
|
|
|
+type
|
|
|
|
+ TECDSAInt256 = array[0..31] of Byte;
|
|
|
|
+ TECDSAPublicKey = packed record
|
|
|
|
+ Public_x: TECDSAInt256;
|
|
|
|
+ Public_y: TECDSAInt256;
|
|
|
|
+ procedure Clear;
|
|
|
|
+ end;
|
|
|
|
+ TECDSAPrivateKey = packed record
|
|
|
|
+ PublicKey: TECDSAPublicKey;
|
|
|
|
+ Private_d: TECDSAInt256;
|
|
|
|
+ procedure Clear;
|
|
|
|
+ end;
|
|
|
|
+ TECDSASignature = packed record
|
|
|
|
+ Sig_r: TECDSAInt256;
|
|
|
|
+ Sig_s: TECDSAInt256;
|
|
|
|
+ procedure Clear;
|
|
|
|
+ end;
|
|
|
|
+
|
|
|
|
+ TECDSAKey = class
|
|
|
|
+ private
|
|
|
|
+ FAlgorithmHandle: THandle; { BCRYPT_ALG_HANDLE }
|
|
|
|
+ FKeyHandle: THandle; { BCRYPT_KEY_HANDLE }
|
|
|
|
+ class procedure CheckStatus(const AFunctionName: String;
|
|
|
|
+ const AStatus: NTSTATUS); static;
|
|
|
|
+ procedure KeyHandleRequired;
|
|
|
|
+ public
|
|
|
|
+ constructor Create;
|
|
|
|
+ destructor Destroy; override;
|
|
|
|
+ procedure DestroyKey;
|
|
|
|
+ procedure ExportPrivateKey(out APrivateKey: TECDSAPrivateKey);
|
|
|
|
+ procedure ExportPublicKey(out APublicKey: TECDSAPublicKey);
|
|
|
|
+ procedure GenerateKeyPair;
|
|
|
|
+ procedure ImportPrivateKey([ref] const APrivateKey: TECDSAPrivateKey);
|
|
|
|
+ procedure ImportPublicKey([ref] const APublicKey: TECDSAPublicKey);
|
|
|
|
+ procedure SignHash(const AHash: array of Byte;
|
|
|
|
+ out ASignature: TECDSASignature);
|
|
|
|
+ function VerifySignature(const AHash: array of Byte;
|
|
|
|
+ const ASignature: TECDSASignature): Boolean;
|
|
|
|
+ end;
|
|
|
|
+
|
|
|
|
+ EECDSAError = class(Exception);
|
|
|
|
+
|
|
|
|
+implementation
|
|
|
|
+
|
|
|
|
+type
|
|
|
|
+ BCRYPT_ALG_HANDLE = type THandle;
|
|
|
|
+ BCRYPT_KEY_HANDLE = type THandle;
|
|
|
|
+
|
|
|
|
+ BCRYPT_ECCKEY_BLOB = record
|
|
|
|
+ dwMagic: ULONG;
|
|
|
|
+ cbKey: ULONG;
|
|
|
|
+ end;
|
|
|
|
+
|
|
|
|
+const
|
|
|
|
+ BCRYPT_ECDSA_P256_ALGORITHM = 'ECDSA_P256';
|
|
|
|
+ BCRYPT_ECCPRIVATE_BLOB = 'ECCPRIVATEBLOB';
|
|
|
|
+ BCRYPT_ECCPUBLIC_BLOB = 'ECCPUBLICBLOB';
|
|
|
|
+ BCRYPT_ECDSA_PRIVATE_P256_MAGIC = $32534345;
|
|
|
|
+ BCRYPT_ECDSA_PUBLIC_P256_MAGIC = $31534345;
|
|
|
|
+
|
|
|
|
+ STATUS_INVALID_SIGNATURE = NTSTATUS($C000A000);
|
|
|
|
+
|
|
|
|
+var
|
|
|
|
+ BCryptFunctionsInitialized: BOOL;
|
|
|
|
+
|
|
|
|
+ BCryptCloseAlgorithmProvider: function(hAlgorithm: BCRYPT_ALG_HANDLE;
|
|
|
|
+ dwFlags: ULONG): NTSTATUS;
|
|
|
|
+ stdcall;
|
|
|
|
+
|
|
|
|
+ BCryptDestroyKey: function(hKey: BCRYPT_KEY_HANDLE): NTSTATUS;
|
|
|
|
+ stdcall;
|
|
|
|
+
|
|
|
|
+ BCryptExportKey: function(hKey: BCRYPT_KEY_HANDLE; hExportKey: BCRYPT_KEY_HANDLE;
|
|
|
|
+ pszBlobType: LPCWSTR; var pbOutput; cbOutput: ULONG; out pcbResult: ULONG;
|
|
|
|
+ dwFlags: ULONG): NTSTATUS;
|
|
|
|
+ stdcall;
|
|
|
|
+
|
|
|
|
+ BCryptFinalizeKeyPair: function(hKey: BCRYPT_KEY_HANDLE; dwFlags: ULONG): NTSTATUS;
|
|
|
|
+ stdcall;
|
|
|
|
+
|
|
|
|
+ BCryptGenerateKeyPair: function(hAlgorithm: BCRYPT_ALG_HANDLE;
|
|
|
|
+ out phKey: BCRYPT_KEY_HANDLE; dwLength: ULONG; dwFlags: ULONG): NTSTATUS;
|
|
|
|
+ stdcall;
|
|
|
|
+
|
|
|
|
+ BCryptImportKeyPair: function(hAlgorithm: BCRYPT_ALG_HANDLE;
|
|
|
|
+ hImportKey: BCRYPT_KEY_HANDLE; pszBlobType: LPCWSTR;
|
|
|
|
+ out phKey: BCRYPT_KEY_HANDLE; const pbInput; cbInput: ULONG;
|
|
|
|
+ dwFlags: ULONG): NTSTATUS;
|
|
|
|
+ stdcall;
|
|
|
|
+
|
|
|
|
+ BCryptOpenAlgorithmProvider: function(out phAlgorithm: BCRYPT_ALG_HANDLE;
|
|
|
|
+ pszAlgId: LPCWSTR; pszImplementation: LPCWSTR; dwFlags: ULONG): NTSTATUS;
|
|
|
|
+ stdcall;
|
|
|
|
+
|
|
|
|
+ BCryptSignHash: function(hKey: BCRYPT_KEY_HANDLE; pPaddingInfo: Pointer;
|
|
|
|
+ const pbInput; cbInput: ULONG; var pbOutput; cbOutput: ULONG;
|
|
|
|
+ out pcbResult: ULONG; dwFlags: ULONG): NTSTATUS;
|
|
|
|
+ stdcall;
|
|
|
|
+
|
|
|
|
+ BCryptVerifySignature: function(hKey: BCRYPT_KEY_HANDLE; pPaddingInfo: Pointer;
|
|
|
|
+ const pbHash; cbHash: ULONG; const pbSignature; cbSignature: ULONG;
|
|
|
|
+ dwFlags: ULONG): NTSTATUS;
|
|
|
|
+ stdcall;
|
|
|
|
+
|
|
|
|
+type
|
|
|
|
+ { ECDSA-P256 key blob formats specific to BCrypt }
|
|
|
|
+ TBCryptPrivateKeyBlob = record
|
|
|
|
+ Header: BCRYPT_ECCKEY_BLOB;
|
|
|
|
+ Public_x: TECDSAInt256;
|
|
|
|
+ Public_y: TECDSAInt256;
|
|
|
|
+ Private_d: TECDSAInt256;
|
|
|
|
+ procedure Clear;
|
|
|
|
+ end;
|
|
|
|
+ TBCryptPublicKeyBlob = record
|
|
|
|
+ Header: BCRYPT_ECCKEY_BLOB;
|
|
|
|
+ Public_x: TECDSAInt256;
|
|
|
|
+ Public_y: TECDSAInt256;
|
|
|
|
+ procedure Clear;
|
|
|
|
+ end;
|
|
|
|
+
|
|
|
|
+procedure InitBCryptFunctions;
|
|
|
|
+var
|
|
|
|
+ M: HMODULE;
|
|
|
|
+
|
|
|
|
+ procedure InitFunc(out AProc: Pointer; const AProcName: PAnsiChar);
|
|
|
|
+ begin
|
|
|
|
+ AProc := GetProcAddress(M, AProcName);
|
|
|
|
+ if not Assigned(AProc) then
|
|
|
|
+ raise EECDSAError.CreateFmt('Failed to get address of %s in bcrypt.dll',
|
|
|
|
+ [AProcName]);
|
|
|
|
+ end;
|
|
|
|
+
|
|
|
|
+var
|
|
|
|
+ SystemDir: array[0..MAX_PATH-1] of Char;
|
|
|
|
+begin
|
|
|
|
+ if BCryptFunctionsInitialized then begin
|
|
|
|
+ MemoryBarrier;
|
|
|
|
+ Exit;
|
|
|
|
+ end;
|
|
|
|
+
|
|
|
|
+ GetSystemDirectory(SystemDir, SizeOf(SystemDir) div SizeOf(SystemDir[0]));
|
|
|
|
+ M := LoadLibrary(PChar(SystemDir + '\bcrypt.dll'));
|
|
|
|
+ if M = 0 then
|
|
|
|
+ raise EECDSAError.Create('Failed to load bcrypt.dll');
|
|
|
|
+
|
|
|
|
+ InitFunc(@BCryptCloseAlgorithmProvider, 'BCryptCloseAlgorithmProvider');
|
|
|
|
+ InitFunc(@BCryptDestroyKey, 'BCryptDestroyKey');
|
|
|
|
+ InitFunc(@BCryptExportKey, 'BCryptExportKey');
|
|
|
|
+ InitFunc(@BCryptFinalizeKeyPair, 'BCryptFinalizeKeyPair');
|
|
|
|
+ InitFunc(@BCryptGenerateKeyPair, 'BCryptGenerateKeyPair');
|
|
|
|
+ InitFunc(@BCryptImportKeyPair, 'BCryptImportKeyPair');
|
|
|
|
+ InitFunc(@BCryptOpenAlgorithmProvider, 'BCryptOpenAlgorithmProvider');
|
|
|
|
+ InitFunc(@BCryptSignHash, 'BCryptSignHash');
|
|
|
|
+ InitFunc(@BCryptVerifySignature, 'BCryptVerifySignature');
|
|
|
|
+
|
|
|
|
+ MemoryBarrier;
|
|
|
|
+ BCryptFunctionsInitialized := True;
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+{ TBCryptPrivateKeyBlob }
|
|
|
|
+
|
|
|
|
+procedure TBCryptPrivateKeyBlob.Clear;
|
|
|
|
+begin
|
|
|
|
+ FillChar(Self, SizeOf(Self), 0);
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+{ TBCryptPublicKeyBlob }
|
|
|
|
+
|
|
|
|
+procedure TBCryptPublicKeyBlob.Clear;
|
|
|
|
+begin
|
|
|
|
+ FillChar(Self, SizeOf(Self), 0);
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+{ TECDSAPublicKey }
|
|
|
|
+
|
|
|
|
+procedure TECDSAPublicKey.Clear;
|
|
|
|
+begin
|
|
|
|
+ FillChar(Self, SizeOf(Self), 0);
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+{ TECDSAPrivateKey }
|
|
|
|
+
|
|
|
|
+procedure TECDSAPrivateKey.Clear;
|
|
|
|
+begin
|
|
|
|
+ FillChar(Self, SizeOf(Self), 0);
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+{ TECDSASignature }
|
|
|
|
+
|
|
|
|
+procedure TECDSASignature.Clear;
|
|
|
|
+begin
|
|
|
|
+ FillChar(Self, SizeOf(Self), 0);
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+{ TECDSAKey }
|
|
|
|
+
|
|
|
|
+constructor TECDSAKey.Create;
|
|
|
|
+begin
|
|
|
|
+ inherited;
|
|
|
|
+ InitBCryptFunctions;
|
|
|
|
+ var LAlgorithmHandle: BCRYPT_ALG_HANDLE;
|
|
|
|
+ CheckStatus('BCryptOpenAlgorithmProvider',
|
|
|
|
+ BCryptOpenAlgorithmProvider(LAlgorithmHandle, BCRYPT_ECDSA_P256_ALGORITHM,
|
|
|
|
+ nil, 0));
|
|
|
|
+ FAlgorithmHandle := LAlgorithmHandle; { assign only on success }
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+destructor TECDSAKey.Destroy;
|
|
|
|
+begin
|
|
|
|
+ DestroyKey;
|
|
|
|
+ if FAlgorithmHandle <> 0 then
|
|
|
|
+ BCryptCloseAlgorithmProvider(FAlgorithmHandle, 0);
|
|
|
|
+ inherited;
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+class procedure TECDSAKey.CheckStatus(const AFunctionName: String;
|
|
|
|
+ const AStatus: NTSTATUS);
|
|
|
|
+begin
|
|
|
|
+ if AStatus <> 0 then
|
|
|
|
+ raise EECDSAError.CreateFmt('%s failed with error code 0x%x',
|
|
|
|
+ [AFunctionName, AStatus]);
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+procedure TECDSAKey.DestroyKey;
|
|
|
|
+begin
|
|
|
|
+ const H = FKeyHandle;
|
|
|
|
+ if H <> 0 then begin
|
|
|
|
+ FKeyHandle := 0;
|
|
|
|
+ BCryptDestroyKey(H);
|
|
|
|
+ end;
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+procedure TECDSAKey.ExportPrivateKey(out APrivateKey: TECDSAPrivateKey);
|
|
|
|
+begin
|
|
|
|
+ KeyHandleRequired;
|
|
|
|
+
|
|
|
|
+ var KeyBlob: TBCryptPrivateKeyBlob;
|
|
|
|
+ { Initially clear KeyBlob just to make it easier to verify that
|
|
|
|
+ BCryptExportKey overwrites the entire record }
|
|
|
|
+ KeyBlob.Clear;
|
|
|
|
+ try
|
|
|
|
+ var ResultSize: ULONG;
|
|
|
|
+ CheckStatus('BCryptExportKey',
|
|
|
|
+ BCryptExportKey(FKeyHandle, 0, BCRYPT_ECCPRIVATE_BLOB, KeyBlob,
|
|
|
|
+ SizeOf(KeyBlob), ResultSize, 0));
|
|
|
|
+
|
|
|
|
+ if ResultSize <> SizeOf(KeyBlob) then
|
|
|
|
+ raise EECDSAError.Create('BCryptExportKey result invalid (1)');
|
|
|
|
+ if KeyBlob.Header.dwMagic <> BCRYPT_ECDSA_PRIVATE_P256_MAGIC then
|
|
|
|
+ raise EECDSAError.Create('BCryptExportKey result invalid (2)');
|
|
|
|
+ if KeyBlob.Header.cbKey <> SizeOf(KeyBlob.Public_x) then
|
|
|
|
+ raise EECDSAError.Create('BCryptExportKey result invalid (3)');
|
|
|
|
+
|
|
|
|
+ APrivateKey.PublicKey.Public_x := KeyBlob.Public_x;
|
|
|
|
+ APrivateKey.PublicKey.Public_y := KeyBlob.Public_y;
|
|
|
|
+ APrivateKey.Private_d := KeyBlob.Private_d;
|
|
|
|
+ finally
|
|
|
|
+ { Security: don't leave copy of private key on the stack }
|
|
|
|
+ KeyBlob.Clear;
|
|
|
|
+ end;
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+procedure TECDSAKey.ExportPublicKey(out APublicKey: TECDSAPublicKey);
|
|
|
|
+begin
|
|
|
|
+ KeyHandleRequired;
|
|
|
|
+
|
|
|
|
+ var KeyBlob: TBCryptPublicKeyBlob;
|
|
|
|
+ { Initially clear KeyBlob just to make it easier to verify that
|
|
|
|
+ BCryptExportKey overwrites the entire record }
|
|
|
|
+ KeyBlob.Clear;
|
|
|
|
+ try
|
|
|
|
+ var ResultSize: ULONG;
|
|
|
|
+ CheckStatus('BCryptExportKey',
|
|
|
|
+ BCryptExportKey(FKeyHandle, 0, BCRYPT_ECCPUBLIC_BLOB, KeyBlob,
|
|
|
|
+ SizeOf(KeyBlob), ResultSize, 0));
|
|
|
|
+
|
|
|
|
+ if ResultSize <> SizeOf(KeyBlob) then
|
|
|
|
+ raise EECDSAError.Create('BCryptExportKey result invalid (1)');
|
|
|
|
+ if KeyBlob.Header.dwMagic <> BCRYPT_ECDSA_PUBLIC_P256_MAGIC then
|
|
|
|
+ raise EECDSAError.Create('BCryptExportKey result invalid (2)');
|
|
|
|
+ if KeyBlob.Header.cbKey <> SizeOf(KeyBlob.Public_x) then
|
|
|
|
+ raise EECDSAError.Create('BCryptExportKey result invalid (3)');
|
|
|
|
+
|
|
|
|
+ APublicKey.Public_x := KeyBlob.Public_x;
|
|
|
|
+ APublicKey.Public_y := KeyBlob.Public_y;
|
|
|
|
+ finally
|
|
|
|
+ { There's no private key, but clear anyway for consistency }
|
|
|
|
+ KeyBlob.Clear;
|
|
|
|
+ end;
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+procedure TECDSAKey.GenerateKeyPair;
|
|
|
|
+begin
|
|
|
|
+ DestroyKey;
|
|
|
|
+
|
|
|
|
+ var LKeyHandle: BCRYPT_KEY_HANDLE;
|
|
|
|
+ CheckStatus('BCryptGenerateKeyPair',
|
|
|
|
+ BCryptGenerateKeyPair(FAlgorithmHandle, LKeyHandle, 256, 0));
|
|
|
|
+ try
|
|
|
|
+ CheckStatus('BCryptFinalizeKeyPair',
|
|
|
|
+ BCryptFinalizeKeyPair(LKeyHandle, 0));
|
|
|
|
+ except
|
|
|
|
+ BCryptDestroyKey(LKeyHandle);
|
|
|
|
+ raise;
|
|
|
|
+ end;
|
|
|
|
+ FKeyHandle := LKeyHandle; { assign only on success }
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+procedure TECDSAKey.ImportPrivateKey([ref] const APrivateKey: TECDSAPrivateKey);
|
|
|
|
+begin
|
|
|
|
+ DestroyKey;
|
|
|
|
+
|
|
|
|
+ var KeyBlob: TBCryptPrivateKeyBlob;
|
|
|
|
+ try
|
|
|
|
+ KeyBlob.Header.dwMagic := BCRYPT_ECDSA_PRIVATE_P256_MAGIC;
|
|
|
|
+ KeyBlob.Header.cbKey := SizeOf(KeyBlob.Public_x);
|
|
|
|
+ KeyBlob.Public_x := APrivateKey.PublicKey.Public_x;
|
|
|
|
+ KeyBlob.Public_y := APrivateKey.PublicKey.Public_y;
|
|
|
|
+ KeyBlob.Private_d := APrivateKey.Private_d;
|
|
|
|
+
|
|
|
|
+ var LKeyHandle: BCRYPT_KEY_HANDLE;
|
|
|
|
+ CheckStatus('BCryptImportKeyPair',
|
|
|
|
+ BCryptImportKeyPair(FAlgorithmHandle, 0, BCRYPT_ECCPRIVATE_BLOB,
|
|
|
|
+ LKeyHandle, KeyBlob, SizeOf(KeyBlob), 0));
|
|
|
|
+ FKeyHandle := LKeyHandle; { assign only on success }
|
|
|
|
+ finally
|
|
|
|
+ { Security: don't leave copy of private key on the stack }
|
|
|
|
+ KeyBlob.Clear;
|
|
|
|
+ end;
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+procedure TECDSAKey.ImportPublicKey([ref] const APublicKey: TECDSAPublicKey);
|
|
|
|
+begin
|
|
|
|
+ DestroyKey;
|
|
|
|
+
|
|
|
|
+ var KeyBlob: TBCryptPublicKeyBlob;
|
|
|
|
+ try
|
|
|
|
+ KeyBlob.Header.dwMagic := BCRYPT_ECDSA_PUBLIC_P256_MAGIC;
|
|
|
|
+ KeyBlob.Header.cbKey := SizeOf(KeyBlob.Public_x);
|
|
|
|
+ KeyBlob.Public_x := APublicKey.Public_x;
|
|
|
|
+ KeyBlob.Public_y := APublicKey.Public_y;
|
|
|
|
+
|
|
|
|
+ var LKeyHandle: BCRYPT_KEY_HANDLE;
|
|
|
|
+ CheckStatus('BCryptImportKeyPair',
|
|
|
|
+ BCryptImportKeyPair(FAlgorithmHandle, 0, BCRYPT_ECCPUBLIC_BLOB,
|
|
|
|
+ LKeyHandle, KeyBlob, SizeOf(KeyBlob), 0));
|
|
|
|
+ FKeyHandle := LKeyHandle; { assign only on success }
|
|
|
|
+ finally
|
|
|
|
+ { There's no private key, but clear anyway for consistency }
|
|
|
|
+ KeyBlob.Clear;
|
|
|
|
+ end;
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+procedure TECDSAKey.KeyHandleRequired;
|
|
|
|
+begin
|
|
|
|
+ if FKeyHandle = 0 then
|
|
|
|
+ raise EECDSAError.Create('No key has been assigned');
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+procedure TECDSAKey.SignHash(const AHash: array of Byte;
|
|
|
|
+ out ASignature: TECDSASignature);
|
|
|
|
+begin
|
|
|
|
+ KeyHandleRequired;
|
|
|
|
+
|
|
|
|
+ { Initially clear ASignature just to make it easier to verify that
|
|
|
|
+ BCryptSignHash overwrites the entire record }
|
|
|
|
+ ASignature.Clear;
|
|
|
|
+
|
|
|
|
+ var ResultSize: ULONG;
|
|
|
|
+ CheckStatus('BCryptSignHash',
|
|
|
|
+ BCryptSignHash(FKeyHandle, nil, AHash[0], ULONG(Length(AHash)), ASignature,
|
|
|
|
+ SizeOf(ASignature), ResultSize, 0));
|
|
|
|
+
|
|
|
|
+ if ResultSize <> SizeOf(ASignature) then
|
|
|
|
+ raise EECDSAError.Create('BCryptSignHash result size invalid');
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+function TECDSAKey.VerifySignature(const AHash: array of Byte;
|
|
|
|
+ const ASignature: TECDSASignature): Boolean;
|
|
|
|
+begin
|
|
|
|
+ KeyHandleRequired;
|
|
|
|
+
|
|
|
|
+ const Status = BCryptVerifySignature(FKeyHandle, nil, AHash[0],
|
|
|
|
+ ULONG(Length(AHash)), ASignature, SizeOf(ASignature), 0);
|
|
|
|
+ if Status = STATUS_INVALID_SIGNATURE then
|
|
|
|
+ Result := False
|
|
|
|
+ else begin
|
|
|
|
+ CheckStatus('BCryptVerifySignature', Status);
|
|
|
|
+ Result := True;
|
|
|
|
+ end;
|
|
|
|
+end;
|
|
|
|
+
|
|
|
|
+end.
|