// Copyright 1997 The Open Group Research Institute.  All rights reserved.

package krb5.lib;

import krb5.lib.asn1.*;
import krb5.lib.crypto.*;
import java.net.InetAddress;
import java.net.UnknownHostException;

public class ApiUtil {

	public static ASReq as_req(
		PAData[] paData,
		KDCOptions kdc_options,
		PrincipalName cname,
		Realm crealm,
		PrincipalName sname,
		KerberosTime from,
		KerberosTime till,
		KerberosTime rtime,
		int[] eTypes,
		HostAddresses addresses,
		Ticket[] additionalTickets
		) throws Asn1Exception,	BitStringException,	KrbApErrException,
		RealmException,	UnknownHostException {

		PrincipalName req_sname = null;
		if (sname == null)
			req_sname = new PrincipalName(
				Krb5.TGS_DEFAULT_SRV_NAME +
				String.valueOf(Krb5.NAME_COMPONENT_SEPARATOR) +
				crealm.toString(), Krb5.KRB_NT_UNKNOWN);
		else
			req_sname = sname;
			
		KerberosTime req_till = null;
		if (till == null)
			req_till = new KerberosTime();
		else
			req_till = till;

		int[] req_eTypes = null;
		if (eTypes == null)
			req_eTypes = Config.ETYPE_LIST_DEFAULT;
		else
			req_eTypes = eTypes;

		KDCReqBody kdc_req_body = new KDCReqBody(
			kdc_options,
			cname,
			crealm,
			req_sname,
			from,
			req_till,
			rtime,
			Nonce.value(),
			req_eTypes,
			addresses,
			null,
			additionalTickets
		);

		return new ASReq(
			paData, 
			kdc_req_body
		);
	}

	public static TGSReq tgs_req(
			KDCOptions kdc_options,
			Ticket ticket,
			EncryptionKey key,
			KerberosTime ctime,
			PrincipalName cname,
			Realm crealm,
			PrincipalName sname,
			KerberosTime from,
			KerberosTime till,
			KerberosTime rtime,
			int[] eTypes,
			HostAddresses addresses,
			AuthorizationData authorizationData,
			Ticket[] additionalTickets,
			EncryptionKey subKey
		) throws Asn1Exception, BitStringException, KdcErrException,
		UnknownHostException {

		KerberosTime req_till = null;
		if (till == null)
			req_till = new KerberosTime();
		else
			req_till = till;

		int[] req_eTypes = null;
		if (eTypes == null)
			req_eTypes = Config.ETYPE_LIST_DEFAULT;
		else
			req_eTypes = eTypes;

		EncryptionKey reqKey = null;
		EncryptedData encAuthorizationData = null;
		if (authorizationData != null) {
			byte[] ad = authorizationData.asn1Encode();
			if (subKey != null) {
				reqKey = subKey;
				encAuthorizationData = new EncryptedData(reqKey, ad);
			}
			else
				encAuthorizationData = new EncryptedData(key, ad);
		}

		KDCReqBody reqBody = new KDCReqBody(
			kdc_options,
			cname,
			crealm,
			sname,
			from,
			req_till,
			rtime,
			Nonce.value(),
			req_eTypes,
			addresses,
			encAuthorizationData,
			additionalTickets
		);

		byte[] temp = reqBody.asn1Encode(Krb5.KRB_TGS_REQ);

		Checksum cksum = new Checksum(temp);
		
		byte[] tgs_ap_req = ap_req(
			new APOptions(),
			ticket,
			key,
			crealm,
			cname,
			cksum,
			ctime,
			reqKey,
			null,
			null
		);
		
		PAData[] tgsPAData = new PAData[1];
		tgsPAData[0] = new PAData(Krb5.PA_TGS_REQ, tgs_ap_req);

		return new TGSReq(tgsPAData, reqBody);
	}

	public static byte[] ap_req(
			APOptions apOptions,
			Ticket ticket,
			EncryptionKey key,
			Realm crealm,
			PrincipalName cname,
			Checksum cksum,
			KerberosTime ctime,
			EncryptionKey subKey,
			SeqNumber seqNumber,
			AuthorizationData authorizationData
		) throws Asn1Exception, BitStringException, KdcErrException {

		Integer seqno = null;
		
		if (seqNumber != null)
			seqno = new Integer(seqNumber.current());

		Authenticator unenc_authenticator = new Authenticator(
			crealm,
			cname,
			cksum,
			ctime.getMicroSeconds(),
			ctime,
			subKey,
			seqno,
			authorizationData
		);

		byte[] temp = unenc_authenticator.asn1Encode();

		EncryptedData authenticator = new EncryptedData(
			key,
			temp
		);

		APReq ap_req = new APReq(
			apOptions,
			ticket,
			authenticator
		);
		temp = ap_req.asn1Encode();

		return ap_req.asn1Encode();
	}

	public static byte[] ap_rep(
			EncryptionKey key,
			KerberosTime ctime,
			EncryptionKey subKey,
			Integer seqNumber
		) throws Asn1Exception, BitStringException, KdcErrException {

		EncAPRepPart enc_ap_rep_part = new EncAPRepPart(
			ctime,
			ctime.getMicroSeconds(),
			subKey,
			seqNumber
		);

		byte[] enc_part = enc_ap_rep_part.asn1Encode();
		
		EncryptedData new_encPart = new EncryptedData(
			key,
			enc_part
		);
			
		APRep rep = new APRep(new_encPart);

		return rep.asn1Encode();
	}

	public static byte[] mk_priv(
		byte[] userData,
		EncryptionKey key,
		KerberosTime timestamp,
		SeqNumber seqNumber,
		HostAddress sAddress,
		HostAddress rAddress
		) throws Asn1Exception, BitStringException, KdcErrException {

		Integer usec = null;
		Integer seqno = null;
		
		if (timestamp != null)
			usec = new Integer(timestamp.getMicroSeconds());

		if (seqNumber != null) {
			seqno = new Integer(seqNumber.current());
			seqNumber.step();
		}

		EncKrbPrivPart unenc_encKrbPrivPart = new EncKrbPrivPart(
			userData,
			timestamp,
			usec,
			seqno,
			sAddress,
			rAddress
		);

		byte[] temp = unenc_encKrbPrivPart.asn1Encode();

		EncryptedData encKrbPrivPart = new EncryptedData(
			key,
			temp
		);

		KRBPriv krb_priv = new KRBPriv(encKrbPrivPart);

		temp = krb_priv.asn1Encode();

		return krb_priv.asn1Encode();
	}

	public static byte[] rd_priv(
		KRBPriv krb_priv,
		EncryptionKey key,
		SeqNumber seqNumber,
		HostAddress sAddress,
		HostAddress rAddress,
		boolean timestampRequired,
		boolean seqNumberRequired,
		PrincipalName cname,
		Realm crealm
		) throws Asn1Exception, KdcErrException, KrbApErrException {
		
		byte[] temp = krb_priv.encPart.decrypt(key);
		EncodeRef ref = new EncodeRef(temp);
		EncKrbPrivPart enc_part = new EncKrbPrivPart(ref);

		krb_priv_safe_common_checks(
			enc_part.timestamp,
			enc_part.usec,
			enc_part.seqNumber,
			enc_part.sAddress,
			enc_part.rAddress,
			seqNumber,
			sAddress,
			rAddress,
			timestampRequired,
			seqNumberRequired,
			cname,
			crealm
		);

		return enc_part.userData;
	}

	public static byte[] mk_safe(
		byte[] userData,
		EncryptionKey key,
		KerberosTime timestamp,
		SeqNumber seqNumber,
		HostAddress sAddress,
		HostAddress rAddress
		) throws Asn1Exception, BitStringException, KdcErrException,
		KrbApErrException {

		Integer usec = null;
		Integer seqno = null;
		
		if (timestamp != null)
			usec = new Integer(timestamp.getMicroSeconds());

		if (seqNumber != null) {
			seqno = new Integer(seqNumber.current());
			seqNumber.step();
		}

		KRBSafeBody krb_safeBody = new KRBSafeBody(
			userData,
			timestamp,
			usec,
			seqno,
			sAddress,
			rAddress
		);

		byte[] temp = krb_safeBody.asn1Encode();

		Checksum cksum = new Checksum(
			Config.SAFECKSUMTYPE_DEFAULT,
			temp,
			key
		);

		KRBSafe krb_safe = new KRBSafe(
			krb_safeBody,
			cksum
		);

		temp = krb_safe.asn1Encode();

		return krb_safe.asn1Encode();
	}

	public static byte[] rd_safe(
		KRBSafe krb_safe,
		EncryptionKey key,
		SeqNumber seqNumber,
		HostAddress sAddress,
		HostAddress rAddress,
		boolean timestampRequired,
		boolean seqNumberRequired,
		PrincipalName cname,
		Realm crealm
		) throws Asn1Exception, KdcErrException, KrbApErrException {
		
		byte[] temp = krb_safe.safeBody.asn1Encode();

		if (!krb_safe.cksum.verifyKeyedChecksum(temp, key))
			throw new KrbApErrException(Krb5.KRB_AP_ERR_MODIFIED);

		krb_priv_safe_common_checks(
			krb_safe.safeBody.timestamp,
			krb_safe.safeBody.usec,
			krb_safe.safeBody.seqNumber,
			krb_safe.safeBody.sAddress,
			krb_safe.safeBody.rAddress,
			seqNumber,
			sAddress,
			rAddress,
			timestampRequired,
			seqNumberRequired,
			cname,
			crealm
		);

		return krb_safe.safeBody.userData;
	}

	public static void krb_priv_safe_common_checks(
		KerberosTime packetTimestamp,
		Integer packetUsec,
		Integer packetSeqNumber,
		HostAddress packetSAddress,
		HostAddress packetRAddress,
		SeqNumber seqNumber,
		HostAddress sAddress,
		HostAddress rAddress,
		boolean timestampRequired,
		boolean seqNumberRequired,
		PrincipalName packetPrincipal,
		Realm packetRealm
		) throws KrbApErrException {

		if (!Config.AP_EMPTY_ADDRESSES_ALLOWED || sAddress != null) {
			if (packetSAddress == null || sAddress == null ||
				!packetSAddress.equals(sAddress))
				throw new KrbApErrException(Krb5.KRB_AP_ERR_BADADDR);
		}
		
		if (!Config.AP_EMPTY_ADDRESSES_ALLOWED || rAddress != null) {
			if (packetRAddress == null || rAddress == null ||
				!packetRAddress.equals(rAddress))
				throw new KrbApErrException(Krb5.KRB_AP_ERR_BADADDR);
		}
		
		if (packetTimestamp != null) {
			packetTimestamp.setMicroSeconds(packetUsec);
			if (!packetTimestamp.inClockSkew())
				throw new KrbApErrException(Krb5.KRB_AP_ERR_SKEW);
		}
		else
			if (timestampRequired)
				throw new KrbApErrException(Krb5.KRB_AP_ERR_SKEW);

		//XXX replay cache not yet implemented
		//if (rcache.repeated(packetTimestamp, packetUsec, packetSAddress))
		//	throw new KrbApErrException(Krb5.KRB_AP_ERR_REPEAT);

		//XXX consider moving up to api level
		if (seqNumber == null && seqNumberRequired == true)
			//XXX should be KrbException
			throw new KrbApErrException(Krb5.API_INVALID_ARG);

		if (packetSeqNumber != null && seqNumber != null) {
			if (packetSeqNumber.intValue() != seqNumber.current())
				throw new KrbApErrException(Krb5.KRB_AP_ERR_BADORDER);
			//XXX should be done only when no more exceptions are possible
			seqNumber.step();
		}
		else
			if (seqNumberRequired)
				throw new KrbApErrException(Krb5.KRB_AP_ERR_BADORDER);
		
		//Must not be relaxed, per RFC 1510
		if (packetTimestamp == null && packetSeqNumber == null)
			throw new KrbApErrException(Krb5.KRB_AP_ERR_MODIFIED);

		//XXX replay cache not yet implemented
		//rcache.save_identifier(packetTimestamp, packetUsec, packetSAddress,
		//	packetPrincipal, pcaketRealm);
	}

	public static void krb_as_tgs_rep_common_checks(
		KDCReq req,
		KDCRep rep
		) throws BitStringException, KrbApErrException {

		if (!req.reqBody.cname.equalsWithoutRealm(rep.cname)) {
			rep.encKDCRepPart.key.destroy();
			throw new KrbApErrException(Krb5.KRB_AP_ERR_MODIFIED);
		}

		if (!req.reqBody.crealm.equals(rep.crealm)) {
			rep.encKDCRepPart.key.destroy();
			throw new KrbApErrException(Krb5.KRB_AP_ERR_MODIFIED);
		}

		if (!req.reqBody.sname.equalsWithoutRealm(rep.encKDCRepPart.sname)) {
			rep.encKDCRepPart.key.destroy();
			throw new KrbApErrException(Krb5.KRB_AP_ERR_MODIFIED);
		}

		if (!req.reqBody.crealm.equals(rep.encKDCRepPart.srealm)) {
			rep.encKDCRepPart.key.destroy();
			throw new KrbApErrException(Krb5.KRB_AP_ERR_MODIFIED);
		}

		if (
		    //XXX Microsoft doesn't include addresses in the ticket
		    //(req.reqBody.addresses == null && rep.encKDCRepPart.caddr != null) ||
			//(req.reqBody.addresses != null && rep.encKDCRepPart.caddr == null) ||
			((req.reqBody.addresses != null && rep.encKDCRepPart.caddr != null) &&
			!req.reqBody.addresses.equals(rep.encKDCRepPart.caddr))) {
			rep.encKDCRepPart.key.destroy();
			throw new KrbApErrException(Krb5.KRB_AP_ERR_MODIFIED);
		}

		//XXX check flags for compatibility; may need more work
		if (!req.reqBody.kdcOptions.match(rep.encKDCRepPart.flags,
				Krb5.KDC_OPTS_FORWARDABLE) ||
			!req.reqBody.kdcOptions.match(rep.encKDCRepPart.flags,
				Krb5.KDC_OPTS_FORWARDED) ||
			!req.reqBody.kdcOptions.match(rep.encKDCRepPart.flags,
				Krb5.KDC_OPTS_PROXIABLE) ||
			!req.reqBody.kdcOptions.match(rep.encKDCRepPart.flags,
				Krb5.KDC_OPTS_PROXY) ||
			!req.reqBody.kdcOptions.match(rep.encKDCRepPart.flags,
				Krb5.KDC_OPTS_ALLOW_POSTDATE) ||
			!req.reqBody.kdcOptions.match(rep.encKDCRepPart.flags,
				Krb5.KDC_OPTS_POSTDATED) ||
			!req.reqBody.kdcOptions.match(rep.encKDCRepPart.flags,
				Krb5.KDC_OPTS_RENEWABLE) ||
			!req.reqBody.kdcOptions.match(rep.encKDCRepPart.flags,
				Krb5.KDC_OPTS_RENEWABLE_OK) ||
			!req.reqBody.kdcOptions.match(rep.encKDCRepPart.flags,
				Krb5.KDC_OPTS_ENC_TKT_IN_SKEY)) {
			rep.encKDCRepPart.key.destroy();
			throw new KrbApErrException(Krb5.KRB_AP_ERR_MODIFIED);
		}

		if ((req.reqBody.from == null) || req.reqBody.from.isZero())
			if ((rep.encKDCRepPart.starttime != null) && //XXX verify this is allowed
				!rep.encKDCRepPart.starttime.inClockSkew()) {
				rep.encKDCRepPart.key.destroy();
				throw new KrbApErrException(Krb5.KRB_AP_ERR_SKEW);
			}

		if ((req.reqBody.from != null) && !req.reqBody.from.isZero())
			if ((rep.encKDCRepPart.starttime != null) && //XXX verify this is allowed
				!req.reqBody.from.equals(rep.encKDCRepPart.starttime)) {
				rep.encKDCRepPart.key.destroy();
				throw new KrbApErrException(Krb5.KRB_AP_ERR_MODIFIED);
			}

		if (!req.reqBody.till.isZero() &&
			rep.encKDCRepPart.endtime.greaterThan(req.reqBody.till)) {
			rep.encKDCRepPart.key.destroy();
			throw new KrbApErrException(Krb5.KRB_AP_ERR_MODIFIED);
		}

		if (req.reqBody.kdcOptions.get(Krb5.KDC_OPTS_RENEWABLE))
			if (req.reqBody.rtime != null && !req.reqBody.rtime.isZero())
				if ((rep.encKDCRepPart.renewTill == null) || //XXX verify this is required
					rep.encKDCRepPart.renewTill.greaterThan(req.reqBody.rtime)
					) {
					rep.encKDCRepPart.key.destroy();
					throw new KrbApErrException(Krb5.KRB_AP_ERR_MODIFIED);
				}

		if (req.reqBody.kdcOptions.get(Krb5.KDC_OPTS_RENEWABLE_OK) &&
			rep.encKDCRepPart.flags.get(Krb5.KDC_OPTS_RENEWABLE))
			if (!req.reqBody.till.isZero())
				if ((rep.encKDCRepPart.renewTill == null) || //XXX verify this is required
					rep.encKDCRepPart.renewTill.greaterThan(req.reqBody.till)
					) {
					rep.encKDCRepPart.key.destroy();
					throw new KrbApErrException(Krb5.KRB_AP_ERR_MODIFIED);
				}
	}

}