Secret.c   [plain text]


/* -*- Mode: C; tab-width: 4 -*-
 *
 * Copyright (c) 2002-2013 Apple Computer, Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "Secret.h"
#include <stdarg.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#include <process.h>
#include <ntsecapi.h>
#include <lm.h>
#include "DebugServices.h"


mDNSlocal OSStatus MakeLsaStringFromUTF8String( PLSA_UNICODE_STRING output, const char * input );
mDNSlocal OSStatus MakeUTF8StringFromLsaString( char * output, size_t len, PLSA_UNICODE_STRING input );


mDNSBool
LsaGetSecret( const char * inDomain, char * outDomain, unsigned outDomainSize, char * outKey, unsigned outKeySize, char * outSecret, unsigned outSecretSize )
{
	PLSA_UNICODE_STRING		domainLSA;
	PLSA_UNICODE_STRING		keyLSA;
	PLSA_UNICODE_STRING		secretLSA;
	size_t					i;
	size_t					dlen;
	LSA_OBJECT_ATTRIBUTES	attrs;
	LSA_HANDLE				handle = NULL;
	NTSTATUS				res;
	OSStatus				err;

	check( inDomain );
	check( outDomain );
	check( outKey );
	check( outSecret );

	// Initialize

	domainLSA	= NULL;
	keyLSA		= NULL;
	secretLSA	= NULL;

	// Make sure we have enough space to add trailing dot

	dlen = strlen( inDomain );
	err = strcpy_s( outDomain, outDomainSize - 2, inDomain );
	require_noerr( err, exit );

	// If there isn't a trailing dot, add one because the mDNSResponder
	// presents names with the trailing dot.

	if ( outDomain[ dlen - 1 ] != '.' )
	{
		outDomain[ dlen++ ] = '.';
		outDomain[ dlen ] = '\0';
	}

	// Canonicalize name by converting to lower case (keychain and some name servers are case sensitive)

	for ( i = 0; i < dlen; i++ )
	{
		outDomain[i] = (char) tolower( outDomain[i] );  // canonicalize -> lower case
	}

	// attrs are reserved, so initialize to zeroes.

	ZeroMemory( &attrs, sizeof( attrs ) );

	// Get a handle to the Policy object on the local system

	res = LsaOpenPolicy( NULL, &attrs, POLICY_GET_PRIVATE_INFORMATION, &handle );
	err = translate_errno( res == 0, LsaNtStatusToWinError( res ), kUnknownErr );
	require_noerr( err, exit );

	// Get the encrypted data

	domainLSA = ( PLSA_UNICODE_STRING ) malloc( sizeof( LSA_UNICODE_STRING ) );
	require_action( domainLSA != NULL, exit, err = mStatus_NoMemoryErr );
	err = MakeLsaStringFromUTF8String( domainLSA, outDomain );
	require_noerr( err, exit );

	// Retrieve the key

	res = LsaRetrievePrivateData( handle, domainLSA, &keyLSA );
	err = translate_errno( res == 0, LsaNtStatusToWinError( res ), kUnknownErr );
	require_noerr_quiet( err, exit );

	// <rdar://problem/4192119> Lsa secrets use a flat naming space.  Therefore, we will prepend "$" to the keyname to
	// make sure it doesn't conflict with a zone name.	
	// Strip off the "$" prefix.

	err = MakeUTF8StringFromLsaString( outKey, outKeySize, keyLSA );
	require_noerr( err, exit );
	require_action( outKey[0] == '$', exit, err = kUnknownErr );
	memcpy( outKey, outKey + 1, strlen( outKey ) );

	// Retrieve the secret

	res = LsaRetrievePrivateData( handle, keyLSA, &secretLSA );
	err = translate_errno( res == 0, LsaNtStatusToWinError( res ), kUnknownErr );
	require_noerr_quiet( err, exit );

	// Convert the secret to UTF8 string

	err = MakeUTF8StringFromLsaString( outSecret, outSecretSize, secretLSA );
	require_noerr( err, exit );

exit:

	if ( domainLSA != NULL )
	{
		if ( domainLSA->Buffer != NULL )
		{
			free( domainLSA->Buffer );
		}

		free( domainLSA );
	}

	if ( keyLSA != NULL )
	{
		LsaFreeMemory( keyLSA );
	}

	if ( secretLSA != NULL )
	{
		LsaFreeMemory( secretLSA );
	}

	if ( handle )
	{
		LsaClose( handle );
		handle = NULL;
	}

	return ( !err ) ? TRUE : FALSE;
}


mDNSBool
LsaSetSecret( const char * inDomain, const char * inKey, const char * inSecret )
{
	size_t					inDomainLength;
	size_t					inKeyLength;
	char					domain[ 1024 ];
	char					key[ 1024 ];
	LSA_OBJECT_ATTRIBUTES	attrs;
	LSA_HANDLE				handle = NULL;
	NTSTATUS				res;
	LSA_UNICODE_STRING		lucZoneName;
	LSA_UNICODE_STRING		lucKeyName;
	LSA_UNICODE_STRING		lucSecretName;
	BOOL					ok = TRUE;
	OSStatus				err;

	require_action( inDomain != NULL, exit, ok = FALSE );
	require_action( inKey != NULL, exit, ok = FALSE );
	require_action( inSecret != NULL, exit, ok = FALSE );

	// If there isn't a trailing dot, add one because the mDNSResponder
	// presents names with the trailing dot.

	ZeroMemory( domain, sizeof( domain ) );
	inDomainLength = strlen( inDomain );
	require_action( inDomainLength > 0, exit, ok = FALSE );
	err = strcpy_s( domain, sizeof( domain ) - 2, inDomain );
	require_action( !err, exit, ok = FALSE );

	if ( domain[ inDomainLength - 1 ] != '.' )
	{
		domain[ inDomainLength++ ] = '.';
		domain[ inDomainLength ] = '\0';
	}

	// <rdar://problem/4192119>
	//
	// Prepend "$" to the key name, so that there will
	// be no conflict between the zone name and the key
	// name

	ZeroMemory( key, sizeof( key ) );
	inKeyLength = strlen( inKey );
	require_action( inKeyLength > 0 , exit, ok = FALSE );
	key[ 0 ] = '$';
	err = strcpy_s( key + 1, sizeof( key ) - 3, inKey );
	require_action( !err, exit, ok = FALSE );
	inKeyLength++;

	if ( key[ inKeyLength - 1 ] != '.' )
	{
		key[ inKeyLength++ ] = '.';
		key[ inKeyLength ] = '\0';
	}

	// attrs are reserved, so initialize to zeroes.

	ZeroMemory( &attrs, sizeof( attrs ) );

	// Get a handle to the Policy object on the local system

	res = LsaOpenPolicy( NULL, &attrs, POLICY_ALL_ACCESS, &handle );
	err = translate_errno( res == 0, LsaNtStatusToWinError( res ), kUnknownErr );
	require_noerr( err, exit );

	// Intializing PLSA_UNICODE_STRING structures

	err = MakeLsaStringFromUTF8String( &lucZoneName, domain );
	require_noerr( err, exit );

	err = MakeLsaStringFromUTF8String( &lucKeyName, key );
	require_noerr( err, exit );

	err = MakeLsaStringFromUTF8String( &lucSecretName, inSecret );
	require_noerr( err, exit );

	// Store the private data.

	res = LsaStorePrivateData( handle, &lucZoneName, &lucKeyName );
	err = translate_errno( res == 0, LsaNtStatusToWinError( res ), kUnknownErr );
	require_noerr( err, exit );

	res = LsaStorePrivateData( handle, &lucKeyName, &lucSecretName );
	err = translate_errno( res == 0, LsaNtStatusToWinError( res ), kUnknownErr );
	require_noerr( err, exit );

exit:

	if ( handle )
	{
		LsaClose( handle );
		handle = NULL;
	}

	return ok;
}


//===========================================================================================================================
//	MakeLsaStringFromUTF8String
//===========================================================================================================================

mDNSlocal OSStatus
MakeLsaStringFromUTF8String( PLSA_UNICODE_STRING output, const char * input )
{
	int			size;
	OSStatus	err;
	
	check( input );
	check( output );

	output->Buffer = NULL;

	size = MultiByteToWideChar( CP_UTF8, 0, input, -1, NULL, 0 );
	err = translate_errno( size > 0, GetLastError(), kUnknownErr );
	require_noerr( err, exit );

	output->Length = (USHORT)( size * sizeof( wchar_t ) );
	output->Buffer = (PWCHAR) malloc( output->Length );
	require_action( output->Buffer, exit, err = mStatus_NoMemoryErr );
	size = MultiByteToWideChar( CP_UTF8, 0, input, -1, output->Buffer, size );
	err = translate_errno( size > 0, GetLastError(), kUnknownErr );
	require_noerr( err, exit );

	// We're going to subtrace one wchar_t from the size, because we didn't
	// include it when we encoded the string

	output->MaximumLength = output->Length;
	output->Length		-= sizeof( wchar_t );
	
exit:

	if ( err && output->Buffer )
	{
		free( output->Buffer );
		output->Buffer = NULL;
	}

	return( err );
}



//===========================================================================================================================
//	MakeUTF8StringFromLsaString
//===========================================================================================================================

mDNSlocal OSStatus
MakeUTF8StringFromLsaString( char * output, size_t len, PLSA_UNICODE_STRING input )
{
	size_t		size;
	OSStatus	err = kNoErr;

	// The Length field of this structure holds the number of bytes,
	// but WideCharToMultiByte expects the number of wchar_t's. So
	// we divide by sizeof(wchar_t) to get the correct number.

	size = (size_t) WideCharToMultiByte(CP_UTF8, 0, input->Buffer, ( input->Length / sizeof( wchar_t ) ), NULL, 0, NULL, NULL);
	err = translate_errno( size != 0, GetLastError(), kUnknownErr );
	require_noerr( err, exit );
	
	// Ensure that we have enough space (Add one for trailing '\0')

	require_action( ( size + 1 ) <= len, exit, err = mStatus_NoMemoryErr );

	// Convert the string

	size = (size_t) WideCharToMultiByte( CP_UTF8, 0, input->Buffer, ( input->Length / sizeof( wchar_t ) ), output, (int) size, NULL, NULL);	
	err = translate_errno( size != 0, GetLastError(), kUnknownErr );
	require_noerr( err, exit );

	// have to add the trailing 0 because WideCharToMultiByte doesn't do it,
	// although it does return the correct size

	output[size] = '\0';

exit:

	return err;
}