android 如何给strings.xml文件内容加密?

896 阅读7分钟

序:本篇文章只是交流,如需上线还需要自己多完善

各位同学可想过如何给strings.xml里面的内容进行加密,然后在使用的地方进行解密呢? 我们想一想如果要做加密,应该要解决什么问题?

  1. 什么时候给strings.xml进行加密?
  2. activity和xml里面的内容,怎么解密?

第一个问题 我们可以在编译成apk的时候进行任务拦截,然后处理合并之后的strings.xml文件内容,注意,在清单文件或anim等非常规xml文件中使用的不能加密

伪代码如下

void attachObs(Project pj) {
        pj.afterEvaluate(new Action<Project>() {
            @Override
            void execute(Project project) {

                Map<Project, Set<Task>> allTasks = project.getAllTasks(true)
                for (Map.Entry<Project, Set<Task>> projectSetEntry : allTasks.entrySet()) {
                    Set<Task> value = projectSetEntry.getValue()
                    for (Task task : value) {

                            if(task.name.matches("^merge\S*ReleaseResources\$")) {
                               
                                String channel = task.name
                                channel = task.name.substring(5, channel.length() - 16)
                                task.doFirst { t ->
                                    println("-------mergeReleaseResources---------" + channel)

                                    for (File file : t.getInputs().getFiles().getFiles()) {
                                        if (file.isDirectory()) {
                                            getStringFile(file, project, channel.toLowerCase())
                                        }
                                    }

                                    modifyAppStringXmlFile(project)

                                }
                                
                            }

                    }
                }
            }
        }
    }


    /***
     * 给string.xml进行加密
     * @param file
     * @param project
     * @param channel
     */
    void getStringFile(File file, Project project, String channel) {
        for (File cfile : file.listFiles()) {
            if (cfile.isDirectory()) {
                getStringFile(cfile, project, channel)
            } else if (cfile.absolutePath.contains("values") && cfile.name.endsWith(".xml")) {
                println("values file->" + cfile.absolutePath)
                // 处理所需要加密的values.xml文件,因为strings.xml是一个xml文件,所以我们完全可以使用XmlParser来解析,而里面的内容,其实就是一个一个node节点,我们把要加密的node节点,添加到app这个module的strings.xml文件中,这样打包会覆盖其原来的内容
                
                
            }
        }
    }

下边是加密伪代码

boolean isAppStringFile = false;
if(absolutePath.contains("values"+File.separator+"strings.xml")) {
    // 说明是app下的values下的strings.xml
    Utils.println("获取到app下的values下的strings.xml");
    appStringFile = xmlFile;
    isAppStringFile = true;
}
try {
    XmlParser xmlparser = new XmlParser();
    Node xml = xmlparser.parse(xmlFile.getPath());
    if(xml == null) {
        return;
    }
    Iterator iterator = xml.iterator();

    while(iterator.hasNext()) {
        Object next = iterator.next();
        if(next instanceof Node) {
            Node node = (Node) next;
            String nodeText = node.text();
            if(node.name().equals("string")) {
                if(nodeText != null && nodeText.length() > 0) {
                    String nodeNameAttr = node.attribute("name").toString();
                    Utils.println("string--- node=" + nodeText + "  name=" + node.name() + "  text=" + node.text() +
                            " nodeNameAttr=" + nodeNameAttr + " node.attribute="+node.attributes() );
                    // 这里就可以对你的strings.xml的node节点做加密
                    if(nodeNameAttr.equals(你的strings.xml里面的要加密的name的名字)){
                        // 这里保存你的node节点,到modifyAppStringXmlFile方法去修改app下的strings.xml文件
                    }
                }
            }
        }
    }

} catch (ParserConfigurationException e) {
    e.printStackTrace();
} catch (SAXException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
}

/***
     * 修改app下的strings.xml文件
     * @param project
     */
    void modifyAppStringXmlFile(Project project) {
    
        removeStringXmlSameNode(project,appStringFile,needEntryNodeList) // 这个方法是为了在jenkins上打包的时候,先删除掉上一次加密过的strings.xml里面的节点,demo中可注释
        addNodeToStringXml(project,appStringFile,needEntryNodeList)

    }

    /***
     * 添加node节点到strings.xml文件中
     * @param project
     * @param appStringFile
     * @param needEntryNodeList
     */
    void addNodeToStringXml(Project project,File appStringFile,List<Node> needEntryNodeList) {
        def xml = project.file(appStringFile)
        def appStringxml = new XmlParser().parseText(xml.getText())
        if(appStringxml != null) {
            List<Node> appendNodeList = new ArrayList<>()

            for(Node node1:needEntryNodeList) {
                String needEntryText = node1.text();
                needEntryText = Utils.getRandomString(5) + needEntryText
                String str = AESUtil.encrypt(needEntryText,"hello");
                println("string---加密后"+str)
                Node node2 = new Node(node1.parent(),"string",node1.attributes(),str)
                appendNodeList.add(node2)
                appStringxml.append(node2)
            }
            // 保存修改后的strings.xml文件
            def serialize = groovy.xml.XmlUtil.serialize(appStringxml)
            xml.withWriter {writer->
                writer.write(serialize)
            }

           
        }
    }

上边是加密的核心代码,现在再来回顾一下逻辑

  1. 遍历所有的strings.xml文件(有的string写到了values.xml中)
  2. 找到要加密的strings.xml文件中要加密的node节点
  3. 将要加密的node节点写入到app下的strings.xml文件中,以便可以用加密后的节点覆盖加密前的节点

下边我们再来说一说如何解密? 其实当时做的时候,我是有一些纠结的,因为我不知道在哪里去对R.string.xxx去进行解密,而且如果布局xml文件中有使用的话,那又该怎么做解密呢?

其实所有的资源文件都是通过resources类来做解析的,这里的原理跟换肤有点类似,伪代码如下



public class WxjResources extends Resources {

    private static final String TAG = "BaseActivity";

    private Resources mResources;

    public WxjResources(Resources resources) {
        super(resources.getAssets(), resources.getDisplayMetrics(), resources.getConfiguration());
        mResources = resources;
    }


    @NonNull
    @Override
    public CharSequence getText(int id) throws NotFoundException {
        //CharSequence value = mResources.getResourceEntryName(id);
        CharSequence value = mResources.getText(id);

        value = jiemiStr2(value);
        Log.d(TAG, "getText 2222222222 value===="+value+"id="+id);
        return value;
    }

    @Override
    public CharSequence getText(int id, CharSequence def) {
        CharSequence value = mResources.getText(id, def);
        value = jiemiStr2(value);
        Log.d(TAG, "getText 2222222222 value===="+value+"id="+id);
        return value;
    }

    private CharSequence jiemiStr2(CharSequence value) {
        if(AESUtil.checkHexString(value.toString())) {
            String decryptValue = null;
            try {
                decryptValue = AESUtil.decrypt(value.toString(), "hello");
            } catch (Exception e) {
                e.printStackTrace();
            }
            Log.d(TAG, "jiemistr2 decryptValue===="+decryptValue);
            if(TextUtils.isEmpty(decryptValue)) {
                return value;
            }
            decryptValue = decryptValue.substring(5);

            return decryptValue;
        }
        Log.d(TAG, "jiemistr2 value===="+value);
        return value;
    }

    private String jiemiStr(String value) {
        if(AESUtil.checkHexString(value)) {
            String decryptValue = null;
            try {
                decryptValue = AESUtil.decrypt(value, "hello");
            } catch (Exception e) {
                e.printStackTrace();
            }
            Log.d(TAG, "jiemiStr decryptValue===="+decryptValue);
            if(TextUtils.isEmpty(decryptValue)) {
                return value;
            }
            decryptValue = decryptValue.substring(5);

            return decryptValue;
        }
        Log.d(TAG, "jiemistr value===="+value);
        return value;
    }

    @NonNull
    @Override
    public String getString(int id, Object... formatArgs) throws NotFoundException {
        String value = mResources.getString(id, formatArgs);
        if(TextUtils.isEmpty(value)) {
            return value;
        }
        Log.d(TAG, "getString 111111111111 value===="+value);
        value = jiemiStr(value);
        return value;
    }

    @NonNull
    @Override
    public String getString(int id) throws NotFoundException {
        String value = mResources.getString(id);
        if(TextUtils.isEmpty(value)) {
            return value;
        }
        value = jiemiStr(value);
        Log.d(TAG, "getString 2222222222 value===="+value+"id="+id);
        return value;
    }






    @RequiresApi(api = Build.VERSION_CODES.O)
    @NonNull
    @Override
    public Typeface getFont(int id) throws NotFoundException {
        return mResources.getFont(id);
    }

    @NonNull
    @Override
    public CharSequence getQuantityText(int id, int quantity) throws NotFoundException {
        return mResources.getQuantityText(id, quantity);
    }

    @NonNull
    @Override
    public String getQuantityString(int id, int quantity, Object... formatArgs) throws NotFoundException {
        return mResources.getQuantityString(id, quantity, formatArgs);
    }

    @NonNull
    @Override
    public String getQuantityString(int id, int quantity) throws NotFoundException {
        return mResources.getQuantityString(id, quantity);
    }

    @NonNull
    @Override
    public CharSequence[] getTextArray(int id) throws NotFoundException {
        return mResources.getTextArray(id);
    }

    @NonNull
    @Override
    public String[] getStringArray(int id) throws NotFoundException {
        return mResources.getStringArray(id);
    }

    @NonNull
    @Override
    public int[] getIntArray(int id) throws NotFoundException {
        return mResources.getIntArray(id);
    }

    @NonNull
    @Override
    public TypedArray obtainTypedArray(int id) throws NotFoundException {
        return mResources.obtainTypedArray(id);
    }

    @Override
    public float getDimension(int id) throws NotFoundException {
        return mResources.getDimension(id);
    }

    @Override
    public int getDimensionPixelOffset(int id) throws NotFoundException {
        return mResources.getDimensionPixelOffset(id);
    }

    @Override
    public int getDimensionPixelSize(int id) throws NotFoundException {
        return mResources.getDimensionPixelSize(id);
    }

    @Override
    public float getFraction(int id, int base, int pbase) {
        return mResources.getFraction(id, base, pbase);
    }

    @Override
    public Drawable getDrawable(int id) throws NotFoundException {
        return mResources.getDrawable(id);
    }

    @Override
    public Drawable getDrawable(int id, @Nullable Theme theme) throws NotFoundException {
        return mResources.getDrawable(id, theme);
    }

    @Nullable
    @Override
    public Drawable getDrawableForDensity(int id, int density) throws NotFoundException {
        return mResources.getDrawableForDensity(id, density);
    }

    @Nullable
    @Override
    public Drawable getDrawableForDensity(int id, int density, @Nullable Theme theme) {
        return mResources.getDrawableForDensity(id, density, theme);
    }

    @Override
    public Movie getMovie(int id) throws NotFoundException {
        return mResources.getMovie(id);
    }

    @Override
    public int getColor(int id) throws NotFoundException {
        return mResources.getColor(id);
    }

    @Override
    public int getColor(int id, @Nullable Theme theme) throws NotFoundException {
        return mResources.getColor(id, theme);
    }

    @NonNull
    @Override
    public ColorStateList getColorStateList(int id) throws NotFoundException {
        return mResources.getColorStateList(id);
    }

    @NonNull
    @Override
    public ColorStateList getColorStateList(int id, @Nullable Theme theme) throws NotFoundException {
        return mResources.getColorStateList(id, theme);
    }

    @Override
    public boolean getBoolean(int id) throws NotFoundException {
        return mResources.getBoolean(id);
    }

    @Override
    public int getInteger(int id) throws NotFoundException {
        return mResources.getInteger(id);
    }

    @RequiresApi(api = Build.VERSION_CODES.Q)
    @Override
    public float getFloat(int id) {
            return mResources.getFloat(id);
    }

    @NonNull
    @Override
    public XmlResourceParser getLayout(int id) throws NotFoundException {
        return mResources.getLayout(id);
    }

    @NonNull
    @Override
    public XmlResourceParser getAnimation(int id) throws NotFoundException {
        return mResources.getAnimation(id);
    }

    @NonNull
    @Override
    public XmlResourceParser getXml(int id) throws NotFoundException {
        return mResources.getXml(id);
    }

    @NonNull
    @Override
    public InputStream openRawResource(int id) throws NotFoundException {
        return mResources.openRawResource(id);
    }

    @NonNull
    @Override
    public InputStream openRawResource(int id, TypedValue value) throws NotFoundException {
        return mResources.openRawResource(id, value);
    }

    @Override
    public AssetFileDescriptor openRawResourceFd(int id) throws NotFoundException {
        return mResources.openRawResourceFd(id);
    }

    @Override
    public void getValue(int id, TypedValue outValue, boolean resolveRefs) throws NotFoundException {
        mResources.getValue(id, outValue, resolveRefs);
    }

    @Override
    public void getValueForDensity(int id, int density, TypedValue outValue, boolean resolveRefs) throws NotFoundException {
        mResources.getValueForDensity(id, density, outValue, resolveRefs);
    }

    @Override
    public void getValue(String name, TypedValue outValue, boolean resolveRefs) throws NotFoundException {
        mResources.getValue(name, outValue, resolveRefs);
    }

    @Override
    public TypedArray obtainAttributes(AttributeSet set, int[] attrs) {
        return mResources.obtainAttributes(set, attrs);
    }

    @Override
    public void updateConfiguration(Configuration config, DisplayMetrics metrics) {
        mResources.updateConfiguration(config, metrics);
    }

    @Override
    public DisplayMetrics getDisplayMetrics() {
        return mResources.getDisplayMetrics();
    }

    @Override
    public Configuration getConfiguration() {
        return mResources.getConfiguration();
    }

    @Override
    public int getIdentifier(String name, String defType, String defPackage) {
        return mResources.getIdentifier(name, defType, defPackage);
    }

    @Override
    public String getResourceName(int resid) throws NotFoundException {
        return mResources.getResourceName(resid);
    }

    @Override
    public String getResourcePackageName(int resid) throws NotFoundException {
        return mResources.getResourcePackageName(resid);
    }

    @Override
    public String getResourceTypeName(int resid) throws NotFoundException {
        return mResources.getResourceTypeName(resid);
    }

    @Override
    public String getResourceEntryName(int resid) throws NotFoundException {
        return mResources.getResourceEntryName(resid);
    }

    @Override
    public void parseBundleExtras(XmlResourceParser parser, Bundle outBundle) throws IOException, XmlPullParserException {
        mResources.parseBundleExtras(parser, outBundle);
    }

    @Override
    public void parseBundleExtra(String tagName, AttributeSet attrs, Bundle outBundle) throws XmlPullParserException {
        mResources.parseBundleExtra(tagName, attrs, outBundle);
    }

    @Override
    public void addLoaders(@NonNull ResourcesLoader... loaders) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            mResources.addLoaders(loaders);
        }
    }

    @Override
    public void removeLoaders(@NonNull ResourcesLoader... loaders) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            mResources.removeLoaders(loaders);
        }
    }
}

在所有的activity中都需要去使用


public class ResEntryLifecycle implements Application.ActivityLifecycleCallbacks {

    private static final String TAG = "ResEntryLifecycle";

    @Override
    public void onActivityPreCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
        try {
            Resources res = activity.getResources();
            WxjResources wxjResources = new WxjResources(res);

            Field mResources = ContextThemeWrapper.class.getDeclaredField("mResources");
            mResources.setAccessible(true);
            mResources.set(activity,wxjResources);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
        if(activity instanceof AppCompatActivity) {
            installLayoutFactory(activity);
        } else {
            installLayoutFactoryByActivity(activity);
        }
    }

    @Override
    public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {

    }

    private void installLayoutFactoryByActivity(Activity activity) {
        LayoutInflater layoutInflater = activity.getLayoutInflater();
        LayoutInflaterCompat.setFactory2(layoutInflater, new LayoutInflater.Factory2() {
            @Nullable
            @Override
            public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
                Log.d(TAG, "installLayoutFactoryByActivity parent = " + parent + " name="+name + " attrs="+attrs.toString());
                LayoutInflater inflater = LayoutInflater.from(context);
                try {
                    View view = null;
                    if (name.indexOf('.') > 0) { //表明是自定义View
                        view = inflater.createView(name, null, attrs);
                    } else {
                        String prefix = "android.widget.";
                        if(name.equals("ViewStub")) {
                            prefix = "android.view.";
                        }
                        view = inflater.createView(name, prefix, attrs);
                    }
                    Log.d(TAG, "报错类找不到"+view);
                    if(view instanceof TextView) {
                        int[] set = {
                                android.R.attr.text        // idx 0
                        };
                        // 不需要recycler,后面会在创建view时recycle的
                        @SuppressLint("Recycle")
                        TypedArray a = context.obtainStyledAttributes(attrs, set);
                        int resourceId = a.getResourceId(0, 0);
                        if (resourceId != 0) {
                            // 在这里进行解析
                            String value = activity.getResources().getString(resourceId);
                            Log.d(TAG, "installLayoutFactoryByActivity解密后value:"+value);

                            ((TextView) view).setText(value);

                            return view;
                        }
                    } else {
                        Log.d(TAG, "view 不是textview");
                    }

                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }
                return null;
            }

            @Nullable
            @Override
            public View onCreateView(@NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
                return null;
            }
        });
    }

    private void installLayoutFactory(Activity activity) {
        LayoutInflater layoutInflater = activity.getLayoutInflater();
        LayoutInflaterCompat.setFactory2(layoutInflater, new LayoutInflater.Factory2() {
            @Nullable
            @Override
            public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
                LayoutInflater inflater = LayoutInflater.from(context);
                AppCompatActivity activity = null;
                if (parent == null) {
                    if (context instanceof AppCompatActivity) {
                        activity = ((AppCompatActivity)context);
                    }
                } else if (parent.getContext() instanceof AppCompatActivity) {
                    activity = (AppCompatActivity) parent.getContext();
                }
                if (activity == null) {
                    return null;
                }

                AppCompatDelegate delegate = activity.getDelegate();
                int[] set = {
                        android.R.attr.text        // idx 0
                };

                // 不需要recycler,后面会在创建view时recycle的
                @SuppressLint("Recycle")
                TypedArray a = context.obtainStyledAttributes(attrs, set);
                View view = delegate.createView(parent, name, context, attrs);
                if (view == null && name.indexOf('.') > 0) { //表明是自定义View
                    try {
                        view = inflater.createView(name, null, attrs);
                    } catch (ClassNotFoundException e) {
                        e.printStackTrace();
                    }
                }

                if (view instanceof TextView) {
                    int resourceId = a.getResourceId(0, 0);
                    if (resourceId != 0) {
                        // 在这里进行解析
                        String value = activity.getResources().getString(resourceId);
                        Log.d(TAG, "解密后value:"+value);

                        ((TextView) view).setText(value);
                    }
                }

                return view;
            }

            @Nullable
            @Override
            public View onCreateView(@NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
                return null;
            }
        });
    }




    @Override
    public void onActivityStarted(@NonNull Activity activity) {

    }

    @Override
    public void onActivityResumed(@NonNull Activity activity) {

    }

    @Override
    public void onActivityPaused(@NonNull Activity activity) {

    }

    @Override
    public void onActivityStopped(@NonNull Activity activity) {

    }

    @Override
    public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {

    }

    @Override
    public void onActivityDestroyed(@NonNull Activity activity) {

    }

}

下边就是使用了

public class App extends Application {


    @Override
    public void onCreate() {
        super.onCreate();
        ResEntryLifecycle resEntryLifecycle = new ResEntryLifecycle();
        registerActivityLifecycleCallbacks(resEntryLifecycle);
    }

    
    @Override
    public Resources getResources() {
        Resources res = super.getResources();
        return new WxjResources(res);
    }

}

到这里所有的逻辑代码已经完成了,其实上边遗留了一个问题,我们项目是在jenkins上打包,然后当打包失败的时候,很有可能strings.xml里面已经存在加密后的node了,如果此时再次打包,那么就会有两个相同的node,这个问题其实也可以通过加密之前先把strings.xml里面的节点都干掉,然后再添加,虽然暴力,但是有效,还有很多不完善的地方,敬请谅解,只是提供一个思路!