Disclaimer
The encryption algorithm used in this answer is very basic and can be easily broken by any individual with medium to high skills in cryptography. It is used in the solution because the OP is asking for a simple symmetric solution without requiring any library.
Principle
The solution is based on the XOR cipher. From the Wikipedia:
In cryptography, the simple XOR cipher is a type of additive cipher, an encryption algorithm that operates according to the principles:
A X 0 = A,
A X A = 0,
(A X B) X C = A X (B X C),
(B X A) X A = B X 0 = B,
where X denotes the XOR operation.
Pieces of the puzzle
My proposed solution is based in this basic routine:
function XorCipher(const Key, Source: TBytes): TBytes;
var
I: Integer;
begin
if Length(Key) = 0 then
Exit(Source);
SetLength(Result, Length(Source));
for I := Low(Source) to High(Source) do
Result[I] := Key[I mod Length(Key)] xor Source[I];
end;
The routine accepts a key and the source data as an array of bytes, and returns the resulting XORed array of bytes. The same routine functions to encrypt and to decrypt information, given the same key is used in both operations. To encrypt, the source is the plain data, and to decrypt, the source is the encrypted data.
I made two auxiliary routines to allow storing the result as a string. One to convert an array of bytes to a textual sequence of hexadecimal numbers, and the other to perform the reverse conversion:
function BytesToStr(const Bytes: TBytes): string;
var
I: Integer;
begin
Result := '';
for I := Low(Bytes) to High(Bytes) do
Result := Result + LowerCase(IntToHex(Bytes[I], 2));
end;
function StrToBytes(const value: string): TBytes;
var
I: Integer;
begin
SetLength(Result, Length(value) div 2);
for I := Low(Result) to High(Result) do
Result[I] := StrToIntDef('$' + Copy(value, (I * 2) + 1, 2), 0);
end;
With this foundations, you can build all of what you need. For convenience and test my code, I created some other routines, for example:
this one to store the key inside the exe and get it as a TBytes value
function GetKey: TBytes;
begin
Result := TArray<Byte>.Create(
$07, $14, $47, $A0, $F4, $F7, $FF, $48, $21, $32
, $AF, $87, $09, $8E, $B3, $C0, $7D, $54, $45, $87
, $8A, $A8, $23, $32, $00, $56, $11, $1D, $98, $FA
);
end;
you can provide a key of any length, since it rolls to encrypt the data inside XorCipher routine.
this one to properly encode a given string using that key:
function XorEncodeStr(const Source: string): string; overload;
var
BSource: TBytes;
begin
SetLength(BSource, Length(Source) * SizeOf(Char));
Move(Source[1], BSource[0], Length(Source) * SizeOf(Char));
Result := XorEncodeToStr(GetKey, BSource);
end;
this other to properly decode a encoded string to a string
function XorDecodeStr(const Source: string): string; overload;
var
BResult: TBytes;
begin
BResult := XorDecodeFromStr(GetKey, source);
Result := TEncoding.Unicode.GetString( BResult );
end;
Writing the INI file
With this routines accessible to the place where you write and read your INI file, you can easily write and read it, for example:
procedure TForm1.SaveIni;
var
Ini: TIniFile;
I: Integer;
begin
Ini := TIniFile.Create('.config.ini');
try
Ini.WriteInteger('config', 'NumEntries', ListBox1.Items.Count);
for I := 0 to ListBox1.Items.Count - 1 do
Ini.WriteString('listbox', IntToStr(I), XorEncodeStr(listbox1.Items[I]));
finally
Ini.Free;
end;
end;
procedure TForm1.LoadIni;
var
Ini: TIniFile;
Max, I: Integer;
begin
ListBox1.Items.Clear;
Ini := TIniFile.Create('.config.ini');
try
Max := Ini.ReadInteger('config', 'NumEntries', 0);
for I := 0 to Max - 1 do
ListBox1.Items.Add(
XorDecodeStr(Ini.ReadString('listbox', IntToStr(I), ''))
);
finally
Ini.Free;
end;
end;
This is not production ready-code, since it's written only to test the solution, but it is also a starting point for you to make it rock-solid.
A word of caution
This is not strong cryptography, so, don't rely on this to store really sensitive information. One weak point is the key is contained inside your exe in plain form. You can work on this, but the main weakens is the algorithm itself.
Take as an example of this issue the following: since you're encoding Unicode Delphi strings in UTF-16 format, the second byte of each character is usually zero (unless you're in the east or a country with a non-latin alphabet), and you will find the exact bytes of the key repeats in your encoded stored strings. You can make this less apparent by not using a plain hexadecimal representation of the encoded data (for example encoding it using base64 as already suggested here).
You can resort to AnsiStrings to avoid revealing this parts of your key, or you can code your key with explicit zero bytes (or other constant byte) in the even positions.
Anything of this will work if the users of your software are not cryptographically educated, but the fact is that anyone with a medium level of knowledge and good skills can get the key by analyzing your data. If the user knows a un-encoded value, it gets easier.