//
// Copyright (c) SAS Institute Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

package authenticode

import (
	"bufio"
	"bytes"
	"context"
	"crypto"
	"crypto/hmac"
	"encoding/base64"
	"encoding/binary"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"path/filepath"
	"strings"
	"unicode/utf16"

	"github.com/sassoftware/relic/lib/binpatch"
	"github.com/sassoftware/relic/lib/certloader"
	"github.com/sassoftware/relic/lib/pkcs7"
	"github.com/sassoftware/relic/lib/pkcs9"
	"github.com/sassoftware/relic/lib/x509tools"
	"github.com/sassoftware/relic/signers/sigerrors"
)

// Type of signature formatting used for different PowerShell file formats
type PsSigStyle int

const (
	// "Hash" style used by e.g. .ps1 files
	SigStyleHash PsSigStyle = iota + 1
	// XML style used by e.g. .ps1xml files
	SigStyleXML
	// C# style used by .mof files
	SigStyleC
)

var psExtMap = map[string]PsSigStyle{
	".ps1":    SigStyleHash,
	".ps1xml": SigStyleXML,
	".psc1":   SigStyleXML,
	".psd1":   SigStyleHash,
	".psm1":   SigStyleHash,
	".cdxml":  SigStyleXML,
	".mof":    SigStyleC,
}

const psBegin = "SIG # Begin signature block"
const psEnd = "SIG # End signature block"

type sigStyle struct{ start, end string }

var psStyles = map[PsSigStyle]sigStyle{
	SigStyleHash: sigStyle{"# ", ""},
	SigStyleXML:  sigStyle{"<!-- ", " -->"},
	SigStyleC:    sigStyle{"/* ", " */"},
}

// Get the PowerShell signature style for a filename or extension
func GetSigStyle(filename string) (PsSigStyle, bool) {
	style, ok := psExtMap[filepath.Ext(filename)]
	return style, ok
}

// Return all supported PowerShell signature styles
func AllSigStyles() []string {
	var ret []string
	for k := range psExtMap {
		ret = append(ret, k)
	}
	return ret
}

type PsDigest struct {
	Imprint           []byte
	HashFunc          crypto.Hash
	TextSize, SigSize int64
	SigStyle          PsSigStyle
	IsUtf16           bool
}

// Digest a PowerShell script from a stream, returning the sum and the length of the digested bytes.
//
// PowerShell scripts are digested in UTF-16-LE format so, unless already in
// that format, the text is converted first. Existing signatures are discarded.
func DigestPowershell(r io.Reader, style PsSigStyle, hash crypto.Hash) (*PsDigest, error) {
	si, ok := psStyles[style]
	if !ok {
		return nil, errors.New("invalid powershell signature style")
	}
	br := bufio.NewReader(r)
	isUtf16, first, _ := detectUtf16(br, si.start, si.end)
	d := hash.New()
	var textSize, sigSize int64
	var saved string
	for {
		line, err := readLine(br, isUtf16)
		if err != nil && err != io.EOF {
			return nil, err
		}
		if line == first {
			// remove EOL from previous line
			if isUtf16 {
				saved = saved[:len(saved)-4]
				sigSize = 4
			} else {
				saved = saved[:len(saved)-2]
				sigSize = 2
			}
			// count the size of the signature
			sigSize += int64(len(line))
			n, err := io.Copy(ioutil.Discard, br)
			if err != nil {
				return nil, err
			}
			sigSize += n
			break
		} else {
			writeUtf16(d, saved, isUtf16)
			textSize += int64(len(saved))
			saved = line
		}
		if err == io.EOF {
			break
		}
	}
	writeUtf16(d, saved, isUtf16)
	textSize += int64(len(saved))
	return &PsDigest{d.Sum(nil), hash, textSize, sigSize, style, isUtf16}, nil
}

func detectUtf16(br *bufio.Reader, start, end string) (bool, string, string) {
	first := start + psBegin + end + "\r\n"
	last := start + psEnd + end + "\r\n"
	if bom, err := br.Peek(2); err == nil && bom[0] == 0xff && bom[1] == 0xfe {
		// UTF-16-LE
		return true, toUtf16(first), toUtf16(last)
	}
	return false, first, last
}

// Verify a PowerShell script. The signature "style" must already have been
// determined by calling GetSigStyle
func VerifyPowershell(r io.ReadSeeker, style PsSigStyle, skipDigests bool) (*pkcs9.TimestampedSignature, error) {
	si, ok := psStyles[style]
	if !ok {
		return nil, errors.New("invalid powershell signature style")
	}
	br := bufio.NewReader(r)
	isUtf16, first, last := detectUtf16(br, si.start, si.end)
	found := false
	var textSize int64
	var pkcsb bytes.Buffer
	for {
		line, err := readLine(br, isUtf16)
		if err == io.EOF && !found {
			return nil, sigerrors.NotSignedError{Type: "powershell document"}
		} else if err != nil {
			return nil, err
		}
		if found && line == last {
			break
		} else if found {
			lstr := string(line)
			if isUtf16 {
				lstr = fromUtf16(line)
			}
			if !strings.HasPrefix(lstr, si.start) || !strings.HasSuffix(lstr, si.end+"\r\n") {
				return nil, errors.New("malformed powershell signature")
			}
			i := len(si.start)
			j := len(lstr) - len(si.end) - 2
			lder, err := base64.StdEncoding.DecodeString(lstr[i:j])
			if err != nil {
				return nil, err
			}
			pkcsb.Write(lder)
		} else if line == first {
			// remove preceding \r\n from size
			if isUtf16 {
				textSize -= 4
			} else {
				textSize -= 2
			}
			found = true
		} else {
			textSize += int64(len(line))
		}
	}
	psd, err := pkcs7.Unmarshal(pkcsb.Bytes())
	if err != nil {
		return nil, err
	}
	if !psd.Content.ContentInfo.ContentType.Equal(OidSpcIndirectDataContent) {
		return nil, errors.New("not an authenticode signature")
	}
	sig, err := psd.Content.Verify(nil, false)
	if err != nil {
		return nil, err
	}
	ts, err := pkcs9.VerifyOptionalTimestamp(sig)
	if err != nil {
		return nil, err
	}
	indirect := new(SpcIndirectDataContentMsi)
	if err := psd.Content.ContentInfo.Unmarshal(indirect); err != nil {
		return nil, err
	}
	hash, err := x509tools.PkixDigestToHashE(indirect.MessageDigest.DigestAlgorithm)
	if err != nil {
		return nil, err
	}
	if !skipDigests {
		if _, err := r.Seek(0, 0); err != nil {
			return nil, err
		}
		digest, err := DigestPowershell(r, style, hash)
		if err != nil {
			return nil, err
		}
		if !hmac.Equal(digest.Imprint, indirect.MessageDigest.Digest) {
			return nil, fmt.Errorf("digest mismatch: %x != %x", digest.Imprint, indirect.MessageDigest.Digest)
		}
	}
	return &ts, nil
}

// Sign a previously digested PowerShell script and return the Authenticode structure
func (pd *PsDigest) Sign(ctx context.Context, cert *certloader.Certificate) (*binpatch.PatchSet, *pkcs9.TimestampedSignature, error) {
	ts, err := SignSip(ctx, pd.Imprint, pd.HashFunc, psSipInfo, cert)
	if err != nil {
		return nil, nil, err
	}
	patch, err := pd.MakePatch(ts.Raw)
	if err != nil {
		return nil, nil, err
	}
	return patch, ts, nil
}

// Create a patchset that will add or replace the signature on the digested script
func (pd *PsDigest) MakePatch(sig []byte) (*binpatch.PatchSet, error) {
	si, ok := psStyles[pd.SigStyle]
	if !ok {
		return nil, errors.New("invalid powershell signature style")
	}
	var buf bytes.Buffer
	buf.WriteString("\r\n" + si.start + psBegin + si.end + "\r\n")
	b64 := base64.StdEncoding.EncodeToString(sig)
	for i := 0; i < len(b64); i += 64 {
		j := i + 64
		if j > len(b64) {
			j = len(b64)
		}
		buf.WriteString(si.start + b64[i:j] + si.end + "\r\n")
	}
	buf.WriteString(si.start + psEnd + si.end + "\r\n")
	patch := binpatch.New()
	var encoded []byte
	if pd.IsUtf16 {
		encoded = []byte(toUtf16(buf.String()))
	} else {
		encoded = buf.Bytes()
	}
	patch.Add(pd.TextSize, int64(pd.SigSize), encoded)
	return patch, nil
}

func readLine(br *bufio.Reader, isUtf16 bool) (string, error) {
	line, err := br.ReadString('\n')
	if isUtf16 && err == nil {
		// \n\0
		var zero byte
		zero, err = br.ReadByte()
		if zero != 0 {
			return "", errors.New("malformed utf16")
		}
		line += "\x00"
	}
	return line, err
}

// Convert UTF8 to UTF-16-LE
func toUtf16(x string) string {
	runes := utf16.Encode([]rune(x))
	buf := bytes.NewBuffer(make([]byte, 0, 2*len(runes)))
	binary.Write(buf, binary.LittleEndian, runes)
	return buf.String()
}

// Convert UTF8 to UTF-16-LE and write it to "d"
func writeUtf16(d io.Writer, x string, isUtf16 bool) error {
	if isUtf16 {
		_, err := d.Write([]byte(x))
		return err
	}
	runes := utf16.Encode([]rune(x))
	return binary.Write(d, binary.LittleEndian, runes)
}

// Convert UTF-16-LE to UTF8
func fromUtf16(x string) string {
	runes := make([]uint16, len(x)/2)
	binary.Read(bytes.NewReader([]byte(x)), binary.LittleEndian, runes)
	return string(utf16.Decode(runes))
}
