/*
 * Copyright (c) 2008-2025 Jonathan Schleifer <js@nil.im>
 *
 * All rights reserved.
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License version 3.0 only,
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
 * version 3.0 for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * version 3.0 along with this program. If not, see
 * <https://www.gnu.org/licenses/>.
 */

#include "config.h"

#import "OFSecureTransportKeychain.h"

#ifndef OF_IOS
# import "OFIRI.h"
# import "OFLocale.h"
# ifdef OF_HAVE_THREADS
#  import "OFMutex.h"
# endif
# import "OFString.h"
# import "OFSystemInfo.h"
# import "OFUUID.h"

# import "OFInitializationFailedException.h"

/*
 * Apple deprecated Secure Transport without providing a replacement that can
 * work with any socket. On top of that, their replacement, Network.framework,
 * doesn't support STARTTLS at all.
 */
# if OF_GCC_VERSION >= 402
#  pragma GCC diagnostic ignored "-Wdeprecated"
# endif

static OFSecureTransportKeychain *temporaryKeychain;
# ifdef OF_HAVE_THREADS
static OFMutex *temporaryKeychainMutex;
# endif

@implementation OFSecureTransportKeychain
@synthesize keychain = _keychain;

static void
cleanup(void)
{
# ifdef OF_HAVE_THREADS
	[temporaryKeychainMutex lock];
	@try {
# endif
		if (temporaryKeychain != nil &&
		    temporaryKeychain->_keychain != NULL)
			SecKeychainDelete(temporaryKeychain->_keychain);
# ifdef OF_HAVE_THREADS
	} @finally {
		[temporaryKeychainMutex unlock];
	}
# endif
}

+ (void)initialize
{
	if (self != [OFSecureTransportKeychain class])
		return;

# ifdef OF_HAVE_THREADS
	temporaryKeychainMutex = [[OFMutex alloc] init];
# endif

	atexit(cleanup);
}

+ (instancetype)temporaryKeychain
{
	OFSecureTransportKeychain *keychain;

# ifdef OF_HAVE_THREADS
	[temporaryKeychainMutex lock];
	@try {
# endif
		if (temporaryKeychain != nil)
			keychain = temporaryKeychain;
		else {
			SecKeychainSettings settings = {
				.version = SEC_KEYCHAIN_SETTINGS_VERS1,
				.lockOnSleep = false,
				.useLockInterval = false,
				.lockInterval = INT_MAX
			};
			void *pool;
			OFString *filename, *path, *password;

			keychain = [[[self alloc] init] autorelease];
			pool = objc_autoreleasePoolPush();
			filename = [OFString stringWithFormat:
			    @"%@.keychain", [OFUUID UUID]];
# ifdef OF_HAVE_FILES
			path = [[OFSystemInfo temporaryDirectoryIRI]
			    IRIByAppendingPathComponent: filename]
			    .fileSystemRepresentation;
# else
			path = [@"/tmp/" stringByAppendingString: filename];
# endif
			password = [OFString stringWithFormat:
			    @"%08X%08X", OFRandom64(), OFRandom64()];

			if (SecKeychainCreate([path cStringWithEncoding:
			    [OFLocale encoding]],
			    (UInt32)password.UTF8StringLength,
			    password.UTF8String, NO, NULL,
			    &keychain->_keychain) != noErr)
				@throw [OFInitializationFailedException
				    exceptionWithClass: self];

			/*
			 * Make sure the keychain never gets locked
			 * automatically, as on the next time it is being used,
			 * the user would be asked to enter the keychain's
			 * password via a popup - a password we randomly
			 * generated and the user cannot know.
			 */
			if (SecKeychainSetSettings(keychain->_keychain,
			    &settings) != noErr)
				@throw [OFInitializationFailedException
				    exceptionWithClass: self];

			objc_autoreleasePoolPop(pool);

			temporaryKeychain = keychain;
		}
# ifdef OF_HAVE_THREADS
	} @finally {
		[temporaryKeychainMutex unlock];
	}
# endif

	return [[keychain retain] autorelease];
}

- (void)dealloc
{
# ifdef OF_HAVE_THREADS
	[temporaryKeychainMutex lock];
	@try {
# endif
		if (self == temporaryKeychain)
			temporaryKeychain = nil;
# ifdef OF_HAVE_THREADS
	} @finally {
		[temporaryKeychainMutex unlock];
	}
# endif

	if (_keychain != NULL) {
		SecKeychainDelete(_keychain);
		[(id)_keychain release];
	}

	[super dealloc];
}
@end
#endif
