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

WFCatP2PIdentityManager.lua

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

ADOBE CONFIDENTIAL
------------------
Copyright 2020 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.


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

	-- Rx
local Rx = require 'Rx'
local Observable = Rx.Observable

local KSDate = require "KSDate"
local KSPathUtils = require "KSPathUtils"
local KSJsonDecoder = require "KSJsonDecoder"
local KSKeychainService = require "KSKeychainService"

local WFFileUtils = require "WFFileUtils"

local WFCrypto = require "WFCrypto"

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

local WFCatP2PIdentityManager = {}

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

local kMinimumValidityBeforeRenewal = 30 -- days

WFCatP2PIdentityManager.kCertificateServiceUrl = 'https://p2p.oztrunk.com/request_certificate'

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

function WFCatP2PIdentityManager.defaultIdentityFileName( commonName, optSuffix )

	local suffix = optSuffix or '.p12'

	return KSPathUtils.child( KSPathUtils.getStandardFilePath( 'adobeAppData' ), commonName .. suffix )
	
end

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

function WFCatP2PIdentityManager.defaultCAFileName()

	return KSPathUtils.child( KSPathUtils.getStandardFilePath( 'adobeAppData' ), 'adobeCA.pem' )

end

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

--[[!
	\brief Get or request an identity (certificate + plus private key) suitable for peer-to-peer.

	\param catalog (wfCatalog) The catalog
	\param commonName (string) The commonName for the certificate (typically a hash of the catalogId)
	\param options (optional, table) configuration options

		<ul>
			<li> \c reset (boolean): Remove the existing certificate and CA (forces a re-request).</li>
			<li> \c url (string) URL for the certificate server.</li>
			<li> \c fileName (string) The filename for the identity. 
				A default filename will be used if this is not set.</li>
			<li> \c caFileName (string) TThe filename for the CA Certificate. 
				A default filename will be used if this is not set.</li>
		</ul>

	\return return <code>Observable< string, string, string></code> Resolves to fileName and password of a pkcs #12 file
	containing the identity. The third string is a fileName containing the Adobe Device CA in PEM format.

]]
function WFCatP2PIdentityManager.getOrRequestIdentity( catalog, commonName, options )

	-- The identity (a certificate plus the private key used for the CSR) is stored in a pkcs #12 file
	-- that is protected by a 64 char long random password that is itself stored in the keychain.

	options = options or {}

	local fileName = options.fileName or WFCatP2PIdentityManager.defaultIdentityFileName( commonName )
	local caFileName = options.caFileName or WFCatP2PIdentityManager.defaultCAFileName()

	if options.reset then
		WFFileUtils.deleteFile( fileName )
		WFFileUtils.deleteFile( caFileName )
	end

	local url = options.url or WFCatP2PIdentityManager.kCertificateServiceUrl

	-- SPAM [immisch 2020-03-30]: we store the password for the pkcs #12 file under the commonName (i.e. catalog hash)
	-- in the keychain. If no password exists, a new password is generated.

	return KSKeychainService.read( commonName )

		:flatMap( function( maybePassword )

			if not maybePassword then

				local password = WFCrypto.randomPassword( 64 )

				return KSKeychainService.write( commonName, password )
					:defaultIfEmpty( password )

			else
				return Observable.just( maybePassword )
			end

		end )

		-- we've got the password
		:flatMap( function( password )

			local privateKey

			if WFFileUtils.exists( fileName ) and WFFileUtils.exists( caFileName ) then

				local success, certificateOrError
				success, certificateOrError, privateKey = pcall( WFCrypto.readP12, fileName, password )

				if success then

					if KSDate.differenceInDays( KSDate.timeInIso8601Format(), certificateOrError.notAfter ) > kMinimumValidityBeforeRenewal then

						-- certificate is still valid, check CA validity

						local caCertificate = WFCrypto.readPEM( caFileName )

						if caCertificate and caCertificate.isValidNow then
							return Rx.Observable.just( fileName, password, caFileName )
						end

					end
				end

			end

			local privateKeyObservable

			if privateKey then
				privateKeyObservable = Rx.Observable.just( privateKey )
			else
				privateKeyObservable = WFCrypto.asyncGenerateKeyPair()
			end

			return privateKeyObservable
				:flatMap( function( privateKey )

					local csr = WFCrypto.generateCSR( commonName, privateKey )

					local catSpec = catalog:ozCatalogSpec()

					return catSpec:put( url, csr, { requestMethod = 'POST' } )
						:flatMap( function( data, headers )

							if headers.status_code == '200' then

								local certData = KSJsonDecoder.decodeWebSafeJson( data )

								if not certData.Certificate then
									return Observable.throw( 'request to certificate server failed ' .. url )
								end

								local certificate = WFCrypto.WFCertificate( certData.Certificate )

								local caCertificate = WFCrypto.WFCertificate( certData.CertificateChain )

								if not caCertificate.isValidNow then
									return Observable.throw( 'Adobe Device CA Certificate is expired' )
								end

								caCertificate:writePEM( caFileName )
								certificate:writeP12( fileName, privateKey, password )

								return Observable.just( fileName, password, caFileName )

							else
								return Observable.throw( 'request to certificate server failed ' .. url )
							end

						end )

				end )
		end )

end

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

return WFCatP2PIdentityManager