mcp_pconline: MCPTool.java

File MCPTool.java, 17.5 KB (added by luochengbin, 9 years ago)
Line 
1package cn.com.pc.framwork.utils.app;
2
3import java.io.BufferedReader;
4import java.io.ByteArrayOutputStream;
5import java.io.Closeable;
6import java.io.File;
7import java.io.FileInputStream;
8import java.io.FileOutputStream;
9import java.io.FileReader;
10import java.io.IOException;
11import java.io.RandomAccessFile;
12import java.nio.ByteBuffer;
13import java.nio.ByteOrder;
14import java.nio.channels.FileChannel;
15import java.security.Key;
16import java.util.ArrayList;
17import java.util.Arrays;
18import java.util.LinkedHashMap;
19import java.util.Map;
20import java.util.zip.ZipFile;
21
22import javax.crypto.Cipher;
23import javax.crypto.SecretKeyFactory;
24import javax.crypto.spec.DESKeySpec;
25import javax.crypto.spec.IvParameterSpec;
26
27/**
28 * 倚枠道打包工具<br/>
29 * 利甚的是Zip文件“可以添加comment泚释”的数据结构特点圚文件的末尟写入任意数据而䞍甚重新解压zip文件apk文件就是zip文件栌匏<br/>
30 * 创建时闎 2016-12-16
31 * @version 1.1
32 * @since JDK1.7 Android2.2
33 */
34public class MCPTool {
35    /**
36     * 数据结构䜓的筟名标记
37     */
38    private static final String SIG = "MCPT";
39    /**
40     * 数据结构的版本号
41     */
42    private static final String VERSION_1_1 = "1.1";
43    /**
44     * 数据猖码栌匏
45     */
46    private static final String CHARSET_NAME = "UTF-8";
47    /**
48     * 加密甚的IvParameterSpec参数
49     */
50    private static final byte[] IV = new byte[] { 5, 2, 4, 1, 5, 4, 2, 5 };
51
52    private static final String PAS = "3kcdo0wg";
53
54    /**
55     * 写入数据
56     * @param path 文件路埄
57     * @param content 写入的内容
58     * @param password 加密密钥
59     * @throws Exception
60     */
61    private static void write(File path, String content, String password) throws Exception {
62        write(path, content.getBytes(CHARSET_NAME), password);
63    }
64
65    /**
66     * 写入数据劂枠道号
67     * @param path 文件路埄
68     * @param content 写入的内容
69     * @param password 加密密钥
70     * @throws Exception
71     */
72    private static void write(File path, byte[] content, String password) throws Exception {
73        ZipFile zipFile = new ZipFile(path);
74        boolean isIncludeComment = zipFile.getComment() != null;
75        zipFile.close();
76        if (isIncludeComment) {
77            throw new IllegalStateException("Zip comment is exists, Repeated write is not recommended.");
78        }
79
80        boolean isEncrypt = password != null && password.length() > 0;
81        byte[] bytesContent = isEncrypt ? encrypt(password, content) : content;
82        byte[] bytesVersion = VERSION_1_1.getBytes(CHARSET_NAME);
83        ByteArrayOutputStream baos = new ByteArrayOutputStream();
84        baos.write(bytesContent); // 写入内容
85        baos.write(short2Stream((short) bytesContent.length)); // 写入内容长床
86        baos.write(isEncrypt ? 1 : 0); // 写入是吊加密标瀺
87        baos.write(bytesVersion); // 写入版本号
88        baos.write(short2Stream((short) bytesVersion.length)); // 写入版本号长床
89        baos.write(SIG.getBytes(CHARSET_NAME)); // 写入SIG标记
90        byte[] data = baos.toByteArray();
91        baos.close();
92        if (data.length > Short.MAX_VALUE) {
93            throw new IllegalStateException("Zip comment length > 32767.");
94        }
95
96        // Zip文件末尟数据结构{@see java.util.zip.ZipOutputStream.writeEND}
97        RandomAccessFile raf = new RandomAccessFile(path, "rw");
98        raf.seek(path.length() - 2); // comment长床是short类型
99        raf.write(short2Stream((short) data.length)); // 重新写入comment长床泚意Android apk文件䜿甚的是ByteOrder.LITTLE_ENDIAN小端序
100        raf.write(data);
101        raf.close();
102    }
103
104    /**
105     * 读取数据
106     * @param path 文件路埄
107     * @param password 解密密钥
108     * @return 被该工具写入的数据劂枠道号
109     * @throws Exception
110     */
111    private static byte[] read(File path, String password) throws Exception {
112        byte[] bytesContent = null;
113        byte[] bytesMagic = SIG.getBytes(CHARSET_NAME);
114        byte[] bytes = new byte[bytesMagic.length];
115        RandomAccessFile raf = new RandomAccessFile(path, "r");
116        Object[] versions = getVersion(raf);
117        long index = (long) versions[0];
118        String version = (String) versions[1];
119        if (VERSION_1_1.equals(version)) {
120            bytes = new byte[1];
121            index -= bytes.length;
122            readFully(raf, index, bytes); // 读取内容长床
123            boolean isEncrypt = bytes[0] == 1;
124
125            bytes = new byte[2];
126            index -= bytes.length;
127            readFully(raf, index, bytes); // 读取内容长床
128            int lengthContent = stream2Short(bytes, 0);
129
130            bytesContent = new byte[lengthContent];
131            index -= lengthContent;
132            readFully(raf, index, bytesContent); // 读取内容
133
134            if (isEncrypt && password != null && password.length() > 0) {
135                bytesContent = decrypt(password, bytesContent);
136            }
137        }
138        raf.close();
139        return bytesContent;
140    }
141
142    /**
143     * 读取数据结构的版本号
144     * @param raf RandomAccessFile
145     * @return 数组对象[0] randomAccessFile.seek的index[1] 数据结构的版本号
146     * @throws IOException
147     */
148    private static Object[] getVersion(RandomAccessFile raf) throws IOException {
149        String version = null;
150        byte[] bytesMagic = SIG.getBytes(CHARSET_NAME);
151        byte[] bytes = new byte[bytesMagic.length];
152        long index = raf.length();
153        index -= bytesMagic.length;
154        readFully(raf, index, bytes); // 读取SIG标记
155        if (Arrays.equals(bytes, bytesMagic)) {
156            bytes = new byte[2];
157            index -= bytes.length;
158            readFully(raf, index, bytes); // 读取版本号长床
159            int lengthVersion = stream2Short(bytes, 0);
160            index -= lengthVersion;
161            byte[] bytesVersion = new byte[lengthVersion];
162            readFully(raf, index, bytesVersion); // 读取内容
163            version = new String(bytesVersion, CHARSET_NAME);
164        }
165        return new Object[] { index, version };
166    }
167
168    /**
169     * RandomAccessFile seek and readFully
170     * @param raf
171     * @param index
172     * @param buffer
173     * @throws IOException
174     */
175    private static void readFully(RandomAccessFile raf, long index, byte[] buffer) throws IOException {
176        raf.seek(index);
177        raf.readFully(buffer);
178    }
179
180    /**
181     * 读取数据劂枠道号
182     * @param path 文件路埄
183     * @param password 解密密钥
184     * @return 被该工具写入的数据劂枠道号
185     */
186    public static String readContent(File path, String password) {
187        try {
188            return new String(read(path, password), CHARSET_NAME);
189        } catch (Exception ignore) {
190        }
191        return null;
192    }
193
194    /**
195     * Android平台读取枠道号
196     * @param context Android侭的android.content.Context对象
197     * @param mcptoolPassword mcptool解密密钥
198     * @param defValue 读取䞍到时甚该倌䜜䞺默讀倌
199     * @return
200     */
201    public static String getChannelId(Object context, String defValue) {
202        String content = MCPTool.readContent(new File(getPackageCodePath(context)), PAS);
203        return content == null || content.length() == 0 ? defValue : content;
204    }
205
206    /**
207     * 获取已安装apk文件的存傚路埄这里䜿甚反射因䞺MCPTool项目本身䞍需芁富入Android的运行库
208     * @param context Android侭的Context对象
209     * @return
210     */
211    private static String getPackageCodePath(Object context) {
212        try {
213            return (String) context.getClass().getMethod("getPackageCodePath").invoke(context);
214        } catch (Exception ignore) {
215        }
216        return null;
217    }
218
219    /**
220     * 加密
221     * @param password
222     * @param content
223     * @return
224     * @throws Exception
225     */
226    private static byte[] encrypt(String password, byte[] content) throws Exception {
227        return cipher(Cipher.ENCRYPT_MODE, password, content);
228    }
229
230    /**
231     * 解密
232     * @param password
233     * @param content
234     * @return
235     * @throws Exception
236     */
237    private static byte[] decrypt(String password, byte[] content) throws Exception {
238        return cipher(Cipher.DECRYPT_MODE, password, content);
239    }
240
241    /**
242     * 加解密
243     * @param cipherMode
244     * @param password
245     * @param content
246     * @return
247     * @throws Exception
248     */
249    private static byte[] cipher(int cipherMode, String password, byte[] content) throws Exception {
250        DESKeySpec dks = new DESKeySpec(password.getBytes(CHARSET_NAME));
251        SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
252        Key secretKey = keyFactory.generateSecret(dks);
253        Cipher cipher = Cipher.getInstance("DES/CBC/PKCS5Padding");
254        IvParameterSpec spec = new IvParameterSpec(IV);
255        cipher.init(cipherMode, secretKey, spec);
256        return cipher.doFinal(content);
257    }
258
259    /**
260     * short蜬换成字节数组小端序
261     * @param data
262     * @return
263     */
264    private static short stream2Short(byte[] stream, int offset) {
265        ByteBuffer buffer = ByteBuffer.allocate(2);
266        buffer.order(ByteOrder.LITTLE_ENDIAN);
267        buffer.put(stream[offset]);
268        buffer.put(stream[offset + 1]);
269        return buffer.getShort(0);
270    }
271
272    /**
273     * 字节数组蜬换成short小端序
274     * @param stream
275     * @param offset
276     * @return
277     */
278    private static byte[] short2Stream(short data) {
279        ByteBuffer buffer = ByteBuffer.allocate(2);
280        buffer.order(ByteOrder.LITTLE_ENDIAN);
281        buffer.putShort(data);
282        buffer.flip();
283        return buffer.array();
284    }
285
286    /**
287     * nio高速拷莝文件
288     * @param source
289     * @param target
290     * @return
291     * @throws IOException
292     */
293    private static boolean nioTransferCopy(File source, File target) throws IOException {
294        FileChannel in = null;
295        FileChannel out = null;
296        FileInputStream inStream = null;
297        FileOutputStream outStream = null;
298        try {
299            File parent = target.getParentFile();
300            if (!parent.exists()) {
301                parent.mkdirs();
302            }
303            inStream = new FileInputStream(source);
304            outStream = new FileOutputStream(target);
305            in = inStream.getChannel();
306            out = outStream.getChannel();
307            return in.transferTo(0, in.size(), out) == in.size();
308        } finally {
309            close(inStream);
310            close(in);
311            close(outStream);
312            close(out);
313        }
314    }
315
316    /**
317     * 关闭数据流
318     * @param closeable
319     */
320    private static void close(Closeable closeable) {
321        if (closeable != null) {
322            try {
323                closeable.close();
324            } catch (IOException ignore) {
325            }
326        }
327    }
328
329//      /**
330//       * 简单测试代码段
331//       * @param args
332//       * @throws Exception
333//       */
334//      public static void test() throws Exception {
335//              String content = "abc";
336//              String password = "123456789";
337//              System.out.println("content = " + content);
338//              String contentE = new String(encrypt(password, content.getBytes(CHARSET_NAME)), CHARSET_NAME);
339//              System.out.println("contentE = " + contentE);
340//              String contentD = new String(decrypt(password, contentE.getBytes(CHARSET_NAME)), CHARSET_NAME);
341//              System.out.println("contentD = " + contentD);
342//
343//      }
344
345    /**
346     * jar呜什行的入口方法
347     * @param args
348     * @throws Exception
349     */
350    public static void main(String[] args) throws Exception {
351//              写入枠道号
352//              args = "-path D:/111.apk -outdir D:/111/ -contents googleplay;m360; -password 12345678".split(" ");
353//              查看工具皋序版本号
354//              args = "-version".split(" ");
355//              读取枠道号
356//              args = "-path D:/111_m360.apk -password 12345678".split(" ");
357
358        long time = System.currentTimeMillis();
359        String cmdPath  = "-path";
360        String cmdOutdir  = "-outdir";
361        String cmdContents  = "-contents";
362        String cmdContentsdir = "-contentsdir";
363        String cmdPassword  = "-password";
364        String cmdVersion  = "-version";
365        String help = "甚法java -jar MCPTool.jar [" + cmdPath + "] [arg0] [" + cmdOutdir + "] [arg1] [" + cmdContents + "] [arg2] [" + cmdPassword + "] [arg3]"
366                + "\n" + cmdPath + "            APK文件路埄"
367                + "\n" + cmdOutdir + "          蟓出路埄可选默讀蟓出到APK文件同䞀级目圕"
368                + "\n" + cmdContents + "        写入内容集合倚䞪内容之闎甚“;”分割linux平台请圚“;”前加“\\”蜬义笊劂googleplay;m360; 圓没有" + cmdContents + "”参数时蟓出已有文件䞭的contents"
369                + "\n" + cmdPassword + "        加密密钥可选长床8䜍以䞊劂果没有该参数䞍加密"
370                + "\n" + cmdVersion + " 星瀺MCPTool版本号"
371                + "\n" + cmdContentsdir + "     äŒ å…¥æž é“号文件甚换行笊分隔"
372                + "\n䟋劂"
373                + "\n写入java -jar MCPTool.jar -path D:/test.apk -outdir ./ -contents googleplay;m360;"
374                + "\n读取java -jar MCPTool.jar -path D:/test.apk ";
375
376        if (args.length == 0 || args[0] == null || args[0].trim().length() == 0) {
377            System.out.println(help);
378        } else {
379            if (args.length > 0) {
380                if (args.length == 1 && cmdVersion.equals(args[0])) {
381                    System.out.println("version: " + VERSION_1_1);
382                } else {
383                    Map<String, String> argsMap = new LinkedHashMap<String, String>();
384                    for (int i = 0; i < args.length; i += 2) {
385                        if (i + 1 < args.length) {
386                            if (args[i + 1].startsWith("-")) {
387                                throw new IllegalStateException("args is error, help: \n" + help);
388                            } else {
389                                argsMap.put(args[i], args[i + 1]);
390                            }
391                        }
392                    }
393                    System.out.println("argsMap = " + argsMap);
394                    File path = argsMap.containsKey(cmdPath) ? new File(argsMap.get(cmdPath)) : null;
395                    String parent = path == null? null : (path.getParent() == null ? "./" : path.getParent());
396                    File outdir = parent == null ? null : new File(argsMap.containsKey(cmdOutdir) ? argsMap.get(cmdOutdir) : parent);
397                    String[] contents = argsMap.containsKey(cmdContents) ? argsMap.get(cmdContents).split(";") : null;
398                    if(contents == null && argsMap.containsKey(cmdContentsdir)){
399                        String dir = argsMap.get(cmdContentsdir);
400                        File marketFile = new File(dir);
401                        BufferedReader reader = null;
402                        ArrayList<String> list = new ArrayList<>();
403                        try {
404                            reader = new BufferedReader(new FileReader(marketFile));
405                            String tempString = null;
406                            int line = 1;
407                            // 䞀次读入䞀行盎到读入null䞺文件结束
408                            while ((tempString = reader.readLine()) != null) {
409                                // 星瀺行号
410                                list.add(tempString);
411                                line++;
412                            }
413                            reader.close();
414                        } catch (IOException e) {
415                            e.printStackTrace();
416                        } finally {
417                            if (reader != null) {
418                                try {
419                                    reader.close();
420                                } catch (IOException e1) {
421                                }
422                            }
423                            if(list != null){
424                                contents = new String[list.size()];
425                                list.toArray(contents);
426                            }
427                        }
428
429                    }
430//                                      String password = argsMap.get(cmdPassword);
431                    String password = PAS;
432                    if (path != null) {
433                        System.out.println("path: " + path);
434                        System.out.println("outdir: " + outdir);
435                        if (contents != null && contents.length > 0) {
436                            System.out.println("contents: " + Arrays.toString(contents));
437                        }
438                        System.out.println("password: " + password);
439                        if (contents == null || contents.length == 0) { // 读取数据
440                            System.out.println("content: " + readContent(path, password));
441                        } else { // 写入数据
442                            String fileName = path.getName();
443                            int dot = fileName.lastIndexOf(".");
444                            String prefix = fileName.substring(0, dot);
445                            String suffix = fileName.substring(dot);
446                            for (String content : contents) {
447                                File target = new File(outdir, prefix + "_" + content + suffix);
448                                if (nioTransferCopy(path, target)) {
449                                    write(target, content, password);
450                                }
451                            }
452                        }
453                    }
454                }
455            }
456        }
457        System.out.println("time" + (System.currentTimeMillis() - time));
458    }
459}