Forum > Other OS

macOS Keychain access

(1/1)

CCRDude:
I'm working on improving my small UI for the Wireguard command line tools on macOS (reason: official Wireguard UI allows only one connection at a time, CLI allows more).

The regular UI stores profiles in keychain, the CLI uses the regular Wireguard configuration file location. It's easy to create these, but I want to provide a function to "import" profiles from the keychain.

It's easy to grab the first entry:

--- Code: ---function GetFirstWireguardProfile(out AProfile: ansistring): boolean;
var
   s: OSStatus;
   iMaxLen: uint32;
   iActualLen: uint32;
   pc: pansichar;
   item: KCItemRefPtr;
   str: Str255;
begin
   //   function KCFindGenericPassword( serviceName: ConstStringPtr { can be NULL }; accountName: ConstStringPtr { can be NULL }; maxLength: UInt32; passwordData: UnivPtr; var actualLength: UInt32; item: KCItemRefPtr { can be NULL } ): OSStatus; external name '_KCFindGenericPassword';
   // @see https://developer.apple.com/documentation/coreservices/1562994-kcfindgenericpassword
   str := 'com.wireguard.macos';
   iMaxLen := 1024;
   iActualLen := 0;
   pc := AllocMem(iMaxLen);
   try
      item := nil;
      s := KCFindGenericPassword(@str, nil, iMaxLen, pc, iActualLen, item);
      Result := (0 = s);
      if Result then begin
         AProfile := pc;
      end;
   finally
      FreeMem(pc);
   end;
end;
--- End code ---

Of course I want to access more than the first, so I'm looking for the right API calls. Has anyone had success with items like KCFindFirstItem? Even Google doesn't seem to find examples, regardless of the language.

My approach, not working yet, because I need to find out how to define search parameters most likely:

--- Code: ---function GetWireguardProfiles(constref AList: TStrings): boolean;
var
   s: OSStatus;
   keychain: KCRef;
   attr: KCAttributeListPtr;
   search: KCSearchRef;
   item: KCItemRef;
   iMaxLen: uint32;
   iActualLen: uint32;
   pc: pansichar;
begin
   s := KCGetDefaultKeychain(keychain);
   AList.Add(Format('KCGetDefaultKeychain() = %d', [s]));
   // attr
   attr := nil;
   // search
   search := nil;
   item := nil;
   s := KCFindFirstItem(keychain, attr, search, item);
   AList.Add(Format('KCFindFirstItem() = %d', [s]));
   iMaxLen := 1024;
   iActualLen := 0;
   pc := AllocMem(iMaxLen);
   try
      item := nil;
      s := KCGetData(item, iMaxLen, pc, iActualLen);
      AList.Add(Format('KCGetData(maxLength = %d, actualLength = %d) = %d', [iMaxLen, iActualLen, s]));
      if (0 = s) then begin
         AList.Add(AnsiString(pc));
      end;
   finally
      FreeMem(pc);
   end;
end; 
--- End code ---

(code will go into this repository, Wireguard tool will go into separate repository)

CCRDude:
Update: it was easier than I thought...
Some further fine tuning required (restricting search to Wireguard service name), but the basic keychain access works.


--- Code: ---function GetWireguardProfiles(constref AList: TStrings): boolean;
var
   s: OSStatus;
   keychain: KCRef;
   search: KCSearchRef;
   item: KCItemRef;
   iMaxLen: uint32;
   iActualLen: uint32;
   pc: pansichar;
   attrList: KCAttributeList;
   attra: array[0..1] of KCAttribute;
   attr1: KCAttribute;
   itemClass: KCItemClass;
   sService: string;
begin
   Result := True;
   Initialize(keychain);
   s := KCGetDefaultKeychain(keychain);
   // AList.Add(Format('KCGetDefaultKeychain() = %d', [s]));
   // search
   search := nil;
   item := nil;
   itemClass := kGenericPasswordKCItemClass;

   attra[0].tag := kClassKCItemAttr;
   attra[0].Data := @itemClass;
   attra[0].length := sizeof(itemClass);
   // TODO : restrict search to service
   attrList.Count := 1;
   attrList.attr := @attra[0];
   // @see https://github.com/aptana/Jaxer/blob/f7994fc75a768c9873f094e29868c22e88b46b50/server/src/mozilla/extensions/wallet/src/singsign.cpp#L3204
   s := KCFindFirstItem(keychain, @attrList, search, item);
   // AList.Add(Format('KCFindFirstItem() = %d', [s]));
   repeat
      iMaxLen := 1024;
      iActualLen := 0;
      pc := AllocMem(iMaxLen);
      try
         attr1.tag := kServiceKCItemAttr;
         attr1.length := iMaxLen;
         attr1.Data := pc;
         s := KCGetAttribute(item, attr1, iActualLen);
         if (s = 0) then begin
            sService := ansistring(pc);
            // AList.Add(Format('KCGetAttribute() = %d, server = %s', [s, sService]));
            if ('com.wireguard.macos' = sService) then begin
               s := KCGetData(item, iMaxLen, pc, iActualLen);
               // AList.Add(Format('KCGetData(maxLength = %d, actualLength = %d) = %d', [iMaxLen, iActualLen, s]));
               if (0 = s) then begin
                  AList.Add(ansistring(pc));
               end;
            end;
         end else begin
            // AList.Add(Format('KCGetAttribute() = %d', [s]));
         end;
      finally
         FreeMem(pc);
      end;
      s := KCFindNextItem(search, item);
      // AList.Add(Format('KCFindNextItem() = %d', [s]));
   until (s <> 0);
end;
--- End code ---

CCRDude:
Public repository now includes code to read all entries for a specific service (for me: com.wireguard.macos):
https://gitlab.com/ccrdude-pascal/firefly-macos/-/blob/main/source/Firefly.MacOS.KeyChain.pas

cdbc:
Hi
Just curious, you have "KCFindFirstItem()" & "KCFindNextItem()", is it just me that wants a "KCFindClose()" somewhere at the end?!?
I guess, I'm just too used to "FindFirst", "FindNext" & "FindClose" from file-searching....
Pardon
Benny

CCRDude:
You're absolutely right, it still needs a KCReleaseSearch and even KCReleaseItem calls. These have been added now!

Due to the Apple documentation pages about these calls being empty, there's some guessing involved, but I finally found some examples searching on GitHub, and these also use them.

Navigation

[0] Message Index

Go to full version