com.att.aro.datacollector.ioscollector.utilities.AppSigningHelper.java Source code

Java tutorial

Introduction

Here is the source code for com.att.aro.datacollector.ioscollector.utilities.AppSigningHelper.java

Source

/*
*  Copyright 2017 AT&T
*
* 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 com.att.aro.datacollector.ioscollector.utilities;

import java.io.IOException;
import java.io.StringReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.FileTime;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import java.util.TreeMap;

import org.apache.commons.lang.StringUtils;

import com.att.aro.core.ILogger;
import com.att.aro.core.SpringContextUtil;
import com.att.aro.core.commandline.IExternalProcessRunner;
import com.att.aro.core.fileio.IFileManager;
import com.att.aro.core.resourceextractor.IReadWriteFileExtractor;
import com.att.aro.core.settings.impl.SettingsImpl;
import com.att.aro.core.util.Util;
import com.att.aro.datacollector.ioscollector.app.IOSAppException;

public final class AppSigningHelper {
    private static final ILogger LOGGER = SpringContextUtil.getInstance().getContext().getBean(ILogger.class);
    private static final String IDEVICE_DEBUG = "/usr/local/bin/idevicedebug";
    private static final String IDEVICE_INSTALLER = "/usr/local/bin/ideviceinstaller";
    private static final String VO_APP_FILE = "VideoOptimizer.app";
    private static final String VO_ZIP_FILE = "VideoOptimizer.zip";
    private static final String CODE_SIGNATURE_FOLDER_NAME = "_CodeSignature";
    private static final String PROVISIONING_PROFILE_NAME = "embedded.mobileprovision";
    private static final String ENTITLEMENTS_PLIST_FILENAME = "entitlements.plist";
    private static final String INFO_PLIST_FILENAME = "Info.plist";
    private static final String PROV_FILE_APP_ID_KEY = "application-identifier";
    private static final String PROV_FILE_TEAM_ID_KEY = "com.apple.developer.team-identifier";
    private static final String PROV_FILE_ACCESS_GRP_KEY = "keychain-access-groups";
    private static final String INFO_PLIST_BUNDLE_ID_KEY = "CFBundleIdentifier";
    private static final String VO_APP_ID_VALUE = "com.att.VideoOptimizer";
    private static final String SIGNATURE_REPLACED_MSG = ": replacing existing signature";
    private static final String APP_INSTALL_COMPLETE_TXT = "Complete";
    private static final String APP_NEEDS_TRUST_TXT = "error: process launch failed: Security";
    private static final String[] FILES_TO_SIGN = { "libswiftRemoteMirror.dylib" };
    private static final String APP_PATH = Util.getVideoOptimizerLibrary() + Util.FILE_SEPARATOR + VO_APP_FILE;
    private static final String ZIP_PATH = Util.getVideoOptimizerLibrary() + Util.FILE_SEPARATOR + VO_ZIP_FILE;
    private static final String ENTITLEMENTS_PLIST_PATH = Util.getVideoOptimizerLibrary() + Util.FILE_SEPARATOR
            + ENTITLEMENTS_PLIST_FILENAME;

    private final IExternalProcessRunner extProcRunner = SpringContextUtil.getInstance().getContext()
            .getBean(IExternalProcessRunner.class);
    private final IReadWriteFileExtractor fileExtractor = SpringContextUtil.getInstance().getContext()
            .getBean(IReadWriteFileExtractor.class);
    private final IFileManager fileManager = SpringContextUtil.getInstance().getContext()
            .getBean(IFileManager.class);

    private static AppSigningHelper INSTANCE;

    private ProvProfile provProfile;
    private String packageName = "com.att.vo.test";
    private boolean signed = false;

    private AppSigningHelper() {

    }

    public static synchronized AppSigningHelper getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new AppSigningHelper();
        }
        return INSTANCE;
    }

    public void extractAndSign(String devProvProfilePath, String certName) throws IOSAppException {
        if (signed) {
            return;
        }
        extractVoZip();
        unZipAndClean();
        provProfile = new ProvProfile(devProvProfilePath);
        if (isProvProfileExpired()) {
            throw new IOSAppException(ErrorCodeRegistry.getProvProfileileExpiredError());
        }
        removeCodeSignatureDir();
        createAndUpdatePlists();
        replaceProvProfile(devProvProfilePath);
        signFiles(certName, provProfile.getCodesignId(), FILES_TO_SIGN);
        removeEntitlementsPlist();
        signed = true;
    }

    public void deployAndLaunchApp() throws IOSAppException {
        String cmdOutput = extProcRunner.executeCmdRunner(IDEVICE_INSTALLER + " --install " + APP_PATH, true,
                "success");
        verifyAppDeployed(cmdOutput);
        extractPackageName();
        launchApp();
    }

    private void extractPackageName() throws IOSAppException {
        String command = "codesign -dv " + APP_PATH;
        String res = extProcRunner.executeCmd(command);
        StringReader sr = new StringReader(res);
        Properties p = new Properties();
        try {
            p.load(sr);
            String packageName = p.getProperty("Identifier");
            if (packageName == null) {
                throw new IOSAppException("Error recognizing provisioning profile : Failed to find package name");
            }
            this.packageName = packageName;
        } catch (IOException e) {
            throw new IOSAppException("Error recognizing provisioning profile : Failed to find package name");
        }
    }

    private void launchApp() {
        String command = IDEVICE_DEBUG + " run " + packageName;
        executeCmd(command);
    }

    public void relaunchApp() {
        String command = IDEVICE_DEBUG + " run " + packageName + " -state" + " stop_rec";
        executeCmd(command);
    }

    public void executeCmd(String cmd) {
        System.out.println(cmd);
        ProcessBuilder pbldr = new ProcessBuilder();
        if (!Util.isWindowsOS()) {
            pbldr.command(new String[] { "bash", "-c", cmd });
        } else {
            pbldr.command(new String[] { "CMD", "/C", cmd });
        }
        try {
            Process proc = pbldr.start();
            try {
                Thread.sleep(1000 * 2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            proc.destroy();
        } catch (IOException e) {
            //Do nothing
        }
    }

    private void extractVoZip() throws IOSAppException {
        if (!fileExtractor.extractFiles(ZIP_PATH, VO_ZIP_FILE, AppSigningHelper.class.getClassLoader())) {
            throw new IOSAppException(ErrorCodeRegistry.getAppSavingError().getDescription());
        }
    }

    private void unZipAndClean() throws IOSAppException {
        executeCmd(Commands.unzipFile());
        if (!fileManager.fileExist(APP_PATH)) {
            throw new IOSAppException(ErrorCodeRegistry.getAppUnzipError());
        }
        executeCmd(Commands.removeFileOrDir(ZIP_PATH));
    }

    private boolean isProvProfileExpired() throws IOSAppException {
        DateTimeFormatter formatter = DateTimeFormatter.ISO_INSTANT;
        Instant dateTime = Instant.from(formatter.parse(provProfile.getExpiration()));
        return dateTime.compareTo(Instant.now()) < 0;
    }

    public String getPackageName() {
        return packageName;
    }

    private void removeEntitlementsPlist() {
        /* Not verifying file removed here as we will
         * check if this file exists when user launches 
         * VO next time.
         */
        extProcRunner.executeCmd(Commands.removeFileOrDir(ENTITLEMENTS_PLIST_PATH));
    }

    // Sign app and all the dylib files
    private void signFiles(String certName, String id, String[] filesToSign) throws IOSAppException {
        String line = extProcRunner.executeCmd(Commands.signApp(certName, provProfile.getCodesignId()));
        verifyFileSigned(line, VO_APP_FILE);
        line = extProcRunner.executeCmd(Commands.signFrameworkFiles(certName, provProfile.getCodesignId()));
        String numOfFiles = extProcRunner
                .executeCmd(Commands.getNumOfFilesInDir(APP_PATH + Util.FILE_SEPARATOR + "Frameworks"));
        if (numOfFiles.matches("^[0-9]*$")) {
            int numFiles = Integer.valueOf(numOfFiles);
            int numFilesReplaced = StringUtils.countMatches(line, SIGNATURE_REPLACED_MSG);
            if (numFiles != numFilesReplaced) {
                throw new IOSAppException(ErrorCodeRegistry.getFileSigningError());
            }
        }

        for (String fileToSign : filesToSign) {
            line = extProcRunner.executeCmd(Commands.signFile(certName, provProfile.getCodesignId(), fileToSign));
            verifyFileSigned(line, fileToSign);
        }

        line = extProcRunner.executeCmd(Commands.signApp(certName, provProfile.getCodesignId()));
        verifyFileSigned(line, VO_APP_FILE);
    }

    private void replaceProvProfile(String devProvProfilePath) throws IOSAppException {
        try {
            Path filePath = Paths.get(APP_PATH + Util.FILE_SEPARATOR + PROVISIONING_PROFILE_NAME);
            FileTime ftBefore = Files.getLastModifiedTime(filePath);
            // format path to be used for command line
            devProvProfilePath = devProvProfilePath.replaceAll(" ", "\\\\ ");
            extProcRunner.executeCmd(Commands.copyProvProfile(devProvProfilePath));
            FileTime ftAfter = Files.getLastModifiedTime(filePath);
            verifyFileUpdated(PROVISIONING_PROFILE_NAME, ftBefore, ftAfter);
        } catch (IOException e) {
            LOGGER.error("Error getting provisioning profile last modified time", e);
        }
    }

    private void createAndUpdatePlists() throws IOSAppException {
        // In case there is an old one, we need to remove it
        // or there will be problem parsing the file b/c
        // new content will be appended to the old file
        extProcRunner.executeCmd(Commands.removeFileOrDir(ENTITLEMENTS_PLIST_PATH));
        verifyEntitlementsPlistRemoved();
        extProcRunner.executeCmd(Commands.createEntitlementsPlist());
        verifyEntitlementsPlistCreated();

        try {

            Path infoPlistPath = Paths.get(APP_PATH + Util.FILE_SEPARATOR + INFO_PLIST_FILENAME);
            FileTime infoFTBefore = Files.getLastModifiedTime(infoPlistPath);
            updatePlists();
            FileTime infoFTAfter = Files.getLastModifiedTime(infoPlistPath);

            verifyEntitlementsUpdated();
            verifyFileUpdated(INFO_PLIST_FILENAME, infoFTBefore, infoFTAfter);

        } catch (IOException e) {
            LOGGER.error("Error getting plist file last modified time", e);
        }
    }

    private void verifyEntitlementsUpdated() throws IOSAppException {
        /*
         * Not able to use file modified time or file size to
         * verify file got updated. Look for a string that 
         * should have been replaced instead. 
         * (entitlements.plist is a small file)
         */
        try {
            List<String> lines = Files.readAllLines(Paths.get(ENTITLEMENTS_PLIST_PATH), StandardCharsets.UTF_8);
            for (String line : lines) {
                if (line.contains(VO_APP_ID_VALUE)) {
                    throw new IOSAppException(ErrorCodeRegistry.getFileUpdateError(ENTITLEMENTS_PLIST_FILENAME));
                }
            }
        } catch (IOException e) {
            LOGGER.error("Error verifying entitlements.plist was updated", e);
        }
    }

    private void verifyEntitlementsPlistRemoved() throws IOSAppException {
        String line = extProcRunner.executeCmd(Commands.listDirectory(Util.getVideoOptimizerLibrary()));
        if (line.contains(ENTITLEMENTS_PLIST_FILENAME)) {
            throw new IOSAppException(ErrorCodeRegistry.getRemoveEntitlementsFileError());
        }
    }

    private void updatePlists() throws IOSAppException {
        updateEntitlementsPlist();
        updateInfoPlist();
    }

    private void updateEntitlementsPlist() throws IOSAppException {
        extProcRunner.executeCmd(Commands.updatePlistEntry(ENTITLEMENTS_PLIST_PATH, ":" + PROV_FILE_APP_ID_KEY,
                provProfile.getAppId()));
        extProcRunner.executeCmd(Commands.updatePlistEntry(ENTITLEMENTS_PLIST_PATH, ":" + PROV_FILE_TEAM_ID_KEY,
                provProfile.getTeamId()));
        extProcRunner.executeCmd(Commands.updatePlistEntry(ENTITLEMENTS_PLIST_PATH,
                ":" + PROV_FILE_ACCESS_GRP_KEY + ":0 ", provProfile.getAppId()));
    }

    private void updateInfoPlist() throws IOSAppException {
        extProcRunner.executeCmd(Commands.updatePlistEntry(APP_PATH + Util.FILE_SEPARATOR + INFO_PLIST_FILENAME,
                ":" + INFO_PLIST_BUNDLE_ID_KEY, provProfile.getCodesignId()));
    }

    private void removeCodeSignatureDir() throws IOSAppException {
        extProcRunner
                .executeCmd(Commands.removeFileOrDir(APP_PATH + Util.FILE_SEPARATOR + CODE_SIGNATURE_FOLDER_NAME));
        verifyCodeSignatureFolderRemoved();
    }

    private void verifyFileSigned(String cmdOutput, String filename) throws IOSAppException {
        String replacedMsg = filename + SIGNATURE_REPLACED_MSG;
        if (!cmdOutput.contains(replacedMsg)) {
            throw new IOSAppException(ErrorCodeRegistry.getFileSigningError());
        }
    }

    private void verifyFileUpdated(String filename, FileTime before, FileTime after) throws IOSAppException {
        if (after.toMillis() <= before.toMillis()) {
            throw new IOSAppException(ErrorCodeRegistry.getFileUpdateError(filename));
        }
    }

    private void verifyCodeSignatureFolderRemoved() throws IOSAppException {
        String line = extProcRunner.executeCmd(Commands.listDirectory(APP_PATH));
        if (line.contains(CODE_SIGNATURE_FOLDER_NAME)) {
            throw new IOSAppException(ErrorCodeRegistry.getRemoveCodeSignatureError());
        }
    }

    private void verifyEntitlementsPlistCreated() throws IOSAppException {
        String line = extProcRunner.executeCmd(Commands.listDirectory(Util.getVideoOptimizerLibrary()));
        if (!line.contains(ENTITLEMENTS_PLIST_FILENAME)) {
            throw new IOSAppException(ErrorCodeRegistry.getCreateEntitlementsFileError());
        }
    }

    public String executeProcessExtractionCmd(String processList, String iosDeployPath) {
        TreeMap<Date, String> pidList = new TreeMap<>();
        if (processList != null) {
            String[] lineArr = processList.split(Util.LINE_SEPARATOR);
            SimpleDateFormat formatter = new SimpleDateFormat("hh:mma");
            for (String str : lineArr) {
                String[] strArr = str.split(" +");
                try {
                    if (str.contains(iosDeployPath) && strArr.length >= 8) {
                        Date timestamp = formatter.parse(strArr[8]);
                        pidList.put(timestamp, strArr[1]);
                    }

                } catch (ParseException e) {
                    LOGGER.error("Exception during pid extraction");
                }
            }
        }

        return pidList.lastEntry().getValue();
    }

    public int getIosVersion() {
        String cmd = "instruments -w device";
        String deviceList = extProcRunner.executeCmd(cmd);
        String[] devicesArray = deviceList.split("\n");
        int iosVersion = -1;
        for (String device : devicesArray) {
            if ((!device.contains("Simulator")) && device.contains("iPhone")) {
                try {
                    String versionStr = device.substring(device.indexOf("(") + 1, device.indexOf(")"));
                    if (versionStr.indexOf(".") != -1)
                        iosVersion = Integer.valueOf(versionStr.substring(0, versionStr.indexOf(".")));
                } catch (NumberFormatException e) {
                    LOGGER.error("Non numeric value cannot represent ios version: " + iosVersion);
                }
                break;
            }
        }
        return iosVersion;
    }

    private void verifyAppDeployed(String cmdOutput) throws IOSAppException {
        System.out.println(cmdOutput);
        if (!cmdOutput.contains(APP_INSTALL_COMPLETE_TXT)) {
            throw new IOSAppException(ErrorCodeRegistry.getAppDeploymentError());
        }
        if (cmdOutput.contains(APP_NEEDS_TRUST_TXT)) {
            throw new IOSAppException(ErrorCodeRegistry.getAppTrustError());
        }
    }

    private final static class Commands {

        private Commands() {
        };

        static String unzipFile() {
            StringBuilder strBuilder = new StringBuilder();
            strBuilder.append("unzip " + Util.getVideoOptimizerLibrary() + Util.FILE_SEPARATOR + VO_ZIP_FILE);
            strBuilder.append(" -d ");
            strBuilder.append(Util.getVideoOptimizerLibrary());
            return strBuilder.toString();
        }

        static String removeFileOrDir(String path) {
            StringBuilder strBuilder = new StringBuilder();
            strBuilder.append("rm -rf ");
            strBuilder.append(path);
            return strBuilder.toString();
        }

        static String createEntitlementsPlist() {
            StringBuilder strBuilder = new StringBuilder();
            strBuilder.append("codesign -d --entitlements :");
            strBuilder.append(ENTITLEMENTS_PLIST_PATH);
            strBuilder.append(" ");
            strBuilder.append(APP_PATH);
            return strBuilder.toString();
        }

        static String copyProvProfile(String devProvProfilePath) {
            StringBuilder strBuilder = new StringBuilder();
            strBuilder.append("cp ");
            strBuilder.append(devProvProfilePath);
            strBuilder.append(" ");
            strBuilder.append(APP_PATH);
            strBuilder.append(Util.FILE_SEPARATOR);
            strBuilder.append(PROVISIONING_PROFILE_NAME);
            return strBuilder.toString();
        }

        static String signApp(String certName, String id) {
            StringBuilder strBuilder = new StringBuilder();
            strBuilder.append("codesign -f -s \"");
            strBuilder.append(certName);
            strBuilder.append("\" -i ");
            strBuilder.append(id);
            strBuilder.append(" --entitlements ");
            strBuilder.append(ENTITLEMENTS_PLIST_PATH);
            strBuilder.append(" ");
            strBuilder.append(APP_PATH);
            return strBuilder.toString();
        }

        static String signFrameworkFiles(String certName, String id) {
            StringBuilder strBuilder = new StringBuilder();
            strBuilder.append("codesign -f -s \"");
            strBuilder.append(certName);
            strBuilder.append("\" -i ");
            strBuilder.append(id);
            strBuilder.append(" --entitlements ");
            strBuilder.append(ENTITLEMENTS_PLIST_PATH);
            strBuilder.append(" ");
            strBuilder.append(APP_PATH);
            strBuilder.append(Util.FILE_SEPARATOR);
            strBuilder.append("Frameworks/*");
            return strBuilder.toString();
        }

        static String signFile(String certName, String id, String filename) {
            StringBuilder strBuilder = new StringBuilder();
            strBuilder.append("codesign -f -s \"");
            strBuilder.append(certName);
            strBuilder.append("\" -i ");
            strBuilder.append(id);
            strBuilder.append(" --entitlements ");
            strBuilder.append(ENTITLEMENTS_PLIST_PATH);
            strBuilder.append(" ");
            strBuilder.append(APP_PATH);
            strBuilder.append(Util.FILE_SEPARATOR);
            strBuilder.append(filename);
            return strBuilder.toString();
        }

        static String listDirectory(String path) {
            StringBuilder strBuilder = new StringBuilder();
            strBuilder.append("ls ");
            strBuilder.append(path);
            return strBuilder.toString();
        }

        static String getNumOfFilesInDir(String dirPath) {
            StringBuilder strBuilder = new StringBuilder();
            strBuilder.append("ls -1 ");
            strBuilder.append(dirPath);
            strBuilder.append(" | wc -l");
            return strBuilder.toString();
        }

        static String updatePlistEntry(String filePath, String entry, String value) {
            StringBuilder strBuilder = new StringBuilder();
            strBuilder.append("/usr/libexec/PlistBuddy -c \"Set ");
            strBuilder.append(entry);
            strBuilder.append(" ");
            strBuilder.append(value);
            strBuilder.append("\" ");
            strBuilder.append(filePath);
            return strBuilder.toString();
        }
    }

    public static boolean isCertInfoPresent() {
        String provProfile = SettingsImpl.getInstance().getAttribute("iosProv");
        String certName = SettingsImpl.getInstance().getAttribute("iosCert");
        return StringUtils.isNotBlank(provProfile) && StringUtils.isNotBlank(certName);
    }

}