序:本篇文章只是交流,如需上线还需要自己多完善
各位同学可想过如何给strings.xml里面的内容进行加密,然后在使用的地方进行解密呢?
我们想一想如果要做加密,应该要解决什么问题?
什么时候给strings.xml进行加密?
activity和xml里面的内容,怎么解密?
第一个问题
我们可以在编译成apk的时候进行任务拦截,然后处理合并之后的strings.xml文件内容,注意,在清单文件或anim等非常规xml文件中使用的不能加密
伪代码如下
void attachObs(Project pj) {
pj.afterEvaluate(new Action() {
@Override
void execute(Project project) {
Map> allTasks = project.getAllTasks(true)for (Map.Entry> projectSetEntry : allTasks.entrySet()) {Set value = projectSetEntry.getValue()for (Task task : value) {if(task.name.matches("^merge\S*ReleaseResources\$")) {String channel = task.namechannel = 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 needEntryNodeList) {def xml = project.file(appStringFile)def appStringxml = new XmlParser().parseText(xml.getText())if(appStringxml != null) {List appendNodeList = new ArrayList<>()for(Node node1:needEntryNodeList) {String needEntryText = node1.text();needEntryText = Utils.getRandomString(5) + needEntryTextString 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)}}
}
复制代码
上边是加密的核心代码,现在再来回顾一下逻辑
遍历所有的strings.xml文件(有的string写到了values.xml中)
找到要加密的strings.xml文件中要加密的node节点
将要加密的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@Overridepublic 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) { //表明是自定义Viewview = 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@Overridepublic 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@Overridepublic 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) { //表明是自定义Viewtry {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@Overridepublic 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里面的节点都干掉,然后再添加,虽然暴力,但是有效,还有很多不完善的地方,敬请谅解,只是提供一个思路!