//+-----------------------------------------------------------------------
//
// Microsoft Windows
//
// Copyright (c) Microsoft Corporation 1992 - 1997
//
// File:        sidcache.cxx
//
// Contents:    routines to cache username->sid translations to speed up
//              logon performance
//
//
// History:     27-May-1998     MikeSw          Created
//
//------------------------------------------------------------------------

#include <kerb.hxx>
#include <kerbp.h>


#define KERB_LOGON_SID_CACHE_KEY L"SidCache"
#define KERB_LOGON_SID_CACHE_ENTRIES L"CacheEntries"
#define KERB_LOGON_SID_CACHE_ENTRY_NAME L"Entry%d"
#define KERB_MACHINE_SID_CACHE_NAME L"MachineSid"
#define KERB_LOGON_SID_CACHE_MAX_ENTRIES 1000
#define KERB_LOGON_SID_CACHE_DEFAULT_ENTRIES 10
#define KERB_LOGON_SID_CACHE_ENTRY_NAME_SIZE (sizeof(L"Entry") * 4*sizeof(WCHAR))
#define KERB_LOGON_SID_CACHE_VERSION 0


KERBEROS_LIST KerbSidCache;
HKEY KerbSidCacheKey;
ULONG KerbSidCacheMaxEntries = KERB_LOGON_SID_CACHE_DEFAULT_ENTRIES;
ULONG KerbSidCacheEntries;



#define VALIDATE_POINTER(Ptr,Size,Base,Bound,NewBase,Type) \
    if (((PUCHAR)(Ptr) < (PUCHAR)(Base)) || (((PUCHAR)(Ptr) + (Size)) > (PUCHAR)(Bound))) \
    { \
        Status = STATUS_INVALID_PARAMETER; \
        goto Cleanup; \
    } \
    else \
    { \
        (Ptr) = (Type) ((PUCHAR) (Ptr) - (ULONG_PTR) Base + (ULONG_PTR) NewBase); \
    } \



//+-------------------------------------------------------------------------
//
//  Function:   KerbVerifyUnpackAndLinkSidCacheEntry
//
//  Synopsis:   Unmarshalls the entry & verifies that the pointers all
//              match up within the buffer. If this is the case, it
//              inserts the entry into the list & zeroes out the pointer
//              so the caller won't free it.
//
//  Effects:
//
//  Arguments:
//
//  Requires:
//
//  Returns:
//
//  Notes:
//
//
//--------------------------------------------------------------------------


NTSTATUS
KerbVerifyUnpackAndLinkSidCacheEntry(
    IN PKERB_SID_CACHE_ENTRY * SidCacheEntry,
    IN ULONG CacheEntrySize
    )
{
    NTSTATUS Status = STATUS_SUCCESS;
    PKERB_SID_CACHE_ENTRY CacheEntry = *SidCacheEntry;
    PUCHAR Bound;

    //
    // First check the base structure size
    //

    if (CacheEntrySize < sizeof(KERB_SID_CACHE_ENTRY))
    {
        Status = STATUS_INVALID_PARAMETER;
        goto Cleanup;
    }

    //
    // Now check the version
    //

    if (CacheEntry->Version != KERB_LOGON_SID_CACHE_VERSION)
    {
        Status = STATUS_INVALID_PARAMETER;
        goto Cleanup;
    }

    if (CacheEntry->Size != CacheEntrySize)
    {
        Status = STATUS_INVALID_PARAMETER;
        goto Cleanup;
    }
    //
    // Now check all the pointers
    //

    Bound = (PUCHAR) CacheEntry->Base + CacheEntrySize;

    VALIDATE_POINTER(
        CacheEntry->Sid,
        RtlLengthRequiredSid(5),                // space for 5 sub authorities
        CacheEntry->Base,
        Bound,
        CacheEntry,
        PSID
        );

    VALIDATE_POINTER(
        CacheEntry->LogonUserName.Buffer,
        CacheEntry->LogonUserName.MaximumLength,
        CacheEntry->Base,
        Bound,
        CacheEntry,
        PWSTR
        );

    VALIDATE_POINTER(
        CacheEntry->LogonDomainName.Buffer,
        CacheEntry->LogonDomainName.MaximumLength,
        CacheEntry->Base,
        Bound,
        CacheEntry,
        PWSTR
        );

    VALIDATE_POINTER(
        CacheEntry->LogonRealm.Buffer,
        CacheEntry->LogonRealm.MaximumLength,
        CacheEntry->Base,
        Bound,
        CacheEntry,
        PWSTR
        );

    KerbInitializeListEntry(
        &CacheEntry->Next
        );

    //
    // This inserts at the head, not the tail
    //

    KerbInsertListEntryTail(
        &CacheEntry->Next,
        &KerbSidCache
        );

    *SidCacheEntry = NULL;

Cleanup:

    return(Status);

}


//+-------------------------------------------------------------------------
//
//  Function:   KerbInitializeLogonSidCache
//
//  Synopsis:   Initializes the list of cached logon sids
//
//  Effects:    Reads data from the registry
//
//  Arguments:
//
//  Requires:
//
//  Returns:
//
//  Notes:
//
//
//--------------------------------------------------------------------------

NTSTATUS
KerbInitializeLogonSidCache(
    VOID
    )
{
    NTSTATUS Status = STATUS_SUCCESS;
    DWORD RegErr = ERROR_SUCCESS;
    DWORD Disposition = 0;
    PKERB_SID_CACHE_ENTRY NextEntry = NULL;
    ULONG NextEntrySize = 0;
    ULONG ValueType;
    ULONG Index;
    HKEY KerbParamKey = NULL;
    WCHAR EntryName[KERB_LOGON_SID_CACHE_ENTRY_NAME_SIZE];

    //
    // Initialize the list
    //

    Status = KerbInitializeList(&KerbSidCache);

    if (!NT_SUCCESS(Status))
    {
        goto Cleanup;
    }

    //
    // Create the sid cache key
    //

    RegErr = RegCreateKeyEx(
                HKEY_LOCAL_MACHINE,
                KERB_PARAMETER_PATH,
                0,
                NULL,
                0,
                KEY_ALL_ACCESS,
                0,
                &KerbParamKey,
                &Disposition
                );
    if (RegErr != ERROR_SUCCESS)
    {
        DebugLog((DEB_ERROR,"Failed to create %ws key: %d\n",KERB_PARAMETER_PATH, RegErr));
        Status = STATUS_UNSUCCESSFUL;
        goto Cleanup;
    }

    RegErr = RegCreateKeyEx(
                KerbParamKey,
                KERB_LOGON_SID_CACHE_KEY,
                0,                              // reserved
                NULL,                           // no class
                0,                              // no options
                KEY_ALL_ACCESS,
                NULL,                           // no security attributes
                &KerbSidCacheKey,
                &Disposition
                );
    if (RegErr != ERROR_SUCCESS)
    {
        DebugLog((DEB_ERROR,"Failed to create key %ws: %d\n",KERB_LOGON_SID_CACHE_KEY, RegErr));
        Status = STATUS_UNSUCCESSFUL;
        goto Cleanup;
    }

    //
    // Read out the size of the cache
    //

    NextEntrySize = sizeof(ULONG);

    RegErr = RegQueryValueEx(
                KerbSidCacheKey,
                KERB_LOGON_SID_CACHE_ENTRIES,
                NULL,                           // reserved,
                &ValueType,
                (PUCHAR) &KerbSidCacheMaxEntries,
                &NextEntrySize
                );
    if (RegErr == ERROR_SUCCESS)
    {
        //
        // Make sure the value is within the range & is of the correc type
        //

        if ( (ValueType != REG_DWORD) ||
             (KerbSidCacheMaxEntries > KERB_LOGON_SID_CACHE_MAX_ENTRIES) ||
             (KerbSidCacheMaxEntries == 0) )

        {
            KerbSidCacheMaxEntries = KERB_LOGON_SID_CACHE_DEFAULT_ENTRIES;
        }
    }

    //
    // Now read in all the entries. Loop through up to the max number of
    // entries, reading the entry, and inserting at the tail of the list.
    //

    for (Index = 0; Index < KerbSidCacheMaxEntries ; Index++ )
    {
        swprintf(EntryName,KERB_LOGON_SID_CACHE_ENTRY_NAME, Index);

        //
        // Query the size of the entry
        //

        NextEntrySize = NULL;
        RegErr = RegQueryValueEx(
                    KerbSidCacheKey,
                    EntryName,
                    NULL,
                    &ValueType,
                    (PUCHAR) NextEntry,
                    &NextEntrySize
                    );
        if ((RegErr == ERROR_SUCCESS) && (ValueType == REG_BINARY))
        {
            //
            // Allocate space for the entry and re-query to get the real
            // value.
            //

            NextEntry = (PKERB_SID_CACHE_ENTRY) KerbAllocate(NextEntrySize);
            if (NextEntry == NULL)
            {
                Status = STATUS_INSUFFICIENT_RESOURCES;
                goto Cleanup;
            }

            RegErr = RegQueryValueEx(
                        KerbSidCacheKey,
                        EntryName,
                        NULL,
                        &ValueType,
                        (PUCHAR) NextEntry,
                        &NextEntrySize
                        );
            if (RegErr != ERROR_SUCCESS)
            {
                DebugLog((DEB_ERROR,"Failed to query for sid cache value: %d\n",
                    RegErr ));
                Status = STATUS_UNSUCCESSFUL;
                goto Cleanup;
            }

            //
            // Call a helper routine to unpack,verify, and insert

            Status = KerbVerifyUnpackAndLinkSidCacheEntry(
                        &NextEntry,
                        NextEntrySize
                        );
            //
            // If the entry was invalid, remove it now
            //
            if (!NT_SUCCESS(Status))
            {
                (VOID) RegDeleteValue(
                            KerbSidCacheKey,
                            EntryName
                            );
                Status = STATUS_SUCCESS;
            }

            if (NextEntry != NULL)
            {
                KerbFree(NextEntry);
                NextEntry = NULL;
            }
            NextEntrySize = 0;



        }
    }

Cleanup:
    if (KerbParamKey != NULL)
    {
        RegCloseKey(KerbParamKey);
    }
    if (NextEntry != NULL)
    {
        KerbFree(NextEntry);
    }
    if (!NT_SUCCESS(Status))
    {
        if (KerbSidCacheKey != NULL)
        {
            RegCloseKey(KerbSidCacheKey);
            KerbSidCacheKey = NULL;
        }
    }
    return(Status);
}


//+-------------------------------------------------------------------------
//
//  Function:   KerbScavengeSidCache
//
//  Synopsis:   removes any stale entries from the sid cache to make it fit
//              within bounds
//
//  Effects:
//
//  Arguments:
//
//  Requires:
//
//  Returns:
//
//  Notes:
//
//
//--------------------------------------------------------------------------


VOID
KerbScavengeSidCache(
    VOID
    )
{
    PKERB_SID_CACHE_ENTRY CacheEntry;
    while (KerbSidCacheEntries > KerbSidCacheMaxEntries)
    {
        //
        // Pickup the last entry and remove it
        //

        CacheEntry = CONTAINING_RECORD(KerbSidCache.List.Blink, KERB_SID_CACHE_ENTRY, Next.Next);

        KerbReferenceListEntry(
            &KerbSidCache,
            &CacheEntry->Next,
            TRUE
            );
        KerbDereferenceSidCacheEntry(
            CacheEntry
            );
        KerbSidCacheEntries--;

    }

}

//+-------------------------------------------------------------------------
//
//  Function:   KerbWriteSidCache
//
//  Synopsis:   Writes the sid cache back to the registry
//
//  Effects:
//
//  Arguments:
//
//  Requires:
//
//  Returns:
//
//  Notes:
//
//
//--------------------------------------------------------------------------



NTSTATUS
KerbWriteSidCache(
    VOID
    )
{
    ULONG Index = 0;
    WCHAR EntryName[KERB_LOGON_SID_CACHE_ENTRY_NAME_SIZE];
    PLIST_ENTRY ListEntry;
    PKERB_SID_CACHE_ENTRY CacheEntry;

    for (ListEntry = KerbSidCache.List.Flink ;
         ListEntry !=  &KerbSidCache.List ;
         ListEntry = ListEntry->Flink )
    {
        CacheEntry = CONTAINING_RECORD(ListEntry, KERB_SID_CACHE_ENTRY, Next.Next);
        swprintf(EntryName,KERB_LOGON_SID_CACHE_ENTRY_NAME, Index++);

        (VOID) RegSetValueEx(
                    KerbSidCacheKey,
                    EntryName,
                    0,                  // reserved
                    REG_BINARY,
                    (PUCHAR) CacheEntry,
                    CacheEntry->Size
                    );
    }

    return(STATUS_SUCCESS);

}

//+-------------------------------------------------------------------------
//
//  Function:   KerbPromoteSidCacheEntry
//
//  Synopsis:   Moves a cache entry to the front of the list
//
//  Effects:
//
//  Arguments:
//
//  Requires:
//
//  Returns:
//
//  Notes:
//
//
//--------------------------------------------------------------------------


VOID
KerbPromoteSidCacheEntry(
    IN PKERB_SID_CACHE_ENTRY CacheEntry
    )
{
    KerbLockList(&KerbSidCache);
    RemoveEntryList(&CacheEntry->Next.Next);
    InsertHeadList(&KerbSidCache.List, &CacheEntry->Next.Next);
    KerbUnlockList(&KerbSidCache);
}


//+-------------------------------------------------------------------------
//
//  Function:   KerbLocateLogonSidCacheEntry
//
//  Synopsis:   Locates a logon sid cache entry by user name and domain name
//
//  Effects:
//
//  Arguments:
//
//  Requires:
//
//  Returns:    returns a referenced cache entry, if available
//
//  Notes:
//
//
//--------------------------------------------------------------------------


PKERB_SID_CACHE_ENTRY
KerbLocateLogonSidCacheEntry(
    IN PUNICODE_STRING LogonUserName,
    IN PUNICODE_STRING LogonDomainName
    )
{
    PKERB_SID_CACHE_ENTRY CacheEntry = NULL;
    PLIST_ENTRY ListEntry;

    if (!KerbGlobalUseSidCache)
    {
        return(NULL);
    }
    KerbLockList(&KerbSidCache);

    for (ListEntry = KerbSidCache.List.Flink ;
         ListEntry !=  &KerbSidCache.List ;
         ListEntry = ListEntry->Flink )
    {
        CacheEntry = CONTAINING_RECORD(ListEntry, KERB_SID_CACHE_ENTRY, Next.Next);

        if (RtlEqualUnicodeString(
                &CacheEntry->LogonUserName,
                LogonUserName,
                TRUE
                ) &&
            RtlEqualUnicodeString(
                &CacheEntry->LogonDomainName,
                LogonDomainName,
                TRUE
                ) )
        {
            //
            // We found it
            //

            KerbReferenceListEntry(
                &KerbSidCache,
                &CacheEntry->Next,
                FALSE                   // don't remove
                );

            break;
        }
        CacheEntry = NULL;
    }


    KerbUnlockList(&KerbSidCache);
    return(CacheEntry);
}



//+-------------------------------------------------------------------------
//
//  Function:   KerbDereferenceSidCacheEntry
//
//  Synopsis:   Dereferences the entry, possibly freeing it
//
//  Effects:
//
//  Arguments:
//
//  Requires:
//
//  Returns:
//
//  Notes:
//
//
//--------------------------------------------------------------------------


VOID
KerbDereferenceSidCacheEntry(
    IN PKERB_SID_CACHE_ENTRY CacheEntry
    )
{
    KerbLockList(&KerbSidCache);
    if (KerbDereferenceListEntry(
            &CacheEntry->Next,
            &KerbSidCache
            ))
    {
        KerbFree(CacheEntry);
    }
    KerbUnlockList(&KerbSidCache);
}

//+-------------------------------------------------------------------------
//
//  Function:   KerbCacheLogonSid
//
//  Synopsis:   Caches a username/domainname & Sid combination for logon
//
//  Effects:    caches the user name/domainname/sid in the registry
//
//  Arguments:  LogonUserName - user name supplied to LogonUser
//              LogonDomainName - domain name supplied to LogonUser
//              LogonRealm - Realm actually containing the account
//              UserSid - Sid of user who just logged on
//
//  Requires:
//
//  Returns:    none - this is just a cache
//
//  Notes:
//
//
//--------------------------------------------------------------------------

VOID
KerbCacheLogonSid(
    IN PUNICODE_STRING LogonUserName,
    IN PUNICODE_STRING LogonDomainName,
    IN PUNICODE_STRING LogonRealm,
    IN PSID UserSid
    )
{
    NTSTATUS Status = STATUS_SUCCESS;
    PKERB_SID_CACHE_ENTRY CacheEntry;
    ULONG CacheEntrySize = 0;
    PUCHAR Where;

    if (!KerbGlobalUseSidCache)
    {
        return;
    }

    CacheEntry = KerbLocateLogonSidCacheEntry(
                        LogonUserName,
                        LogonDomainName
                        );

    //
    // If we found the entry & it is the same as this one, move it up in
    // the list
    //


    if (CacheEntry != NULL)
    {
        if (RtlEqualUnicodeString(
                &CacheEntry->LogonRealm,
                LogonRealm,
                TRUE
                ) &&
            RtlEqualSid(
                CacheEntry->Sid,
                UserSid
                ) )

        {
            KerbPromoteSidCacheEntry(
                CacheEntry
                );
            goto Cleanup;
        }
        else
        {
            //
            // Remove it from the list
            //

            KerbLockList(&KerbSidCache);
            KerbReferenceListEntry(
                &KerbSidCache,
                &CacheEntry->Next,
                TRUE                        // remove
                );

            KerbDereferenceSidCacheEntry(
                CacheEntry
                );
            KerbDereferenceSidCacheEntry(
                CacheEntry
                );
            KerbUnlockList(&KerbSidCache);
            CacheEntry = NULL;
        }
    }

    //
    // Now build a new entry
    //

    CacheEntrySize = sizeof(KERB_SID_CACHE_ENTRY) +
                    RtlLengthSid(UserSid) +
                    LogonUserName->Length + sizeof(WCHAR) +
                    LogonDomainName->Length + sizeof(WCHAR) +
                    LogonRealm->Length + sizeof(WCHAR);

    CacheEntry = (PKERB_SID_CACHE_ENTRY) KerbAllocate(CacheEntrySize);
    if (CacheEntry == NULL)
    {
        Status = STATUS_INSUFFICIENT_RESOURCES;
        goto Cleanup;
    }

    //
    // Fill in all the fixed fields.
    //

    KerbInitializeListEntry(
        &CacheEntry->Next
        );
    CacheEntry->Base = (ULONG_PTR) CacheEntry;
    CacheEntry->Version = KERB_LOGON_SID_CACHE_VERSION;
    CacheEntry->Size = CacheEntrySize;
    Where = (PUCHAR) (CacheEntry + 1);

    //
    // Copy in the sid
    //
    CacheEntry->Sid = (PSID) Where;
    Where += RtlLengthSid( UserSid );

    RtlCopyMemory(
        CacheEntry->Sid,
        UserSid,
        Where - (PUCHAR) CacheEntry->Sid
        );

    //
    // Put the various strings
    //

    KerbPutString(
        LogonUserName,
        &CacheEntry->LogonUserName,
        0,                              // no offset
        &Where
        );

    KerbPutString(
        LogonDomainName,
        &CacheEntry->LogonDomainName,
        0,                              // no offset
        &Where
        );

    KerbPutString(
        LogonRealm,
        &CacheEntry->LogonRealm,
        0,                              // no offset
        &Where
        );

    //
    // Insert it into the list
    //

    KerbLockList( &KerbSidCache );
    KerbInsertListEntry(
        &CacheEntry->Next,
        &KerbSidCache
        );
    //
    // Dereference it because we aren't returning the cache entry
    //

    KerbDereferenceListEntry(
        &CacheEntry->Next,
        &KerbSidCache
        );

    KerbSidCacheEntries++;

    //
    // Remove any extra entries
    //

    KerbScavengeSidCache();

    KerbUnlockList ( &KerbSidCache );

Cleanup:
    if (NT_SUCCESS(Status))
    {
        KerbWriteSidCache();
    }

}



//+-------------------------------------------------------------------------
//
//  Function:   KerbWriteMachineSid
//
//  Synopsis:   Writes the machine account sid to the registry
//
//  Effects:
//
//  Arguments:  MachineSid - If present, is stored. If not present,
//                  is deleted from registry
//
//  Requires:
//
//  Returns:
//
//  Notes:
//
//
//--------------------------------------------------------------------------

VOID
KerbWriteMachineSid(
    IN OPTIONAL PSID MachineSid
    )
{
    if (ARGUMENT_PRESENT(MachineSid))
    {
        (VOID) RegSetValueEx(
                    KerbSidCacheKey,
                    KERB_MACHINE_SID_CACHE_NAME,
                    0,                  // reserved
                    REG_BINARY,
                    (PUCHAR) MachineSid,
                    RtlLengthSid(MachineSid)
                    );
    }
    else
    {
        (VOID) RegDeleteValue(
                    KerbSidCacheKey,
                    KERB_MACHINE_SID_CACHE_NAME
                    );
    }
}


//+-------------------------------------------------------------------------
//
//  Function:   Reads the machine sid from the registry
//
//  Synopsis:
//
//  Effects:
//
//  Arguments:
//
//  Requires:
//
//  Returns:
//
//  Notes:
//
//
//--------------------------------------------------------------------------


NTSTATUS
KerbGetMachineSid(
    OUT PSID * MachineSid
    )
{
    BYTE Buffer[sizeof(SID) + SID_MAX_SUB_AUTHORITIES * sizeof(ULONG)];
    ULONG BufferSize = sizeof(Buffer);
    PSID Sid = (PSID) Buffer;
    DWORD RegErr;
    NTSTATUS Status = STATUS_SUCCESS;
    ULONG ValueType;

    *MachineSid = NULL;
    RegErr = RegQueryValueEx(
                KerbSidCacheKey,
                KERB_MACHINE_SID_CACHE_NAME,
                NULL,
                &ValueType,
                Buffer,
                &BufferSize
                );
    if (RegErr != ERROR_SUCCESS)
    {
        DebugLog((DEB_ERROR,"Failed to query for machine sid value: %d\n",
            RegErr ));
        Status = STATUS_OBJECT_NAME_NOT_FOUND;
        goto Cleanup;
    }

    //
    // If it isn't valid, delete it now
    //

    if (!RtlValidSid(Sid))
    {
        (VOID) RegDeleteValue(
                    KerbSidCacheKey,
                    KERB_MACHINE_SID_CACHE_NAME
                    );
    }

    *MachineSid = (PSID) KerbAllocate(RtlLengthSid(Buffer));

    if (*MachineSid == NULL)
    {
        Status = STATUS_INSUFFICIENT_RESOURCES;
        goto Cleanup;
    }
    RtlCopyMemory(
        *MachineSid,
        Sid,
        RtlLengthSid(Sid)
        );

Cleanup:
    return(Status);

}