序:本篇文章只是交流,如需上线还需要自己多完善
各位同学可想过如何给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.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 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) + 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)
}
}
}
复制代码
上边是加密的核心代码,现在再来回顾一下逻辑
遍历所有的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
@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里面的节点都干掉,然后再添加,虽然暴力,但是有效,还有很多不完善的地方,敬请谅解,只是提供一个思路!