commit 0a1addb2db219cbbbc9184be823bb1c069b9ae89 Author: ecpvint Date: Thu Aug 15 18:43:04 2024 +0800 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..956c004 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/build +/release \ No newline at end of file diff --git a/app.iml b/app.iml new file mode 100644 index 0000000..f8e37cb --- /dev/null +++ b/app.iml @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..3c652cb --- /dev/null +++ b/build.gradle @@ -0,0 +1,21 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 15 + defaultConfig { + applicationId "net.droidtech.ncmdecoder" + minSdkVersion 15 + versionCode 3 + versionName "1.0.6-beta" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) +} diff --git a/proguard-rules.pro b/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..800fb2e --- /dev/null +++ b/readme.md @@ -0,0 +1,7 @@ +## 影沐纪念项目 + +#### NCM解密 + +

解密网易音乐下载后的NCM加密

+ +

安卓项目

\ No newline at end of file diff --git a/src/androidTest/java/net/droidtech/ncmdecoder/ExampleInstrumentedTest.java b/src/androidTest/java/net/droidtech/ncmdecoder/ExampleInstrumentedTest.java new file mode 100644 index 0000000..18bb249 --- /dev/null +++ b/src/androidTest/java/net/droidtech/ncmdecoder/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package net.droidtech.ncmdecoder; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("net.droidtech.ncmdecoder", appContext.getPackageName()); + } +} diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml new file mode 100644 index 0000000..f838cf6 --- /dev/null +++ b/src/main/AndroidManifest.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/java/net/droidtech/io/DroidFile.java b/src/main/java/net/droidtech/io/DroidFile.java new file mode 100644 index 0000000..2a30ff4 --- /dev/null +++ b/src/main/java/net/droidtech/io/DroidFile.java @@ -0,0 +1,119 @@ +package net.droidtech.io; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.security.MessageDigest; + +public class DroidFile extends File{ + + private static final long serialVersionUID = 7311643678032690177L; + private File file=null; + + public DroidFile(String path){ + super(path); + file=new File(path); + } + public DataInputStream getInputStream(){ + try { + if(file.exists()&&!file.isDirectory()){ + DataInputStream in=new DataInputStream(new FileInputStream(file)); + return in; + }else{ + return null; + } + } catch (Exception e) { + // TODO Auto-generated catch block + return null; + } + + } + + public DataOutputStream getOutputStream(){ + try { + if(file.exists()&&!file.isDirectory()){ + DataOutputStream out=new DataOutputStream(new FileOutputStream(file)); + return out; + }else{ + return null; + } + } catch (Exception e) { + // TODO Auto-generated catch block + return null; + } + } + + public void copyTo(File target){ + DroidFile file=new DroidFile(target.getAbsolutePath()); + if(file.createNewFile()){ + StreamWriter writer=new StreamWriter(); + writer.writeStream(this.getInputStream(),file); + } + } + + @Override + public boolean createNewFile(){ + try { + File parentDir=new File(file.getParent()); + if(!parentDir.exists()){ + parentDir.mkdirs(); + } + file.createNewFile(); + } catch (IOException e) { + // TODO Auto-generated catch block + return false; + } + return file.exists(); + } + + @Override + public boolean mkdir(){ + file.mkdirs(); + return file.exists(); + } + + public String getMD5String(){ + byte[] md5data=getMD5Bytes(); + if(md5data!=null){ + return new BigInteger(1,md5data).toString(16); + }else{ + return null; + } + } + + public byte[] getMD5Bytes(){ + DataInputStream in=this.getInputStream(); + if(in==null){ + return null; + } + try { + byte[] buffer=new byte[4096]; + MessageDigest md5=MessageDigest.getInstance("MD5"); + while(true){ + int count=in.read(buffer); + if(count!=-1){ + md5.update(buffer,0,count); + }else{ + in.close(); + break; + } + } + return md5.digest(); + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + return null; + } + } + + public DroidFile getChildFile(String name){ + if(!this.isDirectory()){ + return null; + } + return new File(this.getAbsolutePath()+File.separator+name).exists()?new DroidFile(this.getAbsolutePath()+File.separator+name):null; + } +} diff --git a/src/main/java/net/droidtech/io/StreamReader.java b/src/main/java/net/droidtech/io/StreamReader.java new file mode 100644 index 0000000..9de3123 --- /dev/null +++ b/src/main/java/net/droidtech/io/StreamReader.java @@ -0,0 +1,44 @@ +package net.droidtech.io; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; + +public class StreamReader { + + public String readStream(InputStream in,String encoding){ + try { + return new String(readStream(in),encoding); + } catch (UnsupportedEncodingException e) { + throw new IllegalArgumentException("Undefined encoding:"+encoding); + } + + } + + public byte[] readStream(InputStream in){ + + if(in==null){ + return null; + } + + ByteArrayOutputStream data=new ByteArrayOutputStream(); + byte[] buffer=new byte[4096]; + + try{ + while(true){ + int count=in.read(buffer); + if(count!=-1){ + data.write(buffer,0,count); + }else{ + in.close(); + data.close(); + break; + } + } + return data.toByteArray(); + }catch(IOException e){ + return null; + } + } +} diff --git a/src/main/java/net/droidtech/io/StreamWriter.java b/src/main/java/net/droidtech/io/StreamWriter.java new file mode 100644 index 0000000..cc20853 --- /dev/null +++ b/src/main/java/net/droidtech/io/StreamWriter.java @@ -0,0 +1,48 @@ +package net.droidtech.io; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +public class StreamWriter { + + public void writeStream(InputStream in,File outFile){ + writeStream(in,outFile,false); + } + + public void writeStream(InputStream in,File outFile,boolean append){ + + byte[] buffer=new byte[4096]; + + if(in==null||outFile==null){ + return; + } + + try{ + + DataOutputStream out=new DataOutputStream(new FileOutputStream(outFile,append)); + DataInputStream dataInput=new DataInputStream(in); + + while(true){ + + int count=dataInput.read(buffer); + if(count!=-1){ + out.write(buffer,0,count); + }else{ + dataInput.close(); + out.close(); + break; + } + } + + }catch(IOException e){ + + throw new IllegalStateException(e.getMessage()); + + } + } + +} diff --git a/src/main/java/net/droidtech/ncmdecoder/NCMDecoderThread.java b/src/main/java/net/droidtech/ncmdecoder/NCMDecoderThread.java new file mode 100644 index 0000000..332a4a0 --- /dev/null +++ b/src/main/java/net/droidtech/ncmdecoder/NCMDecoderThread.java @@ -0,0 +1,189 @@ +package net.droidtech.ncmdecoder; + +import android.os.Handler; +import android.os.Message; + +import net.droidtech.io.DroidFile; +import net.droidtech.ncmtypes.NCMApicBlock; +import net.droidtech.ncmtypes.NCMCrc32Block; +import net.droidtech.ncmtypes.NCMKeyBlock; +import net.droidtech.ncmtypes.NCMMetaInfoBlock; +import net.droidtech.ncmtypes.NCMTypeHeader; +import net.droidtech.utils.Formatter; +import net.droidtech.utils.RC4; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.DataInputStream; +import java.io.DataOutputStream; + +/** + * Created by root on 2020/4/9. + */ + +public class NCMDecoderThread extends Thread { + + private Handler handler; + private String filePath; + private NCMDecoderUI ui; + + public void setHandler(Handler handler){ + + this.handler=handler; + + } + + public void attachUI(NCMDecoderUI ui){ + + this.ui=ui; + + } + + public void setSourceNCMFile(String filePath){ + + this.filePath=filePath; + + } + + private void writeLog(String log){ + + this.writeLog(log,0); + + } + + private void writeLog(String log, int level){ + + Message msg=handler.obtainMessage(); + + msg.obj=log; + + if(level!=0){ + msg.what=level; + } + + msg.sendToTarget(); + + } + + @Override + public void run(){ + + DroidFile ncmfile=new DroidFile(filePath); + + this.writeLog(ui.getStringById(R.string.target_file)+filePath); + + if(!ncmfile.exists()||!ncmfile.canRead()||ncmfile.isDirectory()){ + + this.writeLog(ui.getStringById(R.string.access_denied), NCMDecoderUI.LogHandler.RESULT_FAILED); + + return; + + } + + DataInputStream ncmstream=ncmfile.getInputStream(); + + NCMTypeHeader hdr=new NCMTypeHeader(ncmstream); + + this.writeLog(ui.getStringById(R.string.is_ncm_file)+(hdr.isNcmHeader()?ui.getStringById(R.string.yes):ui.getStringById(R.string.no))); + + if(!hdr.isNcmHeader()){ + + this.writeLog(ui.getStringById(R.string.not_an_ncm_file), NCMDecoderUI.LogHandler.RESULT_FAILED); + + return; + + } + + NCMKeyBlock keyblk=new NCMKeyBlock(ncmstream); + + this.writeLog(ui.getStringById(R.string.can_decrypt)+(keyblk.isSupportedKeyBlock()?ui.getStringById(R.string.yes):ui.getStringById(R.string.no))); + + if(!keyblk.isSupportedKeyBlock()){ + + this.writeLog(ui.getStringById(R.string.unsupported_ncm_file), NCMDecoderUI.LogHandler.RESULT_FAILED); + + return; + + } + + byte[] sbox= RC4.getSbox(keyblk.getRC4Key()); + + NCMMetaInfoBlock metainfo=new NCMMetaInfoBlock(ncmstream); + + //System.out.println("歌曲元数据: "+metainfo.getJsonMetaInfo()); + + NCMCrc32Block crc32=new NCMCrc32Block(ncmstream); + + this.writeLog(ui.getStringById(R.string.crc32_check_result)+ui.getStringById(R.string.unimplemented)); + + NCMApicBlock pic=new NCMApicBlock(ncmstream); + + try{ + + byte[] buffer=new byte[4096]; + + JSONObject json=new JSONObject(metainfo.getJsonMetaInfo()); + + String songName=Formatter.removeIllegalCharacters(json.getString("musicName")); + String artist=Formatter.removeIllegalCharacters(new JSONArray(new JSONArray(json.getString("artist")).getString(0)).getString(0)); + String format=json.getString("format"); + + if(pic.getPictureData()!=null){ + + this.writeLog(ui.getStringById(R.string.image_size)+pic.getPictureData().length+ui.getStringById(R.string.size_byte)); + + DroidFile outImg=new DroidFile(ncmfile.getParentFile().getAbsolutePath()+DroidFile.separator+songName+"_"+artist+"_pic.jpg"); + + outImg.createNewFile(); + + this.writeLog(ui.getStringById(R.string.writing_file)+outImg.getAbsolutePath()); + + DataOutputStream imgOutStream=outImg.getOutputStream(); + imgOutStream.write(pic.getPictureData()); + + imgOutStream.close(); + + }else{ + + this.writeLog(ui.getStringById(R.string.apic_not_found)); + + } + + DroidFile outFile=new DroidFile(ncmfile.getParentFile().getAbsolutePath()+DroidFile.separator+songName+"_"+artist+"."+format); + outFile.createNewFile(); + + DataOutputStream out=outFile.getOutputStream(); + + this.writeLog(ui.getStringById(R.string.writing_file)+outFile.getAbsolutePath()); + + while(true){ + + int length=ncmstream.read(buffer); + + if(length==-1){ + + break; + + } + + RC4.decrypt_rc4_prga(buffer,length,sbox); + + out.write(buffer,0, length); + + } + + ncmstream.close(); + out.close(); + + this.writeLog(ui.getStringById(R.string.decryption_success), NCMDecoderUI.LogHandler.RESULT_SUCCESS); + + }catch(Exception e){ + + this.writeLog(e.getMessage(), NCMDecoderUI.LogHandler.RESULT_FAILED); + + } + + } + +} diff --git a/src/main/java/net/droidtech/ncmdecoder/NCMDecoderUI.java b/src/main/java/net/droidtech/ncmdecoder/NCMDecoderUI.java new file mode 100644 index 0000000..8d94b90 --- /dev/null +++ b/src/main/java/net/droidtech/ncmdecoder/NCMDecoderUI.java @@ -0,0 +1,97 @@ +package net.droidtech.ncmdecoder; + +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.app.Activity; +import android.os.Environment; +import android.os.Handler; +import android.os.Message; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; + +public class NCMDecoderUI extends Activity { + + private LinearLayout layout; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_ncmdecoder_ui); + + Intent intent = getIntent(); + Uri uri = intent.getData(); + + if(uri==null){ + + this.finish(); + return; + + } + + String uriPath=uri.getPath(); + + if(uriPath.startsWith("/root")){ + + uriPath=uriPath.replace("/root",""); + + } + + this.setTitle(R.string.window_title); + + this.setFinishOnTouchOutside(false); + + this.layout=(LinearLayout)this.findViewById(R.id.logger); + + NCMDecoderThread thread=new NCMDecoderThread(); + + thread.setHandler(new LogHandler()); + thread.setSourceNCMFile(uriPath); + thread.attachUI(this); + + thread.start(); + + + } + + public String getStringById(int id){ + + return this.getResources().getString(id); + + } + + public class LogHandler extends Handler{ + + public static final int RESULT_FAILED=0xFF; + public static final int RESULT_SUCCESS=0xFC; + private ViewGroup.LayoutParams textViewLayout=new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.MATCH_PARENT); + + @Override + public void handleMessage(Message msg){ + + TextView message=new TextView(NCMDecoderUI.this); + + message.setText((CharSequence)msg.obj); + + message.setLayoutParams(textViewLayout); + + if(msg.what==LogHandler.RESULT_FAILED){ + + message.setTextColor(NCMDecoderUI.this.getResources().getColor(android.R.color.holo_red_light)); + + } + + if(msg.what==LogHandler.RESULT_SUCCESS){ + + message.setTextColor(NCMDecoderUI.this.getResources().getColor(android.R.color.holo_green_light)); + + } + + NCMDecoderUI.this.layout.addView(message); + + } + + } + +} diff --git a/src/main/java/net/droidtech/ncmtypes/NCMApicBlock.java b/src/main/java/net/droidtech/ncmtypes/NCMApicBlock.java new file mode 100644 index 0000000..92a38b7 --- /dev/null +++ b/src/main/java/net/droidtech/ncmtypes/NCMApicBlock.java @@ -0,0 +1,58 @@ +package net.droidtech.ncmtypes; + +import java.io.DataInputStream; + +import net.droidtech.utils.ByteMatcher; +import net.droidtech.utils.IntegerUtils; + +public class NCMApicBlock { + + public static final int APIC_BLOCK_START=1; + + private byte[] mPic; + + public NCMApicBlock(DataInputStream in){ + + try{ + + if(in.read()!=NCMApicBlock.APIC_BLOCK_START){ + + return; + + } + + byte[] magic_len=new byte[4]; + + in.readFully(magic_len); + + byte[] pic_len_data=new byte[4]; + + in.readFully(pic_len_data); + + if(!ByteMatcher.isMatch(magic_len, pic_len_data)){ + + return; + + } + + int pic_len=IntegerUtils.changeEndian(IntegerUtils.byteToInteger(pic_len_data)); + + this.mPic=new byte[pic_len]; + + in.readFully(this.mPic); + + }catch(Exception e){ + + e.printStackTrace(); + + } + + } + + public byte[] getPictureData(){ + + return this.mPic; + + } + +} diff --git a/src/main/java/net/droidtech/ncmtypes/NCMCrc32Block.java b/src/main/java/net/droidtech/ncmtypes/NCMCrc32Block.java new file mode 100644 index 0000000..71d21cf --- /dev/null +++ b/src/main/java/net/droidtech/ncmtypes/NCMCrc32Block.java @@ -0,0 +1,51 @@ +package net.droidtech.ncmtypes; + +import java.io.DataInputStream; + +import net.droidtech.utils.IntegerUtils; + +public class NCMCrc32Block { + + //unimplemented + + private boolean mIsCrc32Matched; + private byte[] mCrc32; + + public NCMCrc32Block(DataInputStream in){ + + try{ + + byte[] crc32=new byte[4]; + in.readFully(crc32); + + this.mCrc32=crc32; + + this.mIsCrc32Matched=true; + + }catch(Exception e){ + + e.printStackTrace(); + + } + + } + + public boolean isCrc32Matched(){ + + return this.mIsCrc32Matched; + + } + + public byte[] getCrc32Byte(){ + + return this.mCrc32; + + } + + public int getCrc32Int(){ + + return IntegerUtils.byteToInteger(this.mCrc32); + + } + +} diff --git a/src/main/java/net/droidtech/ncmtypes/NCMKeyBlock.java b/src/main/java/net/droidtech/ncmtypes/NCMKeyBlock.java new file mode 100644 index 0000000..c7345c2 --- /dev/null +++ b/src/main/java/net/droidtech/ncmtypes/NCMKeyBlock.java @@ -0,0 +1,88 @@ +package net.droidtech.ncmtypes; + +import java.io.DataInputStream; + +import net.droidtech.utils.AESUtils; +import net.droidtech.utils.ByteMatcher; +import net.droidtech.utils.HexStringUtils; +import net.droidtech.utils.IntegerUtils; + +public class NCMKeyBlock{ + + public static final byte[] KEY_BLOCK_MAGIC={0x01,0x70}; + + public static final int XOR_MAIN_KEY=0x64; + public static final String AES_MAIN_KEY="687A4852416D736F356B496E62617857"; + + public static final String KEY_PADDING="neteasecloudmusic"; + + private byte[] mKeyData=null; + private boolean mIsSupportedKeyBlock=false; + + public NCMKeyBlock(DataInputStream in){ + + try{ + + byte[] magic=new byte[NCMKeyBlock.KEY_BLOCK_MAGIC.length]; + + in.readFully(magic); + + /*if(!ByteMatcher.isMatch(NCMKeyBlock.KEY_BLOCK_MAGIC,magic)){ + + return; + + }*/ + + byte[] key_length_data=new byte[4]; + + in.readFully(key_length_data); + + int key_length=IntegerUtils.changeEndian(IntegerUtils.byteToInteger(key_length_data)); + + byte[] key=new byte[key_length]; + + in.readFully(key); + + for(int i=0;iNCMTypeHeader.NCM_HEADER_LENGTH){ + + return; + + } + + if(new String(ncm_hdr).equals(NCMTypeHeader.NCM_HEADER)){ + + this.mIsNcmFmt=true; + + } + + }catch(Exception e){ + + e.printStackTrace(); + + return; + + } + + } + + public boolean isNcmHeader(){ + + return this.mIsNcmFmt; + + } + +} diff --git a/src/main/java/net/droidtech/utils/AESUtils.java b/src/main/java/net/droidtech/utils/AESUtils.java new file mode 100644 index 0000000..8b22be2 --- /dev/null +++ b/src/main/java/net/droidtech/utils/AESUtils.java @@ -0,0 +1,83 @@ +package net.droidtech.utils; + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.spec.SecretKeySpec; + +public class AESUtils { + + public static final int MODE_ENCRYPT=Cipher.ENCRYPT_MODE; + public static final int MODE_DECRYPT=Cipher.DECRYPT_MODE; + + public static final int DEFAULT_KEY_LENGTH=128; + + private SecretKeySpec keySpec; + private Cipher cipher; + + public void setKey(byte[] key){ + + if(key==null){ + return; + } + + if(key.length<16){ + return; + } + + this.keySpec = new SecretKeySpec(key, "AES"); + + } + + public static byte[] generateRandomKey(){ + + return AESUtils.generateRandomKey(256); + + } + + public static byte[] generateRandomKey(int length){ + + try { + KeyGenerator keyGenerator = KeyGenerator.getInstance("AES"); + keyGenerator.init(length); + return keyGenerator.generateKey().getEncoded(); + } catch (Exception e) { + return null; + } + + } + + public void setMode(int mode){ + + try { + this.cipher = Cipher.getInstance("AES/ECB/NoPadding"); + cipher.init(mode, this.keySpec); + } catch (Exception e) { + throw new java.lang.RuntimeException(e.getMessage()); + } + + } + + public byte[] encrypt(byte[] data){ + + try { + byte[] result=this.cipher.doFinal(data); + return result; + } catch (Exception e) { + e.printStackTrace(); + return null; + } + + } + + public byte[] decrypt(byte[] data){ + + try { + byte[] result=this.cipher.doFinal(data); + return result; + } catch (Exception e) { + return null; + } + + } + +} diff --git a/src/main/java/net/droidtech/utils/Base64.java b/src/main/java/net/droidtech/utils/Base64.java new file mode 100644 index 0000000..e426068 --- /dev/null +++ b/src/main/java/net/droidtech/utils/Base64.java @@ -0,0 +1,116 @@ +package net.droidtech.utils; + +import java.io.ByteArrayOutputStream; + +public class Base64 { + + private Base64(){ + + + + } + + public static String encode(byte[] data){ + + char[] tbl = { + 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P', + 'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f', + 'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v', + 'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/' + }; + + StringBuilder buffer = new StringBuilder(); + + int pad = 0; + + for (int i = 0; i < data.length; i += 3) { + + int b = ((data[i] & 0xFF) << 16) & 0xFFFFFF; + if (i + 1 < data.length) { + b |= (data[i+1] & 0xFF) << 8; + } else { + pad++; + } + if (i + 2 < data.length) { + b |= (data[i+2] & 0xFF); + } else { + pad++; + } + + for (int j = 0; j < 4 - pad; j++) { + int c = (b & 0xFC0000) >> 18; + buffer.append(tbl[c]); + b <<= 6; + } + } + for (int j = 0; j < pad; j++) { + buffer.append("="); + } + + return buffer.toString(); + + } + + + public static byte[] decode(String data){ + + int[] tbl = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54, + 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, + 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, + 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, + 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, + 48, 49, 50, 51, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 + }; + + byte[] bytes = data.getBytes(); + + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + + for (int i = 0; i < bytes.length; ) { + + int b = 0; + if (tbl[bytes[i]] != -1) { + b = (tbl[bytes[i]] & 0xFF) << 18; + } + // skip unknown characters + else { + i++; + continue; + } + + int num = 0; + if (i + 1 < bytes.length && tbl[bytes[i+1]] != -1) { + b = b | ((tbl[bytes[i+1]] & 0xFF) << 12); + num++; + } + if (i + 2 < bytes.length && tbl[bytes[i+2]] != -1) { + b = b | ((tbl[bytes[i+2]] & 0xFF) << 6); + num++; + } + if (i + 3 < bytes.length && tbl[bytes[i+3]] != -1) { + b = b | (tbl[bytes[i+3]] & 0xFF); + num++; + } + + while (num > 0) { + int c = (b & 0xFF0000) >> 16; + buffer.write((char)c); + b <<= 8; + num--; + } + i += 4; + } + return buffer.toByteArray(); + } + +} diff --git a/src/main/java/net/droidtech/utils/ByteMatcher.java b/src/main/java/net/droidtech/utils/ByteMatcher.java new file mode 100644 index 0000000..4d20383 --- /dev/null +++ b/src/main/java/net/droidtech/utils/ByteMatcher.java @@ -0,0 +1,33 @@ +package net.droidtech.utils; + +public class ByteMatcher { + + private ByteMatcher(){ + + + + } + + public static boolean isMatch(byte[] src, byte[] dst){ + + if(src.length!=dst.length){ + + return false; + + } + + for(int i=0;i",":","|"}; + public static final String[] mReplaceTable={"\'","-","-","×","?","《","》",":","#"}; + + public static String removeIllegalCharacters(String arg){ + + String result=arg; + + for(int i=0;i2||items[i].length()<=0){ + throw new IllegalArgumentException("Unsupported string format"); + } + result.write(Integer.parseInt(items[i],16)); + } + + } + + return result.toByteArray(); + + } + + public static String byteToHexString(byte[] value){ + + return byteToHexString(value,null); + + } + + public static String byteToHexString(byte[] value, String separator){ + + StringBuffer result=new StringBuffer(); + + for(int i=0;i=0;i--){ + + result=result+(IntegerUtils.getUnsignedByte((byte)(var>>(i*8)))<<(24-i*8)); + + } + + return result; + + } + + public static int byteToInteger(byte[] data){ + + int result=0; + + if(data.length>4){ + + return 0; + + } + + for(int i=0;i<4;i++){ + + result=result+(IntegerUtils.getUnsignedByte(data[i])<<(24-i*8)); + + } + + return result; + + } + + public static int getUnsignedByte(byte var){ + + return var&0xFF; + + } + +} diff --git a/src/main/java/net/droidtech/utils/RC4.java b/src/main/java/net/droidtech/utils/RC4.java new file mode 100644 index 0000000..bbc076d --- /dev/null +++ b/src/main/java/net/droidtech/utils/RC4.java @@ -0,0 +1,59 @@ +package net.droidtech.utils; + +public class RC4 { + + private RC4(){ + + + } + + public static byte[] getSbox(byte[] key){ + + byte[] box=new byte[256]; + + for(int i=0;i=key.length){ + + key_offset=0; + + } + + box[i]=box[c]; + box[c]=(byte)swap; + + last_byte=c; + + } + + return box; + + } + + public static void decrypt_rc4_prga(byte[] data, int data_length, byte[] sbox){ + + for(int i=1;i<(data_length==0?(data.length+1):(data_length+1));i++){ + + int j = i & 0xff; + + data[i-1] ^= sbox[(sbox[j] + sbox[(sbox[j]+j)&0xff])&0xff]; + + } + + } + +} diff --git a/src/main/res/drawable/ic_launcher.png b/src/main/res/drawable/ic_launcher.png new file mode 100644 index 0000000..e3a2939 Binary files /dev/null and b/src/main/res/drawable/ic_launcher.png differ diff --git a/src/main/res/layout/activity_ncmdecoder_ui.xml b/src/main/res/layout/activity_ncmdecoder_ui.xml new file mode 100644 index 0000000..981bd31 --- /dev/null +++ b/src/main/res/layout/activity_ncmdecoder_ui.xml @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml new file mode 100644 index 0000000..59e426c --- /dev/null +++ b/src/main/res/values/strings.xml @@ -0,0 +1,21 @@ + + NCM 解码插件 + 日志 + 要解密的文件: + 解密失败: 目标文件有误,请确保插件有权访问该存储空间! + 是否为NCM格式: + 解密失败: 不是NCM格式! + 能否解密: + 解密失败: 不支持的加密方式! + CRC32检查结果: + (此功能未实现!) + 封面图像大小: + 字节 + 正在写入: + 未找到封面图片! + 解密完成! + + + + + diff --git a/src/test/java/net/droidtech/ncmdecoder/ExampleUnitTest.java b/src/test/java/net/droidtech/ncmdecoder/ExampleUnitTest.java new file mode 100644 index 0000000..ca6b673 --- /dev/null +++ b/src/test/java/net/droidtech/ncmdecoder/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package net.droidtech.ncmdecoder; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() throws Exception { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file