/*
 * Copyright (c) 2008-2024 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"

#include <stdlib.h>

#import "OFPBKDF2.h"
#import "OFHMAC.h"
#import "OFSecureData.h"

#import "OFInvalidArgumentException.h"
#import "OFOutOfMemoryException.h"
#import "OFOutOfRangeException.h"

void
OFPBKDF2(OFPBKDF2Parameters param)
{
	void *pool = objc_autoreleasePoolPush();
	size_t blocks, digestSize = param.HMAC.digestSize;
	OFSecureData *buffer = [OFSecureData
		    dataWithCount: digestSize
	    allowsSwappableMemory: param.allowsSwappableMemory];
	OFSecureData *digest = [OFSecureData
		    dataWithCount: digestSize
	    allowsSwappableMemory: param.allowsSwappableMemory];
	unsigned char *bufferItems = buffer.mutableItems;
	unsigned char *digestItems = digest.mutableItems;
	OFSecureData *extendedSalt;
	unsigned char *extendedSaltItems;

	if (param.HMAC == nil || param.iterations == 0 || param.salt == NULL ||
	    param.password == NULL || param.key == NULL || param.keyLength == 0)
		@throw [OFInvalidArgumentException exception];

	blocks = param.keyLength / digestSize;
	if (param.keyLength % digestSize != 0)
		blocks++;

	if (param.saltLength > SIZE_MAX - 4 || blocks > UINT32_MAX)
		@throw [OFOutOfRangeException exception];

	extendedSalt = [OFSecureData
		    dataWithCount: param.saltLength + 4
	    allowsSwappableMemory: param.allowsSwappableMemory];
	extendedSaltItems = extendedSalt.mutableItems;

	@try {
		uint32_t i = OFToBigEndian32(1);

		[param.HMAC setKey: param.password
			    length: param.passwordLength];

		memcpy(extendedSaltItems, param.salt, param.saltLength);

		while (param.keyLength > 0) {
			size_t length;

			memcpy(extendedSaltItems + param.saltLength, &i, 4);

			[param.HMAC reset];
			[param.HMAC updateWithBuffer: extendedSaltItems
					      length: param.saltLength + 4];
			[param.HMAC calculate];
			memcpy(bufferItems, param.HMAC.digest, digestSize);
			memcpy(digestItems, param.HMAC.digest, digestSize);

			for (size_t j = 1; j < param.iterations; j++) {
				[param.HMAC reset];
				[param.HMAC updateWithBuffer: digestItems
						      length: digestSize];
				[param.HMAC calculate];
				memcpy(digestItems, param.HMAC.digest,
				    digestSize);

				for (size_t k = 0; k < digestSize; k++)
					bufferItems[k] ^= digestItems[k];
			}

			length = digestSize;
			if (length > param.keyLength)
				length = param.keyLength;

			memcpy(param.key, bufferItems, length);
			param.key += length;
			param.keyLength -= length;

			i = OFToBigEndian32(OFFromBigEndian32(i) + 1);
		}
	} @catch (id e) {
		[extendedSalt zero];
		[buffer zero];
		[digest zero];

		@throw e;
	} @finally {
		[param.HMAC zero];
	}

	objc_autoreleasePoolPop(pool);
}
