BSOne.SFC/Tocsg.Lib/VCL/Tocsg.COLib.Encrypt.pas

665 lines
18 KiB
Plaintext

unit Tocsg.COLib.Encrypt;
interface
uses
Winapi.Windows, System.SysUtils, System.Classes, Vcl.Dialogs, System.Math, System.IOUtils,
Tocsg.COLib, Tocsg.Safe;
const
SIG_CO_DRM = 'ToCSG_CO';
DRM_CO_VER = 1;
MAX_HEAD_STR = 260;
type
// 헤더 구조체 (Packed로 선언하여 메모리 정렬 고정)
TTgDrmHead = packed record
wVer: WORD;
dwCustomerCode: DWORD;
sEmpNo,
sHostName,
sPartName,
sPgName: array [0..MAX_HEAD_STR - 1] of UTF8Char;
dtEnc: TDateTime;
end;
TTgColibDrm = class
private
class function EncryptKEK(const pKek: TBytes; const sFilePath: string): Boolean;
class function DecryptKEK(const sPath: string): TBytes;
class procedure EncryptDEKtoKEK(const pDek, pKek: TBytes);
class function DecryptDEKtoKEK(sPath:string; pKek:TBytes): TBytes;
class function CheckSig(aStream: TStream): Boolean;
// 헤더 정보를 읽고 복호화하여 스트림 위치를 데이터 시작점으로 이동
class function ExtractHead(aSrcStream: TStream; pDek: TBytes; var aHead: TTgDrmHead): Boolean;
public
class function CreateSalt(const iKeylen: Integer): TBytes;
class function CreateKEK(const sPass, sSalt: AnsiString; Iterations, iKeyLen: Integer): TBytes;
class procedure CreateDEK(const iKeylen: Integer; pKek: TBytes);
class function GetKEK(sPath:string ): TBytes;
class function GetDEK(sPath:string; pKek:TBytes): TBytes;
// 헤더 정보를 생성하고 파일 스트림에 기록
//class function SetHead(aDstStream: TStream; pDek: TBytes; sEmpNo, sHostName, sPartName, sPgName: UTF8String): Boolean;
//class procedure DoEncrypt(sPath: string; pDek: TBytes; sEmpNo, sHostName, sPartName, sPgName: UTF8String);
class function SetHead(aDstStream: TStream; pDek: TBytes): Boolean;
class function DoEncrypt(sPath: string; pDek: TBytes): Boolean;
class procedure DoDecrypt(sPath: string; pDek: TBytes);
class function IsEncrypt(sPath: string): Boolean;
end;
implementation
{ TTgColibDrm }
type
TDataBlob = record
cbData: DWORD;
pbData: PByte;
end;
PDataBlob = ^TDataBlob;
function CryptProtectData(pDataIn: PDataBlob; szDataDescr: PWideChar;
pOptionalEntropy: PDataBlob; pvReserved: Pointer; pPromptStruct: Pointer;
dwFlags: DWORD; pDataOut: PDataBlob): BOOL; stdcall;
external 'crypt32.dll' name 'CryptProtectData';
function CryptUnprotectData(pDataIn: PDataBlob; ppszDataDescr: PPWideChar;
pOptionalEntropy: PDataBlob; pvReserved: Pointer; pPromptStruct: Pointer;
dwFlags: DWORD; pDataOut: PDataBlob): BOOL; stdcall;
external 'crypt32.dll' name 'CryptUnprotectData';
function ConcatBytes(const pBuf1, pBuf2: TBytes): TBytes;
begin
SetLength(Result, Length(pBuf1) + Length(pBuf2));
Move(pBuf1[0], Result[0], Length(pBuf1));
Move(pBuf2[0], Result[Length(pBuf1)], Length(pBuf2));
end;
procedure XORBytes(var pBuf1: TBytes; const pBuf2: TBytes);
var
i: Integer;
begin
for i := 0 to Min(Length(pBuf1), Length(pBuf2)) - 1 do
pBuf1[i] := pBuf1[i] xor pBuf2[i];
end;
class function TTgColibDrm.CreateSalt(const iKeylen: Integer): TBytes;
var
nResult: Integer;
pSalt: TBytes;
sPath: string;
begin
sPath := 'salt.bin';
if TFile.Exists(sPath) then
begin
Result := TFile.ReadAllBytes(sPath);
Exit;
end;
// 파일이 없는 경우: 새로운 Salt 생성
SetLength(pSalt, iKeylen);
// SHA-256 난수
nResult := COLibDrbg(@pSalt[0], Length(pSalt), nil, 0, nil, 0, 0);
if nResult = COLIB_SUCCESS then
begin
// 생성 성공 시 파일로 기록
try
TFile.WriteAllBytes(sPath, pSalt);
Result := pSalt;
except
on E: Exception do
begin
ShowMessage('Salt 파일 저장 중 오류: ' + E.Message);
Result := nil;
end;
end;
end
else
begin
// 생성 실패 시 처리
ShowMessage('Salt 생성 실패: ' + IntToStr(nResult) + ', ' + COLibGetErrorStr(nResult));
Result := nil;
end;
end;
class function TTgColibDrm.EncryptKEK(const pKek: TBytes; const sFilePath: string): Boolean;
var
DataIn, DataOut: TDataBlob;
FileStream: TFileStream;
begin
Result := False;
if Length(pKek) = 0 then
begin
ShowMessage('KEK가 비어있습니다.');
Exit;
end;
// TBytes 데이터를 DataIn 구조체에 연결
DataIn.pbData := @pKek[0];
DataIn.cbData := Length(pKek);
// DPAPI 암호화 호출 (현재 사용자 계정으로만 복호화 가능하도록 설정)
// 세 번째 인자는 엔트로피(추가 암호)
if CryptProtectData(@DataIn, nil, nil, nil, nil, 0, @DataOut) then
begin
try
FileStream := TFileStream.Create(sFilePath, fmCreate);
try
FileStream.WriteBuffer(DataOut.pbData^, DataOut.cbData);
Result := True;
finally
FileStream.Free;
end;
finally
// DPAPI에서 할당한 메모리 해제
LocalFree(HLOCAL(DataOut.pbData));
end;
end
else
begin
ShowMessage('CryptProtectData is failed: ' + IntToStr(GetLastError));
end;
end;
class function TTgColibDrm.DecryptKEK(const sPath: string): TBytes;
var
DataIn, DataOut: TDataBlob;
ms: TMemoryStream;
begin
SetLength(Result, 0);
if not FileExists(sPath) then
begin
ShowMessage('KEK가 존재하지 않습니다');
Exit;
end;
ms := TMemoryStream.Create;
try
ms.LoadFromFile(sPath);
DataIn.pbData := ms.Memory;
DataIn.cbData := ms.Size;
// DPAPI 복호화 호출
if CryptUnprotectData(@DataIn, nil, nil, nil, nil, 0, @DataOut) then
begin
try
// 복호화된 바이너리 데이터를 TBytes로 복사
SetLength(Result, DataOut.cbData);
if DataOut.cbData > 0 then
Move(DataOut.pbData^, Result[0], DataOut.cbData);
finally
LocalFree(HLOCAL(DataOut.pbData));
end;
end
else
begin
ShowMessage('CryptUnprotectData is failed: ' + IntToStr(GetLastError));
end;
finally
ms.Free;
end;
end;
class function TTgColibDrm.CreateKEK(const sPass, sSalt: AnsiString; Iterations, iKeyLen: Integer): TBytes;
var
i, n, nBlockCnt: Integer;
pSaltAndBlkIdx, pBlockIdx, pTemp, U_prev, U_current, pPass, pSalt: TBytes;
ci: TCIPHER_INFO;
nOut, nResult: Integer;
path: string;
begin
path := 'kek.bin';
if TFile.Exists(path) then
begin
// 파일이 존재하면 복호화후 리턴
//ShowMessage('실제 감지된 경로: ' + TPath.GetFullPath(path));
Result := DecryptKEK(path);
Exit;
end;
SetLength(Result, iKeyLen);
ZeroMemory(Result, iKeyLen);
if (sPass = '') or (sSalt = '') then
exit;
n := Length(sPass);
if n < 16 then
begin
SetLength(pPass, 16);
ZeroMemory(pPass, 16);
end else
SetLength(pPass, n);
CopyMemory(pPass, @sPass[1], n);
n := Length(sSalt);
SetLength(pSalt, n);
CopyMemory(pSalt, @sSalt[1], n);
ZeroMemory(@ci, SizeOf(ci));
SetLength(pBlockIdx, 4);
SetLength(pTemp, iKeyLen);
SetLength(U_prev, iKeyLen);
SetLength(U_current, iKeyLen);
// PBKDF
// 32바이트 단위(SHA256)의 각 블록마다 다음을 수행
// 1) U₁ = HMAC(Pass, Salt || BlockIndex)
// 2) U_n = HMAC(Pass, U_{n-1})
// 3) U_{n-1} XOR U_n
// 32 바이트씩 나눠서 블록개수 계산
nBlockCnt := (iKeyLen + 31) div iKeyLen; // SHA256 = 32 bytes
for i := 1 to nBlockCnt do
begin
// BlockIndex = i (Big Endian)
// 블록 번호를 Big Endian으로 표현한 4바이트 ex) i=[0,0,0,1]
// int(i)로 표현
pBlockIdx[0] := Byte((i shr 24) and $FF);
pBlockIdx[1] := Byte((i shr 16) and $FF);
pBlockIdx[2] := Byte((i shr 8) and $FF);
pBlockIdx[3] := Byte(i and $FF);
// Salt || int(i)
pSaltAndBlkIdx := ConcatBytes(pSalt, pBlockIdx);
nOut := 0;
// 1) U₁ = HMAC(Pass, Salt || BlockIndex)
COLibSetCipherInfo(@ci, COLIB_MODE_HMAC, BLOCK_CRYPTO_MODE_UNUSED, COLIB_PAD_TYPE_UNUSED,
@pPass[0], Length(pPass), nil, 0, @pSaltAndBlkIdx[0], Length(pSaltAndBlkIdx), @U_prev[0], @nOut);
nResult := COLibEncrypt(@ci);
if nResult <> COLIB_SUCCESS then
exit;
CopyMemory(pTemp, U_prev, Length(U_prev));
for n := 2 to Iterations do
begin
nOut := 0;
// 2) U_n = HMAC(Pass, U_{n-1})
COLibSetCipherInfo(@ci, COLIB_MODE_HMAC, BLOCK_CRYPTO_MODE_UNUSED, COLIB_PAD_TYPE_UNUSED,
@pPass[0], Length(pPass), nil, 0, @U_prev[0], Length(U_prev), @U_current[0], @nOut);
nResult := COLibEncrypt(@ci);
if nResult <> COLIB_SUCCESS then
exit;
// 3) U_{n-1} XOR U_n
XORBytes(pTemp, U_current);
CopyMemory(U_prev, U_current, Length(U_current));
end;
Move(pTemp[0], Result[(i - 1) * iKeyLen], Min(iKeyLen, iKeyLen - (i - 1) * iKeyLen));
end;
if not (EncryptKEK(Result, path)) then
begin
ZeroMemory(Result, iKeyLen);
ShowMessage('EncryptKEK is failed');
end;
end;
class function TTgColibDrm.GetKEK(sPath:string): TBytes;
begin
if TFile.Exists(sPath) then
begin
Result := DecryptKEK(sPath);
end
else
begin
//ShowMessage('KEK가 존재하지 않습니다');
Result := nil;
end;
end;
class function TTgColibDrm.GetDEK(sPath:string; pKek:TBytes): TBytes;
begin
if TFile.Exists(sPath) then
begin
Result := DecryptDEKtoKEK(sPath, pKek);
end
else
begin
//ShowMessage('DEK가 존재하지 않습니다.');
Result := nil;
end;
end;
class procedure TTgColibDrm.EncryptDEKtoKEK(const pDek, pKek: TBytes);
var
nResult, nOut: Integer;
ci: TCIPHER_INFO;
arrIV: array [0..COLIB_BLOCK_LEN-1] of Byte;
arrEnc: TBytes;
sOut, sIn: string;
begin
ZeroMemory(@ci, SizeOf(ci));
ZeroMemory(@arrIV, SizeOf(arrIV));
SetLength(arrEnc, Length(pDek)*2);
// for var i := 0 to Length(pDek) - 1 do
// sIn := sIn + Format('%.2x', [pDek[i]]);
// ShowMessage('평문 DEK (Hex) : ' + Inttostr(Length(pDek))+', '+sIn);
COLibSetCipherInfo(@ci, COLIB_MODE_ARIA, BLOCK_CRYPTO_MODE_CBC, COLIB_PAD_TYPE_NO,
@pKek[0], Length(pKek), @arrIV, SizeOf(arrIV),
@pDek[0], Length(pDek), @arrEnc[0], @nOut);
nResult := COLibEncrypt(@ci);
if nResult <> COLIB_SUCCESS then
begin
ShowMessage('COLibEncrypt is failed: ' + IntToStr(nResult) + ', ' + COLibGetErrorStr(nResult));
Exit;
end;
// for var i := 0 to nOut - 1 do
// sOut := sOut + Format('%.2x', [arrEnc[i]]);
// ShowMessage('암호화 DEK (Hex) : ' + Inttostr(nOut)+', '+sOut);
// 암호화된 DEK를 바이너리 그대로 파일에 저장
TFile.WriteAllBytes('dek.bin', Copy(arrEnc, 0, nOut));
end;
class procedure TTgColibDrm.CreateDEK(const iKeylen: Integer; pKek: TBytes);
var
pDek: TBytes;
sPath: string;
begin
sPath := 'dek.bin';
if TFile.Exists(sPath) then
begin
Exit;
end;
SetLength(pDek, iKeylen);
COLibGetKey(@pDek[0], iKeylen);
EncryptDEKtoKEK(pDek, pKek);
end;
class function TTgColibDrm.DecryptDEKtoKEK(sPath:string; pKek:TBytes): TBytes;
var
nResult, nOut: Integer;
ci: TCIPHER_INFO;
arrIV: array [0..COLIB_BLOCK_LEN-1] of Byte;
arrDec, encDek: TBytes;
sOut, sIn: string;
begin
ZeroMemory(@ci, SizeOf(ci));
ZeroMemory(@arrIV, SizeOf(arrIV));
// if not TFile.Exists(sPath) then
// begin
// ShowMessage('DEK 존재하지 않음');
// Result := nil;
// Exit;
// end;
encDek := TFile.ReadAllBytes(sPath);
SetLength(arrDec, Length(encDek)); // 암호문 길이만큼 버퍼 확보
// for var i := 0 to Length(encDek) - 1 do
// sIn := sIn + Format('%.2x', [encDek[i]]);
// ShowMessage('암호화된 DEK (Hex) : ' + Inttostr(Length(encDek))+', '+sIn);
COLibSetCipherInfo(@ci, COLIB_MODE_ARIA, BLOCK_CRYPTO_MODE_CBC, COLIB_PAD_TYPE_NO,
@pKek[0], Length(pKek), @arrIV, SizeOf(arrIV),
@encDek[0], Length(encDek), @arrDec[0], @nOut);
nResult := COLibDecrypt(@ci);
if nResult <> COLIB_SUCCESS then
begin
ShowMessage('COLibDecrypt is failed: ' + IntToStr(nResult) + ', ' + COLibGetErrorStr(nResult));
Result := nil;
Exit;
end;
// 실제 복호화된 길이만큼 잘라내기
//SetLength(arrDec, nOut);
// for var i := 0 to nOut - 1 do
// sOut := sOut + Format('%.2x', [arrDec[i]]);
// ShowMessage('복호화 DEK (Hex) : ' + Inttostr(Length(arrDec))+', '+sOut);
Result := arrDec;
end;
class function TTgColibDrm.CheckSig(aStream: TStream): Boolean;
var
sSig: AnsiString;
pBuf: array[0..7] of AnsiChar; // SIG_CO_DRM 길이만큼
begin
Result := False;
if aStream.Size < Length(SIG_CO_DRM) then Exit;
aStream.Position := 0;
aStream.Read(pBuf, Length(SIG_CO_DRM));
sSig := Copy(pBuf, 0, Length(SIG_CO_DRM));
Result := (sSig = SIG_CO_DRM);
end;
class function TTgColibDrm.IsEncrypt(sPath: string): Boolean;
var
fs: TFileStream;
begin
Result := False;
if not FileExists(sPath) then Exit;
try
Guard(fs, TFileStream.Create(sPath, fmOpenRead or fmShareDenyNone));
Result := CheckSig(fs);
except
Result := False;
end;
end;
class function TTgColibDrm.SetHead(aDstStream: TStream; pDek: TBytes): Boolean;
var
Head: TTgDrmHead;
ci: TCIPHER_INFO;
arrIV: array [0..COLIB_BLOCK_LEN-1] of Byte;
pEncHead: TBytes;
nOut, nResult: Integer;
begin
Result := False;
ZeroMemory(@Head, SizeOf(Head));
Head.wVer := DRM_CO_VER;
Head.dwCustomerCode := 13001;
Head.dtEnc := Now;
//Head.llEncDT := DelphiDtToJavaDt(Now);
StrCopy(Head.sPgName, PUTF8Char(SIG_CO_DRM));
StrCopy(Head.sEmpNo, PUTF8Char(SIG_CO_DRM));
StrCopy(Head.sPartName, PUTF8Char(SIG_CO_DRM));
StrCopy(Head.sHostName, PUTF8Char(SIG_CO_DRM));
// Move(PAnsiChar(sEmpNo)^, Head.sEmpNo[0], Min(Length(sEmpNo), MAX_HEAD_STR));
// Move(PAnsiChar(sHostName)^, Head.sHostName[0], Min(Length(sHostName), MAX_HEAD_STR));
// Move(PAnsiChar(sPartName)^, Head.sPartName[0], Min(Length(sPartName), MAX_HEAD_STR));
// Move(PAnsiChar(sPgName)^, Head.sPgName[0], Min(Length(sPgName), MAX_HEAD_STR));
// 헤더 암호화
ZeroMemory(@ci, SizeOf(ci));
ZeroMemory(@arrIV, SizeOf(arrIV));
SetLength(pEncHead, SizeOf(Head) + 16); // 패딩 고려
COLibSetCipherInfo(@ci, COLIB_MODE_ARIA, BLOCK_CRYPTO_MODE_CBC, COLIB_PAD_TYPE_PKCS,
@pDek[0], Length(pDek), @arrIV, SizeOf(arrIV),
@Head, SizeOf(Head), @pEncHead[0], @nOut);
nResult := COLibEncrypt(@ci);
if nResult = COLIB_SUCCESS then
begin
// 암호화된 헤더 길이(Integer, 4byte)를 먼저 쓰고 데이터를 씀 (복호화 시 필요)
aDstStream.Write(nOut, SizeOf(Integer));
aDstStream.Write(pEncHead[0], nOut);
Result := True;
end;
end;
class function TTgColibDrm.DoEncrypt(sPath: string; pDek: TBytes): Boolean;
const
BUF_LEN_64K = 1024 * 64;
var
fsSrc, fsDst: TFileStream;
arrBuf: array [0..BUF_LEN_64K-1] of Byte;
arrEnc: array [0..BUF_LEN_64K + 31] of Byte; // 패딩 대비 여유 공간
dwRead: DWORD;
nOut, nResult: Integer;
pt: TCOLIB_PAD_TYPE;
ci: TCIPHER_INFO;
arrIV: array [0..COLIB_BLOCK_LEN-1] of Byte;
sSig: AnsiString;
begin
sSig := SIG_CO_DRM;
ZeroMemory(@arrIV, SizeOf(arrIV));
Guard(fsSrc, TFileStream.Create(sPath, fmOpenRead));
Guard(fsDst, TFileStream.Create(sPath + '.enc', fmCreate));
// 시그니처 (평문)
fsDst.Write(sSig[1], Length(sSig));
if not SetHead(fsDst, pDek) then Exit;
while fsSrc.Position < fsSrc.Size do
begin
dwRead := fsSrc.Read(arrBuf, BUF_LEN_64K);
// 마지막 블록인 경우 패딩 처리
if fsSrc.Position >= fsSrc.Size then
pt := COLIB_PAD_TYPE_PKCS
else
pt := COLIB_PAD_TYPE_NO;
ZeroMemory(@ci, SizeOf(ci));
COLibSetCipherInfo(@ci, COLIB_MODE_ARIA, BLOCK_CRYPTO_MODE_CBC, pt,
@pDek[0], Length(pDek), @arrIV, SizeOf(arrIV),
@arrBuf, dwRead, @arrEnc, @nOut);
nResult := COLibEncrypt(@ci);
if nResult <> COLIB_SUCCESS then
begin
ShowMessage('파일 암호화 실패: ' + IntToStr(nResult));
Result := False;
Exit;
end;
fsDst.Write(arrEnc, nOut);
Result := True;
// CBC 모드이므로 다음 블록의 IV는 현재 암호문의 마지막 블록이어야 함 (라이브러리 내부 처리 확인 필요)
// 만약 라이브러리가 IV를 자동 갱신하지 않는다면 여기서 마지막 암호문 블록을 arrIV에 복사해야 한다.
end;
end;
class function TTgColibDrm.ExtractHead(aSrcStream: TStream; pDek: TBytes; var aHead: TTgDrmHead): Boolean;
var
nEncHeadLen, nOut, nResult: Integer;
pEncBuf, pDecBuf: TBytes;
ci: TCIPHER_INFO;
arrIV: array [0..COLIB_BLOCK_LEN-1] of Byte;
begin
Result := False;
// 암호화된 헤더 길이 (4바이트)
if aSrcStream.Read(nEncHeadLen, SizeOf(Integer)) <> SizeOf(Integer) then Exit;
// 암호화된 헤더 본문
SetLength(pEncBuf, nEncHeadLen);
if aSrcStream.Read(pEncBuf[0], nEncHeadLen) <> nEncHeadLen then Exit;
// 헤더 복호화
SetLength(pDecBuf, nEncHeadLen);
ZeroMemory(@ci, SizeOf(ci));
ZeroMemory(@arrIV, SizeOf(arrIV));
COLibSetCipherInfo(@ci, COLIB_MODE_ARIA, BLOCK_CRYPTO_MODE_CBC, COLIB_PAD_TYPE_PKCS,
@pDek[0], Length(pDek), @arrIV, SizeOf(arrIV),
@pEncBuf[0], nEncHeadLen, @pDecBuf[0], @nOut);
nResult := COLibDecrypt(@ci);
if nResult = COLIB_SUCCESS then
begin
Move(pDecBuf[0], aHead, SizeOf(TTgDrmHead));
Result := True;
end;
end;
class procedure TTgColibDrm.DoDecrypt(sPath: string; pDek:TBytes);
const
BUF_LEN_64K = 1024 * 64;
var
fsSrc, fsDst: TFileStream;
arrBuf: array [0..BUF_LEN_64K - 1] of Byte;
arrDec: array [0..BUF_LEN_64K + 31] of Byte; // 패딩 대비 여유 공간
dwRead: DWORD;
nOut, nResult: Integer;
pt: TCOLIB_PAD_TYPE;
ci: TCIPHER_INFO;
arrIV: array [0..COLIB_BLOCK_LEN - 1] of Byte;
HeaderInfo: TTgDrmHead; // 복호화된 헤더 정보를 담을 변수
begin
Guard(fsSrc, TFileStream.Create(sPath, fmOpenRead or fmShareDenyNone));
// 기존 파일명에서 .enc가 있다면 제거하고 .dec를 붙임
Guard(fsDst, TFileStream.Create(ChangeFileExt(sPath, '') + '.dec', fmCreate));
try
// 시그니처 위치(8바이트) 건너뛰기
fsSrc.Position := Length(SIG_CO_DRM);
// 헤더 추출 및 복호화 (데이터 시작 지점으로 스트림 포인터 이동)
if not ExtractHead(fsSrc, pDek, HeaderInfo) then
begin
ShowMessage('헤더 복호화에 실패했습니다. 키 값이 올바르지 않을 수 있습니다.');
Exit;
end;
// 복호화된 헤더 정보 활용
// ShowMessage('복호화 대상 사번: ' + string(UTF8String(HeaderInfo.sEmpNo)));
ZeroMemory(@arrIV, SizeOf(arrIV));
while fsSrc.Position < fsSrc.Size do
begin
dwRead := fsSrc.Read(arrBuf, BUF_LEN_64K);
// 스트림의 마지막 블록인 경우에만 PKCS 패딩 적용
if fsSrc.Position >= fsSrc.Size then
pt := COLIB_PAD_TYPE_PKCS
else
pt := COLIB_PAD_TYPE_NO;
ZeroMemory(@ci, SizeOf(ci));
COLibSetCipherInfo(@ci, COLIB_MODE_ARIA, BLOCK_CRYPTO_MODE_CBC, pt,
@pDek[0], Length(pDek), @arrIV, SizeOf(arrIV),
@arrBuf, dwRead, @arrDec, @nOut);
nResult := COLibDecrypt(@ci);
if nResult <> COLIB_SUCCESS then
begin
ShowMessage('데이터 복호화 중 오류 발생: ' + IntToStr(nResult));
Exit;
end;
fsDst.Write(arrDec, nOut);
end;
//ShowMessage('복호화가 완료되었습니다.');
except
on E: Exception do
ShowMessage('복호화 중 예외 발생: ' + E.Message);
end;
end;
end.