--[[----------------------------------------------------------------------------

WFCrypto.lua

--------------------------------------------------------------------------------

ADOBE CONFIDENTIAL
------------------
Copyright 2019 Adobe Inc.
All Rights Reserved.

NOTICE: All information contained herein is, and remain the property of
Adobe Systems Incorporated and its suppliers, if any. The intellectual and
technical concepts contained herein are proprietary to Adobe Systems
Incorporated and its suppliers and may be covered by U.S. and Foreign
Patents, patents in process, and are protected by trade secret or copyright
law.  Dissemination of this information or reproduction of this material is
strictly forbidden unless prior written permission is obtained from
Adobe Systems Incorporated.

------------------------------------------------------------------------------]]

local WFArguments = require 'WFArguments'
local KSStringUtils = require "KSStringUtils"
local KSData = require 'KSData'

local WFRxNativeObservable = require "WFRxNativeObservable"
local WFCryptoImp = require  "WFCryptoImp"

--==============================================================================

KSData.registerArgumentChecker()

local WFCrypto = {}

--------------------------------------------------------------------------------

--[[!
	\class WFCrypto
	\brief This namespace provides static WFDigestProvider instances
		for these digest algorithms:
		<ul>
		<li>WFCrypto.MD4 (only available in Lightroom)</li>
		<li>WFCrypto.MD5</li>
		<li>WFCrypto.SHA1</li>
		<li>WFCrypto.SHA224</li>
		<li>WFCrypto.SHA256</li>
		<li>WFCrypto.SHA384</li>
		<li>WFCrypto.SHA512</li>
		<li>WFCrypto.HMAC</li></ul>
	These instances are static properties of the namespace. They are NOT
	functions, but due to limitations of our documentation engine, they
	are presented here as functions.

	You can use these instances to encode data in a single operation.
	You can also use these singleton instances to create your own instances
	of WFCrypto to perform streaming digest operations.  Streaming data in
	smaller chunks reduces the memory overhead of the digest operation.
]]
	
--[[!
	\brief (WFDigestProvider) The shared singleton instance for MD4.
	THIS IS NOT A FUNCTION, it is a static property of the namespace.
]]
function WFCrypto.MD4()
	-- DOXYGEN STUB
end
	
--[[!
	\brief (WFDigestProvider) The shared singleton instance for MD5.
	THIS IS NOT A FUNCTION, it is a static property of the namespace.
]]
function WFCrypto.MD5()
	-- DOXYGEN STUB
end
	
--[[!
	\brief (WFDigestProvider) The shared singleton instance for SHA1.
	THIS IS NOT A FUNCTION, it is a static property of the namespace.
]]
function WFCrypto.SHA1()
	-- DOXYGEN STUB
end
	
--[[!
	\brief (WFDigestProvider) The shared singleton instance for SHA256.
	THIS IS NOT A FUNCTION, it is a static property of the namespace.
]]
function WFCrypto.SHA224()
	-- DOXYGEN STUB
end

--[[!
	\brief (WFDigestProvider) The shared singleton instance for SHA256.
	THIS IS NOT A FUNCTION, it is a static property of the namespace.
]]
function WFCrypto.SHA256()
	-- DOXYGEN STUB
end
	
--[[!
	\brief (WFDigestProvider) The shared singleton instance for SHA384.
	THIS IS NOT A FUNCTION, it is a static property of the namespace.
]]
function WFCrypto.SHA384()
	-- DOXYGEN STUB
end
	
--[[!
	\brief (WFDigestProvider) The shared singleton instance for SHA512.
	THIS IS NOT A FUNCTION, it is a static property of the namespace.
]]
function WFCrypto.SHA512()
	-- DOXYGEN STUB
end
	
--[[!
	\brief (WFDigestProvider) The shared singleton instance for HMAC.
	THIS IS NOT A FUNCTION, it is a static property of the namespace.
]]
function WFCrypto.HMAC()
	-- DOXYGEN STUB
end

--------------------------------------------------------------------------------

WFArguments.registerChecker( 'WFDigestAlgorithm', {
	MD4 = true,
	MD5 = true,
	SHA1 = true,
	SHA224 = true,
	SHA256 = true,
	SHA384 = true,
	SHA512 = true,
	HMAC = true,
} )

--------------------------------------------------------------------------------

--[[!
	\brief Digest a file by processing it in 4kB chunks.  This
	limits the overall memory consumed by the process.
	
	\param filePath (string) the file path to digest
	\param algorithm (string) the digest algorithm to use
	
	\return The binary digest data as a string, and as a hex string.
]]
function WFCrypto.digestFile( filePath, algorithm )

	WFArguments.check( "+string", "WFDigestAlgorithm" )
	
	local digester = WFCrypto[ algorithm ].init()
	local f = io.open( filePath, "rb" )
	while true do
		local bytes = f:read( 1024 * 1024 * 4 )
		if not bytes then break end
		digester:update( bytes )
	end
	f:close()
	return digester:digest()
	
end

--------------------------------------------------------------------------------

local WFDigestProvider = {}
-- @mark WFDigestProvider namespace

--[[!
	\class WFDigestProvider
	\brief Provides the ability to encode a string or KSBlob in a supported
	digest protocol, directly using the \c digest() function, or by
	streaming data through a WFDigest instance created by the
	WFDigestProvider's \c init() function.
	
	Shared singleton instances for each supported digest algorithm
	are available as static properties of the WFDigest namespace.
--]]

--------------------------------------------------------------------------------

--[[!
	\brief Encodes data according to the digest algorithm,
	returning both the binary digest and a hex-string version of the digested data.
		
	\param stringOrBlob The data to be encoded, as a string or KSData object.
	\param ... For the HMAC protocol ONLY, there are two additional parameters:
	<ul><li>(string) The name of a secondary digest algorithm (such as "SHA1")</li>
		<li> (string or KSData) A secret.</li></ul>
	\return The binary digest data as a string, and as a hex string.
--]]

function WFDigestProvider.digest( --[[string or blob]] stringOrBlob, ... )
	WFArguments.check( 'string|KSData' )
	-- DOXYGEN STUB
end

--------------------------------------------------------------------------------

--[[!
	\brief Creates a WFDigest instance that you can use for a streaming digest operation
		using the algorithm supported by the singleton WFDigestProvider instance.
	
	\param ... For the HMAC protocol ONLY, there are two additional parameters:
	<ul><li>(string) The name of a secondary digest algorithm (such as "SHA1")</li>
		<li> (string or KSData) A secret.</li></ul>

	\return (WFDigest) The new digest object
--]]

function WFDigestProvider.init( ... )
	-- DOXYGEN STUB
	
	-- here to eliminate lint warning
	return WFDigestProvider
end

--------------------------------------------------------------------------------

-- @mark WFCrypto namespace

--[[!
	\brief Allows additional input to the ongoing streaming digest
		operation.
	
	\param input (string or KSData) The additional input.
--]]

function WFCrypto:update( --[[string or KSData]] input )
	-- DOXYGEN STUB
end

--------------------------------------------------------------------------------

--[[!
	\brief Computes the digest for the accumulated bytes in a
		streaming digest operation.
 
 	As a side-effect this WFDigest instance is reset and is ready
	to compute another hash.
	
	\return The binary digest data as a string, and as a hex string.
--]]
function WFCrypto:digest( )
	-- DOXYGEN STUB
end

--------------------------------------------------------------------------------

local function checkDigestAlgorithms( ... )

	local function CheckIsWFDigestAlgorithm( arg )
		WFArguments.check( "WFDigestAlgorithm" )
	end

	local digestCount = select( '#', ... )
	for i = 1, digestCount do
		CheckIsWFDigestAlgorithm( select( i, ... ) )
	end

end

--------------------------------------------------------------------------------

--[[!
	\brief Calculates digests for the specified file on another thread.
	This observable will schedule the digests when the subscription happens.
	\param filePath (string) The absolute path to the file to digest.
	\param ... < strings > The names of the digests to calculate.
	\return Observable that sends a single value that contains the digest values
		as a tuple of strings.
]]--

function WFCrypto.asyncDigestFile( filePath, ... )

	WFArguments.check( "+string" )

	checkDigestAlgorithms( ... )

	return WFRxNativeObservable( WFCrypto.asyncDigestFileImp, filePath, ... )
		:unpack()

end

--------------------------------------------------------------------------------

function WFCrypto.asyncDigestData( data, ... )
	
	checkDigestAlgorithms( ... )
		
	return WFRxNativeObservable( WFCrypto.asyncDigestDataImp, data, ... )
		:unpack()
		
end

--------------------------------------------------------------------------------

--[[!
	\brief Asynchronously generate an RSA key pair.
	The key length is fixed at 4096 bits.
		
	\return Observable<string, string> An observable that yields the private and public key in PEM format.
]]
function WFCrypto.asyncGenerateKeyPair()
	return WFRxNativeObservable( WFCryptoImp.asyncGenerateKeyPairImp )
		:unpack()
end

--------------------------------------------------------------------------------

--[[!
	\brief Generate an RSA key pair.
	The key length is fixed at 4096 bits.
		
	\return private and public key as strings in PEM format.
]]
function WFCrypto.generateKeyPair()
	-- DOXYGEN STUB
end

--------------------------------------------------------------------------------

--[[!
	\brief Generate a Certificate Signing Request.
	For peer-to-peer, the commonName should be the catalog hash.
	
	\param commonName (string) to embed into the Certificate's subject
	\param privateKey (string) the private key - PEM encoded - for the certificate
	\param optAltName (string?) to embed into the Certificate's subject alternative name
		
	\return The CSR as a string in PEM format.
]]
function WFCrypto.generateCSR( commonName, privateKey, optAltName )
	-- DOXYGEN STUB
end

--------------------------------------------------------------------------------

--[[!
	\brief Generate a rendowm password

	
	\param length (integer) length of the password, must be at least 12
	\param optCharset (string?) character set to construct the password (must be at least 32 chars long)
		
	\return The password (string).
]]
function WFCrypto.randomPassword( length, optCharset )
	-- DOXYGEN STUB
end

--------------------------------------------------------------------------------

--[[!
	\brief Read a certificate from disk

	\param fileName file name to a certificate in PEM format

	\return WFCertificate
]]
function WFCrypto.readPEM( fileName )
	-- DOXYGEN STUB
end

--------------------------------------------------------------------------------

--[[!
	\brief Read an identity from disk

	\param fileName file name to a certificate in PEM format
	\param password the password for the PKCS #12 archive

	\return WFCertificate, privateKey
]]
function WFCrypto.readP12( fileName, password )
	-- DOXYGEN STUB
end

--------------------------------------------------------------------------------

--[[!
	\brief This class represents a X509 certificate.

	\param pem Certificate in PEM format
	
	It has the following readonly properties:
	<ul>
		<li>WFCertificate.isValidNow</li>
		<li>WFCertificate.commonName</li>
		<li>WFCertificate.altName</li>
		<li>WFCertificate.notBefore (in ISO-8601 format)</li>
		<li>WFCertificate.notAfter (in ISO-8601 format)</li>
		<li>WFCertificate.pem (the certificate in PEM format))</li>		
	</ul>
]]

function WFCrypto.WFCertificate( pem )
	-- DOXYGEN STUB
end

WFCrypto.WFCertificate = {}


--------------------------------------------------------------------------------

--[[!
	\brief Write an identity (a certificate and its private key) as a PKCS #12 password protected archive 

	\param fileName File name to store the certificate/private key. 
	\param privateKey The PEM-encoded private key
	\param passwd The password for the PKCS12 container	
	\param optFriendlyName This goes into the pkcs12 bag attributes and may be displayed by the client.
		If omitted, the filename will be used.
]]

function WFCrypto.WFCertificate:writeP12( fileName, privateKey, passwd, optFriendlyName )
	-- DOXYGEN STUB
end

--------------------------------------------------------------------------------

--[[!
	\brief Verify a certificate
	
	\param caStore a WFCAStore containing the root certificates to verify against.
		
	\return true or false.
]]

function WFCrypto.WFCertificate:verify( caStore )
	-- DOXYGEN STUB
end

--[[!
	\brief Sign a certificate

	\param csrPem the Certificate Signing Request in PEM format
	\param privateKeyPem The private key of the certificate in PEM format without password
	\param optValidSeconds Validity period in seconds, defaults to two years

	\return a signed WFCertificate
]]

function WFCrypto.WFCertificate:sign( csrPem, privateKeyPem, optValidSeconds )
	-- DOXYGEN STUB
end

--------------------------------------------------------------------------------

--[[!
	\brief This class represents a store of root certificates.
]]

function WFCrypto.WFCAStore()
	-- DOXYGEN STUB
end

WFCrypto.WFCAStore = {
}

--[[!
	\brief Add a certificate to the CAStore
	
	\param certificate WFCertificate to add.
]]
function WFCrypto.WFCAStore:add( certificate )
	-- DOXYGEN STUB
end

--[[!
	\brief Reset the CAStore
]]
function WFCrypto.WFCAStore:reset()
	-- DOXYGEN STUB
end

--[[!
	\brief Return the number of certificates in the CAStore
]]
function WFCrypto.WFCAStore:size()
	-- DOXYGEN STUB
end
--------------------------------------------------------------------------------

-- Most of WFCrypto is implemented natively -- the previous value of the local WFCrypto
-- was mostly for documentation purposes.

for k, v in pairs( WFCryptoImp ) do

	if type( v ) == 'table' then

		-- special handling for digest implementations
		local init = v.init
		
		if init then
		
			v.digest = function( input, ... )
				local ctx = init( ... )
				assert( ctx, "Invalid params" )
				ctx:update( input )
				return ctx:digest( ctx )
			end

		end

	end

	WFCrypto[ k ] = v
end

--[[!
	\brief Split a subject into a table

	\param subject An X.509 subject separated by /, like \c '/C=US/ST=California/L=San Jose/O=Adobe Inc./OU=Digital Imaging/CN=WFCrypto'

	\return a table containing the subject like { C='US', ST= 
]]

function WFCrypto.splitSubject( subject )

	local splitSubject = KSStringUtils.split( subject, '/' )

	-- the subject must start with '/', so the first fragment must be empty
	if splitSubject[ 1 ] ~= '' then
		error( 'invalid subject' )
	end

	local result = {}

	for i = 2, #splitSubject do

		local fragment = KSStringUtils.split( splitSubject[ i ], '=' )

		if #fragment ~= 2 then
			error( string.format( 'invalid subject fragment %s', splitSubject[ i ] ) )
		end

		result[ fragment[ 1 ] ] = fragment[ 2 ]

	end

	return result

end

--------------------------------------------------------------------------------

return WFCrypto