package com.topjohnwu.signing;

import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1Object;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.DERPrintableString;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;

import java.io.ByteArrayInputStream;
import java.io.FileInputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Arrays;

@Keep
public class SignBoot {

    private static final int BOOT_IMAGE_HEADER_V1_RECOVERY_DTBO_SIZE_OFFSET = 1632;
    private static final int BOOT_IMAGE_HEADER_V2_DTB_SIZE_OFFSET = 1648;

    // Arbitrary maximum header version value; when greater assume the field is dt/extra size
    private static final int BOOT_IMAGE_HEADER_VERSION_MAXIMUM = 8;

    // Maximum header size byte value to read (currently the bootimg minimum page size)
    private static final int BOOT_IMAGE_HEADER_SIZE_MAXIMUM = 2048;

    private static class PushBackRWStream extends FilterInputStream {
        private OutputStream out;
        private int pos = 0;
        private byte[] backBuf;

        PushBackRWStream(InputStream in, OutputStream o) {
            super(in);
            out = o;
        }

        @Override
        public int read() throws IOException {
            int b;
            if (backBuf != null && backBuf.length > pos) {
                b = backBuf[pos++];
            } else {
                b = super.read();
                out.write(b);
            }
            return b;
        }

        @Override
        public int read(byte[] bytes, int off, int len) throws IOException {
            int read = 0;
            if (backBuf != null && backBuf.length > pos) {
                read = Math.min(len, backBuf.length - pos);
                System.arraycopy(backBuf, pos, bytes, off, read);
                pos += read;
                off += read;
                len -= read;
            }
            if (len > 0) {
                int ar = super.read(bytes, off, len);
                read += ar;
                out.write(bytes, off, ar);
            }
            return read;
        }

        void unread(byte[] buf) {
            backBuf = buf;
        }
    }

    public static boolean doSignature(
            @Nullable X509Certificate cert, @Nullable PrivateKey key,
            @NonNull InputStream imgIn, @NonNull OutputStream imgOut, @NonNull String target
    ) {
        try {
            PushBackRWStream in = new PushBackRWStream(imgIn, imgOut);
            byte[] hdr = new byte[BOOT_IMAGE_HEADER_SIZE_MAXIMUM];
            // First read the header
            in.read(hdr);
            int signableSize = getSignableImageSize(hdr);
            // Unread header
            in.unread(hdr);
            BootSignature bootsig = new BootSignature(target, signableSize);
            if (cert == null) {
                cert = CryptoUtils.readCertificate(
                        new ByteArrayInputStream(KeyData.verityCert()));
            }
            bootsig.setCertificate(cert);
            if (key == null) {
                key = CryptoUtils.readPrivateKey(
                        new ByteArrayInputStream(KeyData.verityKey()));
            }
            byte[] sig = bootsig.sign(key, in, signableSize);
            bootsig.setSignature(sig, CryptoUtils.getSignatureAlgorithmIdentifier(key));
            byte[] encoded_bootsig = bootsig.getEncoded();
            imgOut.write(encoded_bootsig);
            imgOut.flush();
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    public static boolean verifySignature(InputStream imgIn, X509Certificate cert) {
        try {
            // Read the header for size
            byte[] hdr = new byte[BOOT_IMAGE_HEADER_SIZE_MAXIMUM];
            if (imgIn.read(hdr) != hdr.length) {
                System.err.println("Unable to read image header");
                return false;
            }
            int signableSize = getSignableImageSize(hdr);

            // Read the rest of the image
            byte[] rawImg = Arrays.copyOf(hdr, signableSize);
            int remain = signableSize - hdr.length;
            if (imgIn.read(rawImg, hdr.length, remain) != remain) {
                System.err.println("Unable to read image");
                return false;
            }

            // Read footer, which contains the signature
            byte[] signature = new byte[4096];
            if (imgIn.read(signature) == -1 || Arrays.equals(signature, new byte [signature.length])) {
                System.err.println("Invalid image: not signed");
                return false;
            }

            BootSignature bootsig = new BootSignature(signature);
            if (cert != null) {
                bootsig.setCertificate(cert);
            }
            if (bootsig.verify(rawImg, signableSize)) {
                System.err.println("Signature is VALID");
                return true;
            } else {
                System.err.println("Signature is INVALID");
            }
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return false;
    }

    public static int getSignableImageSize(byte[] data) throws Exception {
        if (!Arrays.equals(Arrays.copyOfRange(data, 0, 8),
                "ANDROID!".getBytes("US-ASCII"))) {
            throw new IllegalArgumentException("Invalid image header: missing magic");
        }
        ByteBuffer image = ByteBuffer.wrap(data);
        image.order(ByteOrder.LITTLE_ENDIAN);
        image.getLong(); // magic
        int kernelSize = image.getInt();
        image.getInt(); // kernel_addr
        int ramdskSize = image.getInt();
        image.getInt(); // ramdisk_addr
        int secondSize = image.getInt();
        image.getLong(); // second_addr + tags_addr
        int pageSize = image.getInt();
        int length = pageSize // include the page aligned image header
                + ((kernelSize + pageSize - 1) / pageSize) * pageSize
                + ((ramdskSize + pageSize - 1) / pageSize) * pageSize
                + ((secondSize + pageSize - 1) / pageSize) * pageSize;
        int headerVersion = image.getInt(); // boot image header version or dt/extra size
        if (headerVersion > 0 && headerVersion < BOOT_IMAGE_HEADER_VERSION_MAXIMUM) {
            image.position(BOOT_IMAGE_HEADER_V1_RECOVERY_DTBO_SIZE_OFFSET);
            int recoveryDtboLength = image.getInt();
            length += ((recoveryDtboLength + pageSize - 1) / pageSize) * pageSize;
            image.getLong(); // recovery_dtbo address
            int headerSize = image.getInt();
            if (headerVersion == 2) {
                image.position(BOOT_IMAGE_HEADER_V2_DTB_SIZE_OFFSET);
                int dtbLength = image.getInt();
                length += ((dtbLength + pageSize - 1) / pageSize) * pageSize;
                image.getLong(); // dtb address
            }
            if (image.position() != headerSize) {
                throw new IllegalArgumentException(
                        "Invalid image header: invalid header length");
            }
        } else {
            // headerVersion is 0 or actually dt/extra size in this case
            length += ((headerVersion + pageSize - 1) / pageSize) * pageSize;
        }
        length = ((length + pageSize - 1) / pageSize) * pageSize;
        if (length <= 0) {
            throw new IllegalArgumentException("Invalid image header: invalid length");
        }
        return length;
    }

    static class BootSignature extends ASN1Object {
        private ASN1Integer formatVersion;
        private ASN1Encodable certificate;
        private AlgorithmIdentifier algId;
        private DERPrintableString target;
        private ASN1Integer length;
        private DEROctetString signature;
        private PublicKey publicKey;
        private static final int FORMAT_VERSION = 1;

        /**
         * Initializes the object for signing an image file
         * @param target Target name, included in the signed data
         * @param length Length of the image, included in the signed data
         */
        public BootSignature(String target, int length) {
            this.formatVersion = new ASN1Integer(FORMAT_VERSION);
            this.target = new DERPrintableString(target);
            this.length = new ASN1Integer(length);
        }

        /**
         * Initializes the object for verifying a signed image file
         * @param signature Signature footer
         */
        public BootSignature(byte[] signature) throws Exception {
            ASN1InputStream stream = new ASN1InputStream(signature);
            ASN1Sequence sequence = (ASN1Sequence) stream.readObject();
            formatVersion = (ASN1Integer) sequence.getObjectAt(0);
            if (formatVersion.getValue().intValue() != FORMAT_VERSION) {
                throw new IllegalArgumentException("Unsupported format version");
            }
            certificate = sequence.getObjectAt(1);
            byte[] encoded = ((ASN1Object) certificate).getEncoded();
            ByteArrayInputStream bis = new ByteArrayInputStream(encoded);
            CertificateFactory cf = CertificateFactory.getInstance("X.509");
            X509Certificate c = (X509Certificate) cf.generateCertificate(bis);
            publicKey = c.getPublicKey();
            ASN1Sequence algId = (ASN1Sequence) sequence.getObjectAt(2);
            this.algId = new AlgorithmIdentifier((ASN1ObjectIdentifier) algId.getObjectAt(0));
            ASN1Sequence attrs = (ASN1Sequence) sequence.getObjectAt(3);
            target = (DERPrintableString) attrs.getObjectAt(0);
            length = (ASN1Integer) attrs.getObjectAt(1);
            this.signature = (DEROctetString) sequence.getObjectAt(4);
        }

        public ASN1Object getAuthenticatedAttributes() {
            ASN1EncodableVector attrs = new ASN1EncodableVector();
            attrs.add(target);
            attrs.add(length);
            return new DERSequence(attrs);
        }

        public byte[] getEncodedAuthenticatedAttributes() throws IOException {
            return getAuthenticatedAttributes().getEncoded();
        }

        public void setSignature(byte[] sig, AlgorithmIdentifier algId) {
            this.algId = algId;
            signature = new DEROctetString(sig);
        }

        public void setCertificate(X509Certificate cert)
                throws CertificateEncodingException, IOException {
            ASN1InputStream s = new ASN1InputStream(cert.getEncoded());
            certificate = s.readObject();
            publicKey = cert.getPublicKey();
        }

        public byte[] sign(PrivateKey key, InputStream is, int len) throws Exception {
            Signature signer = Signature.getInstance(CryptoUtils.getSignatureAlgorithm(key));
            signer.initSign(key);
            int read;
            byte buffer[] = new byte[4096];
            while ((read = is.read(buffer, 0, Math.min(len, buffer.length))) > 0) {
                signer.update(buffer, 0, read);
                len -= read;
            }
            signer.update(getEncodedAuthenticatedAttributes());
            return signer.sign();
        }

        public boolean verify(byte[] image, int length) throws Exception {
            if (this.length.getValue().intValue() != length) {
                throw new IllegalArgumentException("Invalid image length");
            }
            String algName = CryptoUtils.ID_TO_ALG.get(algId.getAlgorithm().getId());
            if (algName == null) {
                throw new IllegalArgumentException("Unsupported algorithm " + algId.getAlgorithm());
            }
            Signature verifier = Signature.getInstance(algName);
            verifier.initVerify(publicKey);
            verifier.update(image, 0, length);
            verifier.update(getEncodedAuthenticatedAttributes());
            return verifier.verify(signature.getOctets());
        }

        @Override
        public ASN1Primitive toASN1Primitive() {
            ASN1EncodableVector v = new ASN1EncodableVector();
            v.add(formatVersion);
            v.add(certificate);
            v.add(algId);
            v.add(getAuthenticatedAttributes());
            v.add(signature);
            return new DERSequence(v);
        }

    }

    public static void main(String[] args) throws Exception {
        if (args.length > 0 && "-verify".equals(args[0])) {
            X509Certificate cert = null;
            if (args.length >= 2) {
                // args[1] is the path to a public key certificate
                cert = CryptoUtils.readCertificate(new FileInputStream(args[1]));
            }
            boolean signed = SignBoot.verifySignature(System.in, cert);
            System.exit(signed ? 0 : 1);
        } else if (args.length > 0 && "-sign".equals(args[0])) {
            X509Certificate cert = null;
            PrivateKey key = null;
            String name = "/boot";

            if (args.length >= 3) {
                cert = CryptoUtils.readCertificate(new FileInputStream(args[1]));
                key = CryptoUtils.readPrivateKey(new FileInputStream(args[2]));
            }
            if (args.length == 2) {
                name = args[1];
            } else if (args.length >= 4) {
                name = args[3];
            }

            boolean result = SignBoot.doSignature(cert, key, System.in, System.out, name);
            System.exit(result ? 0 : 1);
        } else {
            System.err.println(
                    "BootSigner <actions> [args]\n" +
                    "Input from stdin, output to stdout\n" +
                    "\n" +
                    "Actions:\n" +
                    "   -verify [x509.pem]\n" +
                    "      verify image. cert is optional.\n" +
                    "   -sign [x509.pem] [pk8] [name]\n" +
                    "      sign image. name and the cert/key pair are optional.\n" +
                    "      name should be either /boot (default) or /recovery.\n"
            );
        }
    }

}
