Android OTA系统升级---原理一

本文介绍了Android系统的OTA升级过程,包括使用源码工具生成OTA包、系统验证升级包的流程,涉及android.os.RecoverySystem工具类,需要REBOOT权限及系统签名。升级过程中,首先进行签名验证,然后通过RecoverySystem安装升级包。

最近公司有个需求需要搭建服务器,通过对终端系统进行网络升级。对相关知识进行整理。

android中OTA进行升级,首先需要使用源码中OTA升级包打包工具: build/tools/releasetools/ota_from_target_files 生成OTA包。包有两类:整体完全升级包和增量升级包。

在android系统编译环境下,在终端下使用make otapackage生成。


android平台使用了提供了android.os.RecoverySystem工具类支持升级,需要添加android.Manifest.permission#REBOOT permission,  而REBOOT权限需要添加android:shareUserId="android.uid.system",进行系统签名,赋予升级平台权限后,才能支持升级,或是升级app在系统源码编译环境中编译后才能使用该功能。


使用RecoverySystem工具类,需要两个步骤。

1、   进行签名验证,public static void verifyPackage(File packageFile,
                                     ProgressListener listener,
                                     File deviceCertsZipFile)   throws IOException, GeneralSecurityException 

     * @param packageFile 待验证的升级包
     * @param listener   升级进度监听器,验证的进度
     * @param deviceCertsZipFile   验证证书文件,如果为null, 系统默认的是 "/system/etc/security/otacerts.zip"。
  

    private static final File DEFAULT_KEYSTORE =    new File("/system/etc/security/otacerts.zip")

    /** 通过/cache/recovery/command,与系统通信,接收升级命令指令,详细可以查看源码: bootable/recovery/recovery.c. */
    private static File RECOVERY_DIR = new File("/cache/recovery");
    private static File COMMAND_FILE = new File(RECOVERY_DIR, "command");

//验证OTA升级包方法

    public static void verifyPackage(File packageFile,
                                     ProgressListener listener,
                                     File deviceCertsZipFile)
        throws IOException, GeneralSecurityException {
        long fileLen = packageFile.length();
        RandomAccessFile raf = new RandomAccessFile(packageFile, "r");
        try {
            int lastPercent = 0;
            long lastPublishTime = System.currentTimeMillis();
            if (listener != null) {
                listener.onProgress(lastPercent);
            }


            raf.seek(fileLen - 6);
            byte[] footer = new byte[6];
            raf.readFully(footer);

//升级包是否含有系统签名信息
            if (footer[2] != (byte)0xff || footer[3] != (byte)0xff) {
                throw new SignatureException("no signature in file (no footer)");
            }


            int commentSize = (footer[4] & 0xff) | ((footer[5] & 0xff) << 8);
//签名的起始位置

            int signatureStart = (footer[0] & 0xff) | ((footer[1] & 0xff) << 8);


            byte[] eocd = new byte[commentSize + 22];
            raf.seek(fileLen - (commentSize + 22));
            raf.readFully(eocd);


            // Check that we have found the start of the
            // end-of-central-directory record.
            if (eocd[0] != (byte)0x50 || eocd[1] != (byte)0x4b ||
                eocd[2] != (byte)0x05 || eocd[3] != (byte)0x06) {
                throw new SignatureException("no signature in file (bad footer)");
            }


            for (int i = 4; i < eocd.length-3; ++i) {
                if (eocd[i  ] == (byte)0x50 && eocd[i+1] == (byte)0x4b &&
                    eocd[i+2] == (byte)0x05 && eocd[i+3] == (byte)0x06) {
                    throw new SignatureException("EOCD marker found after start of EOCD");
                }
            }


            // The following code is largely copied from
            // JarUtils.verifySignature().  We could just *call* that
            // method here if that function didn't read the entire
            // input (ie, the whole OTA package) into memory just to
            // compute its message digest.


            BerInputStream bis = new BerInputStream(
                new ByteArrayInputStream(eocd, commentSize+22-signatureStart, signatureStart));
            ContentInfo info = (ContentInfo)ContentInfo.ASN1.decode(bis);
            SignedData signedData = info.getSignedData();
            if (signedData == null) {
                throw new IOException("signedData is null");
            }
            List<Certificate> encCerts = signedData.getCertificates();
            if (encCerts.isEmpty()) {
                throw new IOException("encCerts is empty");
            }
            // Take the first certificate from the signature (packages
            // should contain only one).
            Iterator<Certificate> it = encCerts.iterator();
            X509Certificate cert = null;
            if (it.hasNext()) {
                CertificateFactory cf = CertificateFactory.getInstance("X.509");
                InputStream is = new ByteArrayInputStream(it.next().getEncoded());
                cert = (X509Certificate) cf.generateCertificate(is);
            } else {
                throw new SignatureException("signature contains no certificates");
            }

   //获取到包中所有的签名证书信息
            List<SignerInfo> sigInfos = signedData.getSignerInfos();
            SignerInfo sigInfo;
            if (!sigInfos.isEmpty()) {
                sigInfo = (SignerInfo)sigInfos.get(0);
            } else {
                throw new IOException("no signer infos!");
            }


            // Check that the public key of the certificate contained
            // in the package equals one of our trusted public keys.


            HashSet<X509Certificate> trusted = getTrustedCerts(
                deviceCertsZipFile == null ? DEFAULT_KEYSTORE : deviceCertsZipFile);


            PublicKey signatureKey = cert.getPublicKey();
            boolean verified = false;

//签名验证key比较
            for (X509Certificate c : trusted) {
                if (c.getPublicKey().equals(signatureKey)) {
                    verified = true;
                    break;
                }
            }
            if (!verified) {
                throw new SignatureException("signature doesn't match any trusted key");
            }


            // The signature cert matches a trusted key.  Now verify that
            // the digest in the cert matches the actual file data.


            // The verifier in recovery only handles SHA1withRSA and
            // SHA256withRSA signatures.  SignApk chooses which to use
            // based on the signature algorithm of the cert:
            //
            //    "SHA256withRSA" cert -> "SHA256withRSA" signature
            //    "SHA1withRSA" cert   -> "SHA1withRSA" signature
            //    "MD5withRSA" cert    -> "SHA1withRSA" signature (for backwards compatibility)
            //    any other cert       -> SignApk fails
            //
            // Here we ignore whatever the cert says, and instead use
            // whatever algorithm is used by the signature.


            String da = sigInfo.getDigestAlgorithm();
            String dea = sigInfo.getDigestEncryptionAlgorithm();
            String alg = null;
            if (da == null || dea == null) {
                // fall back to the cert algorithm if the sig one
                // doesn't look right.
                alg = cert.getSigAlgName();
            } else {
                alg = da + "with" + dea;
            }
            Signature sig = Signature.getInstance(alg);
            sig.initVerify(cert);


            // 对整个升级包开始验证
            long toRead = fileLen - commentSize - 2;
            long soFar = 0;
            raf.seek(0);
            byte[] buffer = new byte[4096];
            boolean interrupted = false;
            while (soFar < toRead) {
                interrupted = Thread.interrupted();
                if (interrupted) break;
                int size = buffer.length;
                if (soFar + size > toRead) {
                    size = (int)(toRead - soFar);
                }
                int read = raf.read(buffer, 0, size);
                sig.update(buffer, 0, read);
                soFar += read;


                if (listener != null) {
                    long now = System.currentTimeMillis();
                    int p = (int)(soFar * 100 / toRead);
                    if (p > lastPercent &&
                        now - lastPublishTime > PUBLISH_PROGRESS_INTERVAL_MS) {
                        lastPercent = p;
                        lastPublishTime = now;

//返回验证进度
                        listener.onProgress(lastPercent);
                    }
                }
            }

// 验证进度完毕
            if (listener != null) {
                listener.onProgress(100);
            }


            if (interrupted) {
                throw new SignatureException("verification was interrupted");
            }


            if (!sig.verify(sigInfo.getEncryptedDigest())) {
                throw new SignatureException("signature digest verification failed");
            }
        } finally {
            raf.close();
        }
    }

2、通过系统签名验证通过后,进行安装

    /**
     * Reboots the device in order to install the given update
     * package.
     * Requires the {@link android.Manifest.permission#REBOOT} permission.
     *
     * @param context      the Context to use
     * @param packageFile  OTA升级包路径 ,必须在可恢复的挂载区,一般在/cache and /data 都是安全的
     */
    public static void installPackage(Context context, File packageFile)
        throws IOException {
        String filename = packageFile.getCanonicalPath();
        Log.w(TAG, "!!! REBOOTING TO INSTALL " + filename + " !!!");

        final String filenameArg = "--update_package=" + filename;
        final String localeArg = "--locale=" + Locale.getDefault().toString();

//调用bootCommand进行升级包安装
        bootCommand(context, filenameArg, localeArg);
    }


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值